Compare commits
No commits in common. "82fd17c836b92c29b0cef18f53192b1f313075a2" and "3e5ecb93721bf574bc7e46c5169883ecfc968a2f" have entirely different histories.
82fd17c836
...
3e5ecb9372
39 changed files with 326 additions and 1059 deletions
|
|
@ -1,133 +0,0 @@
|
||||||
package blog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/admin/core"
|
|
||||||
"arimelody-web/admin/templates"
|
|
||||||
"arimelody-web/controller"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Handler(app *model.AppState) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
mux.Handle("/{id}", serveBlogPost(app))
|
|
||||||
mux.Handle("/", serveBlogIndex(app))
|
|
||||||
|
|
||||||
mux.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
blogPost struct {
|
|
||||||
*model.BlogPost
|
|
||||||
Author *model.Account
|
|
||||||
}
|
|
||||||
|
|
||||||
blogPostGroup struct {
|
|
||||||
Year int
|
|
||||||
Posts []*blogPost
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func serveBlogIndex(app *model.AppState) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
|
||||||
|
|
||||||
posts, err := controller.GetBlogPosts(app.DB, false, -1, 0)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog posts: %v\n", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
collections := []*blogPostGroup{}
|
|
||||||
collectionPosts := []*blogPost{}
|
|
||||||
collectionYear := -1
|
|
||||||
for i, post := range posts {
|
|
||||||
author, err := controller.GetAccountByID(app.DB, post.AuthorID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", post.ID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if collectionYear == -1 {
|
|
||||||
collectionYear = post.CreatedAt.Year()
|
|
||||||
}
|
|
||||||
|
|
||||||
authoredPost := blogPost{
|
|
||||||
BlogPost: post,
|
|
||||||
Author: author,
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.CreatedAt.Year() != collectionYear || i == len(posts) - 1 {
|
|
||||||
if i == len(posts) - 1 {
|
|
||||||
collectionPosts = append(collectionPosts, &authoredPost)
|
|
||||||
}
|
|
||||||
collections = append(collections, &blogPostGroup{
|
|
||||||
Year: collectionYear,
|
|
||||||
Posts: slices.Clone(collectionPosts),
|
|
||||||
})
|
|
||||||
collectionPosts = []*blogPost{}
|
|
||||||
collectionYear = post.CreatedAt.Year()
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionPosts = append(collectionPosts, &authoredPost)
|
|
||||||
}
|
|
||||||
|
|
||||||
type blogsData struct {
|
|
||||||
core.AdminPageData
|
|
||||||
TotalPosts int
|
|
||||||
Collections []*blogPostGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
err = templates.BlogsTemplate.Execute(w, blogsData{
|
|
||||||
AdminPageData: core.AdminPageData{
|
|
||||||
Session: session,
|
|
||||||
},
|
|
||||||
TotalPosts: len(posts),
|
|
||||||
Collections: collections,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin blog index: %v\n", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveBlogPost(app *model.AppState) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
|
||||||
|
|
||||||
blogID := r.PathValue("id")
|
|
||||||
|
|
||||||
post, err := controller.GetBlogPost(app.DB, blogID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("can't find blog with ID %s\n", blogID)
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type blogPostData struct {
|
|
||||||
core.AdminPageData
|
|
||||||
Post *model.BlogPost
|
|
||||||
}
|
|
||||||
|
|
||||||
err = templates.EditBlogTemplate.Execute(w, blogPostData{
|
|
||||||
AdminPageData: core.AdminPageData{
|
|
||||||
Session: session,
|
|
||||||
},
|
|
||||||
Post: post,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin edit page for blog %s: %v\n", blogID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
|
|
||||||
"arimelody-web/admin/account"
|
"arimelody-web/admin/account"
|
||||||
"arimelody-web/admin/auth"
|
"arimelody-web/admin/auth"
|
||||||
"arimelody-web/admin/blog"
|
|
||||||
"arimelody-web/admin/core"
|
"arimelody-web/admin/core"
|
||||||
"arimelody-web/admin/logs"
|
"arimelody-web/admin/logs"
|
||||||
"arimelody-web/admin/music"
|
"arimelody-web/admin/music"
|
||||||
|
|
@ -25,10 +24,8 @@ func Handler(app *model.AppState) http.Handler {
|
||||||
mux.Handle("/totp", auth.LoginTOTPHandler(app))
|
mux.Handle("/totp", auth.LoginTOTPHandler(app))
|
||||||
mux.Handle("/logout", core.RequireAccount(auth.LogoutHandler(app)))
|
mux.Handle("/logout", core.RequireAccount(auth.LogoutHandler(app)))
|
||||||
|
|
||||||
mux.Handle("/logs", core.RequireAccount(logs.Handler(app)))
|
|
||||||
mux.Handle("/music/", core.RequireAccount(http.StripPrefix("/music", music.Handler(app))))
|
mux.Handle("/music/", core.RequireAccount(http.StripPrefix("/music", music.Handler(app))))
|
||||||
mux.Handle("/blogs/", core.RequireAccount(http.StripPrefix("/blogs", blog.Handler(app))))
|
mux.Handle("/logs", core.RequireAccount(logs.Handler(app)))
|
||||||
|
|
||||||
mux.Handle("/account/", core.RequireAccount(http.StripPrefix("/account", account.Handler(app))))
|
mux.Handle("/account/", core.RequireAccount(http.StripPrefix("/account", account.Handler(app))))
|
||||||
|
|
||||||
mux.Handle("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -45,13 +42,13 @@ func Handler(app *model.AppState) http.Handler {
|
||||||
view.ServeFiles("./admin/static"))).ServeHTTP(w, r)
|
view.ServeFiles("./admin/static"))).ServeHTTP(w, r)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mux.Handle("/", core.RequireAccount(adminIndexHandler(app)))
|
mux.Handle("/", core.RequireAccount(AdminIndexHandler(app)))
|
||||||
|
|
||||||
// response wrapper to make sure a session cookie exists
|
// response wrapper to make sure a session cookie exists
|
||||||
return core.EnforceSession(app, mux)
|
return core.EnforceSession(app, mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminIndexHandler(app *model.AppState) http.Handler {
|
func AdminIndexHandler(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) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
|
||||||
|
|
@ -156,8 +156,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
|
||||||
func serveNewCredit(app *model.AppState) http.Handler {
|
func serveNewCredit(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) {
|
||||||
split := strings.Split(r.URL.Path, "/")
|
artistID := strings.Split(r.URL.Path, "/")[3]
|
||||||
artistID := split[len(split) - 1]
|
|
||||||
artist, err := controller.GetArtist(app.DB, artistID)
|
artist, err := controller.GetArtist(app.DB, artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
||||||
|
|
@ -233,8 +232,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
|
||||||
func serveNewTrack(app *model.AppState) http.Handler {
|
func serveNewTrack(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) {
|
||||||
split := strings.Split(r.URL.Path, "/")
|
trackID := strings.Split(r.URL.Path, "/")[3]
|
||||||
trackID := split[len(split) - 1]
|
|
||||||
track, err := controller.GetTrack(app.DB, trackID)
|
track, err := controller.GetTrack(app.DB, trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch track %s: %s\n", trackID, err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch track %s: %s\n", trackID, err)
|
||||||
|
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"arimelody-web/admin/core"
|
|
||||||
"arimelody-web/admin/templates"
|
|
||||||
"arimelody-web/controller"
|
|
||||||
"arimelody-web/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
var action string = ""
|
|
||||||
if len(slices) > 1 {
|
|
||||||
action = slices[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(releaseID) > 0 {
|
|
||||||
serveRelease(app, releaseID, action).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReleasesData struct {
|
|
||||||
core.AdminPageData
|
|
||||||
Releases []*model.Release
|
|
||||||
}
|
|
||||||
|
|
||||||
releases, err := controller.GetAllReleases(app.DB, false, 0, true)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = templates.ReleasesTemplate.Execute(w, ReleasesData{
|
|
||||||
AdminPageData: core.AdminPageData{
|
|
||||||
Path: r.URL.Path,
|
|
||||||
Session: session,
|
|
||||||
},
|
|
||||||
Releases: releases,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to serve releases page: %s\n", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveRelease(app *model.AppState, releaseID string, action string) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
|
||||||
|
|
||||||
release, err := controller.GetRelease(app.DB, releaseID, true)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(action) > 0 {
|
|
||||||
switch action {
|
|
||||||
case "editcredits":
|
|
||||||
serveEditCredits(release).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case "addcredit":
|
|
||||||
serveAddCredit(app, release).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case "newcredit":
|
|
||||||
serveNewCredit(app).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case "editlinks":
|
|
||||||
serveEditLinks(release).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case "edittracks":
|
|
||||||
serveEditTracks(release).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case "addtrack":
|
|
||||||
serveAddTrack(app, release).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case "newtrack":
|
|
||||||
serveNewTrack(app).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReleaseResponse struct {
|
|
||||||
core.AdminPageData
|
|
||||||
Release *model.Release
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
|
||||||
|
|
||||||
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
|
|
||||||
AdminPageData: core.AdminPageData{ Path: r.URL.Path, Session: session },
|
|
||||||
Release: release,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveEditCredits(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)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fetch artists not on %s: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type response struct {
|
|
||||||
ReleaseID string;
|
|
||||||
Artists []*model.Artist
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
err = templates.AddCreditTemplate.Execute(w, response{
|
|
||||||
ReleaseID: release.ID,
|
|
||||||
Artists: artists,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveNewCredit(app *model.AppState) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
split := strings.Split(r.URL.Path, "/")
|
|
||||||
artistID := split[len(split) - 1]
|
|
||||||
artist, err := controller.GetArtist(app.DB, artistID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if artist == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
err = templates.NewCreditTemplate.Execute(w, artist)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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.EditLinksTemplate.Execute(w, release)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveEditTracks(release *model.Release) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
|
|
||||||
type editTracksData struct { Release *model.Release }
|
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
|
||||||
|
|
||||||
err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release })
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fetch tracks not on %s: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type response struct {
|
|
||||||
ReleaseID string;
|
|
||||||
Tracks []*model.Track
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
err = templates.AddTrackTemplate.Execute(w, response{
|
|
||||||
ReleaseID: release.ID,
|
|
||||||
Tracks: tracks,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("WARN: Failed to add tracks component for %s: %s\n", release.ID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveNewTrack(app *model.AppState) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
split := strings.Split(r.URL.Path, "/")
|
|
||||||
trackID := split[len(split) - 1]
|
|
||||||
track, err := controller.GetTrack(app.DB, trackID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if track == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
err = templates.NewTrackTemplate.Execute(w, track)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -111,7 +111,7 @@ body {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
@ -252,6 +252,12 @@ a {
|
||||||
transition: color .1s ease-out, background-color .1s ease-out;
|
transition: color .1s ease-out, background-color .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
img.icon {
|
img.icon {
|
||||||
height: .8em;
|
height: .8em;
|
||||||
transition: filter .1s ease-out;
|
transition: filter .1s ease-out;
|
||||||
|
|
@ -264,6 +270,10 @@ code {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 .5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.cards {
|
.cards {
|
||||||
|
|
@ -277,7 +287,7 @@ code {
|
||||||
.card {
|
.card {
|
||||||
flex-basis: 40em;
|
flex-basis: 40em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
|
@ -355,7 +365,7 @@ a.delete:not(.button) {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10em;
|
border-radius: 10em;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
@ -374,27 +384,27 @@ button:active, .button:active {
|
||||||
|
|
||||||
.button.new, button.new {
|
.button.new, button.new {
|
||||||
color: var(--col-on-new);
|
color: var(--col-on-new);
|
||||||
background-color: var(--col-new);
|
background: var(--col-new);
|
||||||
}
|
}
|
||||||
.button.save, button.save {
|
.button.save, button.save {
|
||||||
color: var(--col-on-save);
|
color: var(--col-on-save);
|
||||||
background-color: var(--col-save);
|
background: var(--col-save);
|
||||||
}
|
}
|
||||||
.button.delete, button.delete {
|
.button.delete, button.delete {
|
||||||
color: var(--col-on-delete);
|
color: var(--col-on-delete);
|
||||||
background-color: var(--col-delete);
|
background: var(--col-delete);
|
||||||
}
|
}
|
||||||
.button:hover, button:hover {
|
.button:hover, button:hover {
|
||||||
color: var(--bg-3);
|
color: var(--bg-3);
|
||||||
background-color: var(--fg-3);
|
background: var(--fg-3);
|
||||||
}
|
}
|
||||||
.button:active, button:active {
|
.button:active, button:active {
|
||||||
color: var(--bg-2);
|
color: var(--bg-2);
|
||||||
background-color: var(--fg-0);
|
background: var(--fg-0);
|
||||||
}
|
}
|
||||||
.button[disabled], button[disabled] {
|
.button[disabled], button[disabled] {
|
||||||
color: var(--fg-0) !important;
|
color: var(--fg-0) !important;
|
||||||
background-color: var(--bg-3) !important;
|
background: var(--bg-3) !important;
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
|
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist:hover {
|
.artist:hover {
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
text-decoration: hover;
|
text-decoration: hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,4 @@ document.addEventListener("readystatechange", () => {
|
||||||
document.querySelectorAll(".artists-group .artist").forEach(el => {
|
document.querySelectorAll(".artists-group .artist").forEach(el => {
|
||||||
hijackClickEvent(el, el.querySelector("a.artist-name"))
|
hijackClickEvent(el, el.querySelector("a.artist-name"))
|
||||||
});
|
});
|
||||||
|
|
||||||
const newArtistBtn = document.getElementById("create-artist");
|
|
||||||
if (newArtistBtn) newArtistBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const id = prompt("Enter an ID for this artist:");
|
|
||||||
if (id == null || id == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/artist", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({id})
|
|
||||||
}).then(res => {
|
|
||||||
res.text().then(text => {
|
|
||||||
if (res.ok) {
|
|
||||||
location = "/admin/artists/" + id;
|
|
||||||
} else {
|
|
||||||
alert(text);
|
|
||||||
console.error(text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create artist. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
.blog-collection h2 {
|
|
||||||
margin: .5em 1em;
|
|
||||||
font-size: 1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--fg-0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost {
|
|
||||||
padding: 1em;
|
|
||||||
display: block;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--bg-2);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .title small {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: .6em;
|
|
||||||
transform: translateY(-0.1em);
|
|
||||||
color: var(--fg-0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .description {
|
|
||||||
margin: .5em 0 .6em 0;
|
|
||||||
color: var(--fg-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .meta {
|
|
||||||
margin: 0;
|
|
||||||
font-size: .8em;
|
|
||||||
color: var(--fg-0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .meta .author {
|
|
||||||
color: var(--fg-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .meta .author img {
|
|
||||||
width: 1.3em;
|
|
||||||
height: 1.3em;
|
|
||||||
margin-right: .2em;
|
|
||||||
display: inline-block;
|
|
||||||
transform: translate(0, 4px);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .actions {
|
|
||||||
margin-top: .5em;
|
|
||||||
display: flex;
|
|
||||||
gap: .3em;
|
|
||||||
user-select: none;
|
|
||||||
color: var(--fg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .actions a {
|
|
||||||
padding: .3em .5em;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--bg-3);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
|
|
||||||
transition: color .1s ease-out, background-color .1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blogpost .actions a:hover {
|
|
||||||
background-color: var(--bg-0);
|
|
||||||
color: var(--fg-3);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
@ -33,7 +33,7 @@ form#delete-account input {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,11 +50,18 @@ input[type="text"] {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
input[type="text"]:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
input[type="text"]:active,
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-actions {
|
.artist-actions {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
@ -77,7 +84,7 @@ input[type="text"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -85,7 +92,7 @@ input[type="text"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.credit:hover {
|
.credit:hover {
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-artwork {
|
.release-artwork {
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
input[type="text"] {
|
|
||||||
padding: .3em .5em;
|
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: none;
|
|
||||||
color: inherit;
|
|
||||||
background-color: var(--bg-1);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
padding: 1.5em;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--bg-2);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost label {
|
|
||||||
margin: 1.2em 0 .2em .1em;
|
|
||||||
display: block;
|
|
||||||
font-size: .8em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
#blogpost label:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost #title {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 -.2em;
|
|
||||||
padding: 0 .2em;
|
|
||||||
resize: none;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--fg-3);
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background .1s ease-out, border-color .1s ease-out;
|
|
||||||
|
|
||||||
/*position: relative; outline: none;*/
|
|
||||||
white-space: pre-wrap; overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost #title:hover {
|
|
||||||
background-color: var(--bg-3);
|
|
||||||
border-color: var(--fg-0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost #title:active,
|
|
||||||
#blogpost #title:focus {
|
|
||||||
background-color: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost textarea {
|
|
||||||
width: calc(100% - 2em);
|
|
||||||
margin: 0;
|
|
||||||
padding: 1em;
|
|
||||||
display: block;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--bg-1);
|
|
||||||
color: var(--fg-3);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
resize: vertical;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost #description {
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost select {
|
|
||||||
padding: .5em .8em;
|
|
||||||
font-size: inherit;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10em;
|
|
||||||
color: var(--fg-3);
|
|
||||||
background-color: var(--bg-1);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
#blogpost .blog-actions {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
const blogID = document.getElementById("blogpost").dataset.id;
|
|
||||||
const titleInput = document.getElementById("title");
|
|
||||||
const descInput = document.getElementById("description");
|
|
||||||
const mdInput = document.getElementById("markdown");
|
|
||||||
const blueskyActorInput = document.getElementById("bluesky-actor");
|
|
||||||
const blueskyPostInput = document.getElementById("bluesky-post");
|
|
||||||
const visInput = document.getElementById("visibility");
|
|
||||||
const saveBtn = document.getElementById("save");
|
|
||||||
const deleteBtn = document.getElementById("delete");
|
|
||||||
|
|
||||||
saveBtn.addEventListener("click", () => {
|
|
||||||
fetch("/api/v1/blog/" + blogID, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: titleInput.value,
|
|
||||||
description: descInput.value,
|
|
||||||
markdown: mdInput.value,
|
|
||||||
bluesky_actor: blueskyActorInput.value,
|
|
||||||
bluesky_post: blueskyPostInput.value,
|
|
||||||
visible: visInput.value === "true",
|
|
||||||
}),
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
}).then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
res.text().then(error => {
|
|
||||||
console.error(error);
|
|
||||||
alert("Failed to update blog post: " + error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = location;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteBtn.addEventListener("click", () => {
|
|
||||||
if (blogID != prompt(
|
|
||||||
"You are about to permanently delete " + blogID + ". " +
|
|
||||||
"This action is irreversible. " +
|
|
||||||
"Please enter \"" + blogID + "\" to continue.")) return;
|
|
||||||
fetch("/api/v1/blog/" + blogID, {
|
|
||||||
method: "DELETE",
|
|
||||||
}).then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
res.text().then(error => {
|
|
||||||
console.error(error);
|
|
||||||
alert("Failed to delete blog post: " + error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = "/admin";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
[titleInput, descInput, mdInput, blueskyActorInput, blueskyPostInput, visInput].forEach(input => {
|
|
||||||
input.addEventListener("change", () => {
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
});
|
|
||||||
input.addEventListener("keypress", () => {
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -12,7 +12,7 @@ input[type="text"] {
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -33,7 +33,7 @@ input[type="text"] {
|
||||||
.release-artwork #remove-artwork {
|
.release-artwork #remove-artwork {
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
padding: .3em .6em;
|
padding: .3em .6em;
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-info {
|
.release-info {
|
||||||
|
|
@ -62,13 +62,13 @@ input[type="text"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
#title:hover {
|
#title:hover {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
border-color: var(--fg-0);
|
border-color: var(--fg-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#title:active,
|
#title:active,
|
||||||
#title:focus {
|
#title:focus {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-title small {
|
.release-title small {
|
||||||
|
|
@ -93,7 +93,7 @@ input[type="text"] {
|
||||||
.release-info table tr td:not(:first-child) select:hover,
|
.release-info table tr td:not(:first-child) select:hover,
|
||||||
.release-info table tr td:not(:first-child) input:hover,
|
.release-info table tr td:not(:first-child) input:hover,
|
||||||
.release-info table tr td:not(:first-child) textarea:hover {
|
.release-info table tr td:not(:first-child) textarea:hover {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.release-info table td select,
|
.release-info table td select,
|
||||||
|
|
@ -127,7 +127,7 @@ input[type="text"] {
|
||||||
.release-actions button,
|
.release-actions button,
|
||||||
.release-actions .button {
|
.release-actions .button {
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
|
|
@ -234,7 +234,7 @@ dialog div.dialog-actions {
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@ dialog div.dialog-actions {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
}
|
}
|
||||||
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
|
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
|
||||||
margin: 0 .3em;
|
margin: 0 .3em;
|
||||||
|
|
@ -299,7 +299,6 @@ dialog div.dialog-actions {
|
||||||
#editcredits .credit .delete {
|
#editcredits .credit .delete {
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
#editcredits .credit .delete:hover {
|
#editcredits .credit .delete:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
@ -316,17 +315,14 @@ dialog div.dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--bg-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit ul li.new-artist:nth-child(even) {
|
#addcredit ul li.new-artist:nth-child(even) {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
background-color: var(--bg-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit ul li.new-artist:hover {
|
#addcredit ul li.new-artist:hover {
|
||||||
background: #e0e0e0;
|
background: #e0e0e0;
|
||||||
background-color: var(--bg-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit .new-artist .artist-id {
|
#addcredit .new-artist .artist-id {
|
||||||
|
|
@ -379,8 +375,6 @@ dialog div.dialog-actions {
|
||||||
|
|
||||||
#editlinks tr {
|
#editlinks tr {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--bg-1);
|
|
||||||
transition: background-color .1s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks th {
|
#editlinks th {
|
||||||
|
|
@ -391,7 +385,7 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks tr:nth-child(odd) {
|
#editlinks tr:nth-child(odd) {
|
||||||
background-color: var(--bg-2);
|
background: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks tr th,
|
#editlinks tr th,
|
||||||
|
|
@ -422,11 +416,6 @@ dialog div.dialog-actions {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#editlinks tr .grabber img {
|
|
||||||
filter: invert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#editlinks tr .link-name {
|
#editlinks tr .link-name {
|
||||||
width: 8em;
|
width: 8em;
|
||||||
}
|
}
|
||||||
|
|
@ -465,7 +454,6 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track {
|
#edittracks .track {
|
||||||
background-color: var(--bg-2);
|
|
||||||
transition: transform .2s ease-out, opacity .2s;
|
transition: transform .2s ease-out, opacity .2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,7 +476,7 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track:nth-child(even) {
|
#edittracks .track:nth-child(even) {
|
||||||
background-color: var(--bg-1);
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track-number {
|
#edittracks .track-number {
|
||||||
|
|
@ -504,6 +492,7 @@ dialog div.dialog-actions {
|
||||||
#addtrack ul {
|
#addtrack ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
background: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#addtrack ul li.new-track {
|
#addtrack ul li.new-track {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +45,25 @@
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-info input[type="text"]:hover,
|
||||||
|
.track-info textarea:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info input[type="text"]:active,
|
||||||
|
.track-info textarea:active,
|
||||||
|
.track-info input[type="text"]:focus,
|
||||||
|
.track-info textarea:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
.track-actions {
|
.track-actions {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
74
admin/static/index.js
Normal file
74
admin/static/index.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
const newReleaseBtn = document.getElementById("create-release");
|
||||||
|
const newArtistBtn = document.getElementById("create-artist");
|
||||||
|
const newTrackBtn = document.getElementById("create-track");
|
||||||
|
|
||||||
|
newReleaseBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this release:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/music", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) location = "/admin/releases/" + id;
|
||||||
|
else {
|
||||||
|
res.text().then(err => {
|
||||||
|
alert("Request failed: " + err);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create release. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newArtistBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this artist:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/artist", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/artists/" + id;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create artist. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newTrackBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const title = prompt("Enter an title for this track:");
|
||||||
|
if (title == null || title == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/track", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({title})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/tracks/" + text;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create track. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,7 @@ form#search-form {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ div#search {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--fg-1);
|
color: var(--fg-1);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,8 +100,8 @@ td.log-content {
|
||||||
|
|
||||||
#logs .log.warn {
|
#logs .log.warn {
|
||||||
color: var(--col-on-warn);
|
color: var(--col-on-warn);
|
||||||
background-color: var(--col-warn);
|
background: var(--col-warn);
|
||||||
}
|
}
|
||||||
#logs .log.warn:hover {
|
#logs .log.warn:hover {
|
||||||
background-color: var(--col-warn-hover);
|
background: var(--col-warn-hover);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -67,14 +67,14 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
transition: color .1s ease-out, background .1s ease-out;
|
transition: color .1s ease-out, background .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release .release-actions a:hover {
|
.release .release-actions a:hover {
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
const newReleaseBtn = document.getElementById("create-release");
|
|
||||||
if (newReleaseBtn) newReleaseBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const id = prompt("Enter an ID for this release:");
|
|
||||||
if (id == null || id == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/music", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({id})
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) location = "/admin/releases/" + id;
|
|
||||||
else {
|
|
||||||
res.text().then(err => {
|
|
||||||
alert(err);
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create release. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -44,6 +44,11 @@
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tracks .track-album.empty {
|
||||||
|
color: #ff2020;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#tracks .track-description {
|
#tracks .track-description {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
@ -62,4 +67,61 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
const newTrackBtn = document.getElementById("create-track");
|
|
||||||
if (newTrackBtn) newTrackBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const title = prompt("Enter an title for this track:");
|
|
||||||
if (title == null || title == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/track", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({title})
|
|
||||||
}).then(res => {
|
|
||||||
res.text().then(text => {
|
|
||||||
if (res.ok) {
|
|
||||||
location = "/admin/tracks/" + text;
|
|
||||||
} else {
|
|
||||||
alert(text);
|
|
||||||
console.error(text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create track. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{{define "head"}}
|
|
||||||
<title>Blog - ari melody 💫</title>
|
|
||||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
|
||||||
<link rel="stylesheet" href="/admin/static/blog.css">
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "content"}}
|
|
||||||
<main>
|
|
||||||
<header>
|
|
||||||
<h1>Blog Posts <small>({{.TotalPosts}} total)</small></h2>
|
|
||||||
<a class="button new" id="create-post">Create New</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{{if .Collections}}
|
|
||||||
<div class="blog-group">
|
|
||||||
{{range .Collections}}
|
|
||||||
{{if .Posts}}
|
|
||||||
<div class="blog-collection">
|
|
||||||
<h2>{{.Year}}</h2>
|
|
||||||
{{range .Posts}}
|
|
||||||
{{block "blogpost" .}}{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<p>There are no blog posts.</p>
|
|
||||||
{{end}}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/admin/static/blog.js"></script>
|
|
||||||
{{end}}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{{define "blogpost"}}
|
|
||||||
<div class="blogpost">
|
|
||||||
<h3 class="title"><a href="/admin/blogs/{{.ID}}">{{.Title}}</a>{{if not .Visible}} <small>(Not published)</small>{{end}}</h3>
|
|
||||||
<p class="meta">
|
|
||||||
<span class="author"><img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/> {{.Author.Username}}</span>
|
|
||||||
<span class="date">• {{.PrintDate}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="description">{{.Description}}</p>
|
|
||||||
<div class="actions">
|
|
||||||
<a href="/admin/blogs/{{.ID}}">Edit</a>
|
|
||||||
<a href="/blog/{{.ID}}">View</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
deleteBtn.addEventListener("click", e => {
|
deleteBtn.addEventListener("click", e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!confirm("Are you sure you want to remove " + trackTitle + "?")) return;
|
||||||
trackItem.remove();
|
trackItem.remove();
|
||||||
refreshTrackNumbers();
|
refreshTrackNumbers();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
{{define "head"}}
|
|
||||||
<title>Editing {{.Post.Title}} - ari melody 💫</title>
|
|
||||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
|
||||||
<link rel="stylesheet" href="/admin/static/edit-blog.css">
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "content"}}
|
|
||||||
<main>
|
|
||||||
<h1>Editing Blog Post</h1>
|
|
||||||
|
|
||||||
<div id="blogpost" data-id="{{.Post.ID}}">
|
|
||||||
<label for="title">Title</label>
|
|
||||||
<h2 id="blog-title">
|
|
||||||
<div
|
|
||||||
id="title"
|
|
||||||
name="title"
|
|
||||||
role="textbox"
|
|
||||||
aria-multiline="true"
|
|
||||||
spellcheck="true"
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-invalid="false"
|
|
||||||
aria-autocomplete="list"
|
|
||||||
autocorrect="off"
|
|
||||||
contenteditable="true"
|
|
||||||
zindex="-1"
|
|
||||||
>{{.Post.Title}}</div>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<label for="description">Description</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
value="{{.Post.Description}}"
|
|
||||||
placeholder="No description provided."
|
|
||||||
rows="3"
|
|
||||||
>{{.Post.Description}}</textarea>
|
|
||||||
|
|
||||||
<label for="markdown">Markdown</label>
|
|
||||||
<textarea
|
|
||||||
id="markdown"
|
|
||||||
name="markdown"
|
|
||||||
value="{{.Post.Markdown}}"
|
|
||||||
rows="30"
|
|
||||||
>{{.Post.Markdown}}</textarea>
|
|
||||||
|
|
||||||
<label for="bluesky-actor">Bluesky Author DID</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="bluesky-actor"
|
|
||||||
id="bluesky-actor"
|
|
||||||
placeholder="did:plc:1234abcd..."
|
|
||||||
value="{{if .Post.BlueskyActorID}}{{.Post.BlueskyActorID}}{{end}}">
|
|
||||||
<label for="bluesky-post">Bluesky Post ID</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="bluesky-post"
|
|
||||||
id="bluesky-post"
|
|
||||||
placeholder="3m109a03..."
|
|
||||||
value="{{if .Post.BlueskyPostID}}{{.Post.BlueskyPostID}}{{end}}">
|
|
||||||
|
|
||||||
<label for="visibility">Visibility</label>
|
|
||||||
<select name="visibility" id="visibility">
|
|
||||||
<option value="true"{{if .Post.Visible}} selected{{end}}>Visible</option>
|
|
||||||
<option value="false"{{if not .Post.Visible}} selected{{end}}>Hidden</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div class="blog-actions">
|
|
||||||
<a href="/blog/{{.Post.ID}}" class="button">View</a>
|
|
||||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" id="danger">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2>Danger Zone</h2>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Clicking the button below will delete this blog post.
|
|
||||||
This action is <strong>irreversible</strong>.
|
|
||||||
You will be prompted to confirm this decision.
|
|
||||||
</p>
|
|
||||||
<button class="delete" id="delete">Delete Release</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/admin/static/edit-blog.js"></script>
|
|
||||||
{{end}}
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<main>
|
<main>
|
||||||
<h1>Editing Release</h1>
|
<h1>Editing {{.Release.Title}}</h1>
|
||||||
|
|
||||||
<div id="release" data-id="{{.Release.ID}}">
|
<div id="release" data-id="{{.Release.ID}}">
|
||||||
<div class="release-artwork">
|
<div class="release-artwork">
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Release.Credits}}
|
{{if not .Release.Credits}}
|
||||||
<p>This release has no credits.</p>
|
<p>There are no credits.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -141,15 +141,11 @@
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Release.Links}}
|
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Release.Links}}
|
{{range .Release.Links}}
|
||||||
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
|
||||||
<p>This release has no links.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tracks" class="card">
|
<div id="tracks" class="card">
|
||||||
|
|
@ -166,9 +162,6 @@
|
||||||
{{range .Release.Tracks}}
|
{{range .Release.Tracks}}
|
||||||
{{block "track" .}}{{end}}
|
{{block "track" .}}{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Release.Tracks}}
|
|
||||||
<p>This release has no tracks.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="danger">
|
<div class="card" id="danger">
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/admin/static/releases.js"></script>
|
|
||||||
<script type="module" src="/admin/static/artists.js"></script>
|
<script type="module" src="/admin/static/artists.js"></script>
|
||||||
<script type="module" src="/admin/static/tracks.js"></script>
|
<script type="module" src="/admin/static/index.js"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@
|
||||||
<div class="nav-item{{if eq .Path "/logs"}} active{{end}}">
|
<div class="nav-item{{if eq .Path "/logs"}} active{{end}}">
|
||||||
<a href="/admin/logs">logs</a>
|
<a href="/admin/logs">logs</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<p class="section-label">music</p>
|
<p class="section-label">music</p>
|
||||||
<div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}">
|
<div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}">
|
||||||
<a href="/admin/music/releases/">releases</a>
|
<a href="/admin/music/releases/">releases</a>
|
||||||
|
|
@ -40,13 +38,6 @@
|
||||||
<div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}">
|
<div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}">
|
||||||
<a href="/admin/music/tracks/">tracks</a>
|
<a href="/admin/music/tracks/">tracks</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p class="section-label">blog</p>
|
|
||||||
<div class="nav-item{{if hasPrefix .Path "/blogs"}} active{{end}}">
|
|
||||||
<a href="/admin/blogs/">blog</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="flex-fill"></div>
|
<div class="flex-fill"></div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,4 @@
|
||||||
<p>There are no releases.</p>
|
<p>There are no releases.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/admin/static/releases.js"></script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="tracks">
|
<div id="tracks">
|
||||||
{{range .Tracks}}
|
{{range $Track := .Tracks}}
|
||||||
{{block "track" .}}{{end}}
|
{{block "track" $Track}}{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,63 @@ var prideflagHTML string
|
||||||
//go:embed "html/index.html"
|
//go:embed "html/index.html"
|
||||||
var indexHTML string
|
var indexHTML string
|
||||||
|
|
||||||
|
//go:embed "html/register.html"
|
||||||
|
var registerHTML string
|
||||||
|
//go:embed "html/login.html"
|
||||||
|
var loginHTML string
|
||||||
|
//go:embed "html/login-totp.html"
|
||||||
|
var loginTotpHTML string
|
||||||
|
//go:embed "html/totp-confirm.html"
|
||||||
|
var totpConfirmHTML string
|
||||||
|
//go:embed "html/totp-setup.html"
|
||||||
|
var totpSetupHTML string
|
||||||
|
//go:embed "html/logout.html"
|
||||||
|
var logoutHTML string
|
||||||
|
|
||||||
//go:embed "html/logs.html"
|
//go:embed "html/logs.html"
|
||||||
var logsHTML string
|
var logsHTML string
|
||||||
|
|
||||||
|
//go:embed "html/edit-account.html"
|
||||||
|
var editAccountHTML string
|
||||||
|
|
||||||
|
//go:embed "html/releases.html"
|
||||||
|
var releasesHTML string
|
||||||
|
//go:embed "html/artists.html"
|
||||||
|
var artistsHTML string
|
||||||
|
//go:embed "html/tracks.html"
|
||||||
|
var tracksHTML string
|
||||||
|
|
||||||
|
//go:embed "html/edit-release.html"
|
||||||
|
var editReleaseHTML string
|
||||||
|
//go:embed "html/edit-artist.html"
|
||||||
|
var editArtistHTML string
|
||||||
|
//go:embed "html/edit-track.html"
|
||||||
|
var editTrackHTML string
|
||||||
|
|
||||||
|
//go:embed "html/components/credit/newcredit.html"
|
||||||
|
var componentNewCreditHTML string
|
||||||
|
//go:embed "html/components/credit/addcredit.html"
|
||||||
|
var componentAddCreditHTML string
|
||||||
|
//go:embed "html/components/credit/editcredits.html"
|
||||||
|
var componentEditCreditsHTML string
|
||||||
|
|
||||||
|
//go:embed "html/components/link/editlinks.html"
|
||||||
|
var componentEditLinksHTML 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/track/newtrack.html"
|
||||||
|
var componentNewTrackHTML string
|
||||||
|
//go:embed "html/components/track/addtrack.html"
|
||||||
|
var componentAddTrackHTML string
|
||||||
|
//go:embed "html/components/track/edittracks.html"
|
||||||
|
var componentEditTracksHTML string
|
||||||
|
|
||||||
var BaseTemplate = template.Must(
|
var BaseTemplate = template.Must(
|
||||||
template.New("base").Funcs(
|
template.New("base").Funcs(
|
||||||
template.FuncMap{
|
template.FuncMap{
|
||||||
|
|
@ -30,13 +84,6 @@ var BaseTemplate = template.Must(
|
||||||
prideflagHTML,
|
prideflagHTML,
|
||||||
}, "\n")))
|
}, "\n")))
|
||||||
|
|
||||||
//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
|
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -48,83 +95,37 @@ var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/login.html"
|
|
||||||
var loginHTML string
|
|
||||||
var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
|
var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
|
||||||
|
|
||||||
//go:embed "html/login-totp.html"
|
|
||||||
var loginTotpHTML string
|
|
||||||
var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
|
var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
|
||||||
|
|
||||||
//go:embed "html/register.html"
|
|
||||||
var registerHTML string
|
|
||||||
var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML))
|
var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML))
|
||||||
|
|
||||||
//go:embed "html/logout.html"
|
|
||||||
var logoutHTML string
|
|
||||||
var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML))
|
var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML))
|
||||||
|
|
||||||
//go:embed "html/edit-account.html"
|
|
||||||
var editAccountHTML string
|
|
||||||
var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML))
|
var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML))
|
||||||
|
|
||||||
//go:embed "html/totp-setup.html"
|
|
||||||
var totpSetupHTML string
|
|
||||||
var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
|
var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
|
||||||
|
|
||||||
//go:embed "html/totp-confirm.html"
|
|
||||||
var totpConfirmHTML string
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
"toLower": func (str string) string {
|
|
||||||
return strings.ToLower(str)
|
|
||||||
},
|
|
||||||
"prettyTime": func (t time.Time) string {
|
|
||||||
return t.Format("02 Jan 2006, 15:04:05")
|
|
||||||
},
|
|
||||||
}).Parse(logsHTML))
|
}).Parse(logsHTML))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/releases.html"
|
|
||||||
var releasesHTML string
|
|
||||||
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,
|
||||||
componentReleaseHTML,
|
componentReleaseHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
//go:embed "html/artists.html"
|
|
||||||
var artistsHTML string
|
|
||||||
var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
artistsHTML,
|
artistsHTML,
|
||||||
componentArtistHTML,
|
componentArtistHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
//go:embed "html/tracks.html"
|
|
||||||
var tracksHTML string
|
|
||||||
var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
tracksHTML,
|
tracksHTML,
|
||||||
|
|
@ -134,21 +135,13 @@ var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/edit-release.html"
|
|
||||||
var editReleaseHTML string
|
|
||||||
var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
editReleaseHTML,
|
editReleaseHTML,
|
||||||
componentTrackHTML,
|
componentTrackHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
|
|
||||||
//go:embed "html/edit-artist.html"
|
|
||||||
var editArtistHTML string
|
|
||||||
var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
|
var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
|
||||||
|
|
||||||
//go:embed "html/edit-track.html"
|
|
||||||
var editTrackHTML string
|
|
||||||
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,
|
||||||
|
|
@ -158,54 +151,41 @@ var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/components/credit/newcredit.html"
|
|
||||||
var componentNewCreditHTML string
|
|
||||||
var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
|
var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
|
||||||
|
|
||||||
//go:embed "html/components/credit/addcredit.html"
|
|
||||||
var componentAddCreditHTML string
|
|
||||||
var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
|
var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
|
||||||
|
|
||||||
//go:embed "html/components/credit/editcredits.html"
|
|
||||||
var componentEditCreditsHTML string
|
|
||||||
var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML))
|
var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML))
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/components/link/editlinks.html"
|
|
||||||
var componentEditLinksHTML string
|
|
||||||
var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML))
|
var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML))
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/components/track/newtrack.html"
|
|
||||||
var componentNewTrackHTML string
|
|
||||||
var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
|
var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
|
||||||
|
|
||||||
//go:embed "html/components/track/addtrack.html"
|
|
||||||
var componentAddTrackHTML string
|
|
||||||
var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
|
var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
|
||||||
|
|
||||||
//go:embed "html/components/track/edittracks.html"
|
|
||||||
var componentEditTracksHTML string
|
|
||||||
var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML))
|
var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//go:embed "html/blogs.html"
|
func parseLevel(level log.LogLevel) string {
|
||||||
var blogsHTML string
|
switch level {
|
||||||
//go:embed "html/components/blog/blogpost.html"
|
case log.LEVEL_INFO:
|
||||||
var componentBlogPostHTML string
|
return "INFO"
|
||||||
var BlogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
case log.LEVEL_WARN:
|
||||||
strings.Join([]string{
|
return "WARN"
|
||||||
blogsHTML,
|
}
|
||||||
componentBlogPostHTML,
|
return fmt.Sprintf("%d?", level)
|
||||||
}, "\n"),
|
}
|
||||||
))
|
func titleCase(logType string) string {
|
||||||
|
runes := []rune(logType)
|
||||||
//go:embed "html/edit-blog.html"
|
for i, r := range runes {
|
||||||
var editBlogHTML string
|
if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
|
||||||
var EditBlogTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
runes[i] = r + ('A' - 'a')
|
||||||
strings.Join([]string{
|
}
|
||||||
editBlogHTML,
|
}
|
||||||
componentBlogPostHTML,
|
return string(runes)
|
||||||
}, "\n"),
|
}
|
||||||
))
|
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")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -324,10 +324,6 @@ func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handl
|
||||||
|
|
||||||
err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
|
err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
|
||||||
http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
@ -370,14 +366,14 @@ func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Hand
|
||||||
err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
|
err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
|
http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to update links 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,10 +394,6 @@ func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handle
|
||||||
|
|
||||||
err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
|
err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
|
||||||
http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ header ul li a:hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border-bottom: 1px solid #888;
|
border-bottom: 1px solid #888;
|
||||||
background-color: var(--background);
|
background: var(--background);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--background);
|
background: var(--background);
|
||||||
color: var(--on-background);
|
color: var(--on-background);
|
||||||
font-family: "Monaspace Argon", monospace;
|
font-family: "Monaspace Argon", monospace;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
@ -165,7 +165,7 @@ a#backtotop:hover {
|
||||||
@keyframes list-item-fadein {
|
@keyframes list-item-fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: var(--links);
|
background: var(--links);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ header {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
filter: blur(25px) saturate(25%) brightness(0.5);
|
filter: blur(25px) saturate(25%) brightness(0.5);
|
||||||
-webkit-filter: blur(25px) saturate(25%) brightness(0.5);
|
-webkit-filter: blur(25px) saturate(25%) brightness(0.5);;
|
||||||
animation: background-init .5s forwards,background-loop 30s ease-in-out infinite
|
animation: background-init .5s forwards,background-loop 30s ease-in-out infinite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,9 @@
|
||||||
|
|
||||||
{{else if .IsSingle}}
|
{{else if .IsSingle}}
|
||||||
|
|
||||||
{{index .Tracks 0}}
|
{{$Track := index .Tracks 0}}
|
||||||
{{if .Description}}
|
{{if $Track.Description}}
|
||||||
<p id="description">{{.Description}}</p>
|
<p id="description">{{$Track.Description}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -132,18 +132,18 @@
|
||||||
{{else if .Tracks}}
|
{{else if .Tracks}}
|
||||||
<div id="tracks">
|
<div id="tracks">
|
||||||
<h2>TRACKS</h2>
|
<h2>TRACKS</h2>
|
||||||
{{range .Tracks}}
|
{{range $i, $track := .Tracks}}
|
||||||
<details>
|
<details>
|
||||||
<summary class="album-track-title">{{.Number}}. {{.Title}}</summary>
|
<summary class="album-track-title">{{add $i 1}}. {{$track.Title}}</summary>
|
||||||
|
|
||||||
{{if .Description}}
|
{{if $track.Description}}
|
||||||
<p class="album-track-subheading">DESCRIPTION</p>
|
<p class="album-track-subheading">DESCRIPTION</p>
|
||||||
{{.Description}}
|
{{$track.Description}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<p class="album-track-subheading">LYRICS</p>
|
<p class="album-track-subheading">LYRICS</p>
|
||||||
{{if .Lyrics}}
|
{{if $track.Lyrics}}
|
||||||
{{.GetLyricsHTML}}
|
{{$track.GetLyricsHTML}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,11 @@ var blogHTML string
|
||||||
var blogPostHTML string
|
var blogPostHTML string
|
||||||
|
|
||||||
var BaseTemplate = template.Must(
|
var BaseTemplate = template.Must(
|
||||||
template.New("base").Parse(strings.Join([]string{
|
template.New("base").Funcs(
|
||||||
|
template.FuncMap{
|
||||||
|
"add": func (a int, b int) int { return a + b },
|
||||||
|
},
|
||||||
|
).Parse(strings.Join([]string{
|
||||||
layoutHTML,
|
layoutHTML,
|
||||||
headerHTML,
|
headerHTML,
|
||||||
footerHTML,
|
footerHTML,
|
||||||
|
|
|
||||||
|
|
@ -78,15 +78,15 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !release.IsReleased() && !privileged {
|
response := *release
|
||||||
release.Tracks = nil
|
|
||||||
release.Credits = nil
|
if release.IsReleased() || privileged {
|
||||||
release.Links = nil
|
response.Tracks = release.Tracks
|
||||||
|
response.Credits = release.Credits
|
||||||
|
response.Links = release.Links
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
err := templates.MusicGatewayTemplate.Execute(w, response)
|
||||||
|
|
||||||
err := templates.MusicGatewayTemplate.Execute(w, release)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error rendering music gateway for %s: %v\n", release.ID, err)
|
fmt.Fprintf(os.Stderr, "Error rendering music gateway for %s: %v\n", release.ID, err)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue