Compare commits

...

2 commits

25 changed files with 280 additions and 100 deletions

View file

@ -18,12 +18,12 @@ import (
func accountHandler(app *model.AppState) http.Handler { func accountHandler(app *model.AppState) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/totp-setup", totpSetupHandler(app)) mux.Handle("/account/totp-setup", totpSetupHandler(app))
mux.Handle("/totp-confirm", totpConfirmHandler(app)) mux.Handle("/account/totp-confirm", totpConfirmHandler(app))
mux.Handle("/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app))) mux.Handle("/account/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app)))
mux.Handle("/password", changePasswordHandler(app)) mux.Handle("/account/password", changePasswordHandler(app))
mux.Handle("/delete", deleteAccountHandler(app)) mux.Handle("/account/delete", deleteAccountHandler(app))
return mux return mux
} }

View file

@ -10,9 +10,9 @@ import (
"arimelody-web/model" "arimelody-web/model"
) )
func serveArtist(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) {
slices := strings.Split(r.URL.Path[1:], "/") slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/artists")[1:], "/")
id := slices[0] id := slices[0]
artist, err := controller.GetArtist(app.DB, id) artist, err := controller.GetArtist(app.DB, id)
if err != nil { if err != nil {

View file

@ -47,15 +47,16 @@ func Handler(app *model.AppState) http.Handler {
mux.Handle("/register", registerAccountHandler(app)) mux.Handle("/register", registerAccountHandler(app))
mux.Handle("/account", requireAccount(accountIndexHandler(app))) mux.Handle("/account", requireAccount(accountIndexHandler(app)))
mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app)))) mux.Handle("/account/", requireAccount(accountHandler(app)))
mux.Handle("/logs", requireAccount(logsHandler(app))) mux.Handle("/logs", requireAccount(logsHandler(app)))
mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app)))) mux.Handle("/releases", requireAccount(serveReleases(app)))
mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app)))) mux.Handle("/releases/", requireAccount(serveReleases(app)))
mux.Handle("/track/", requireAccount(http.StripPrefix("/track", serveTrack(app)))) mux.Handle("/artists/", requireAccount(serveArtists(app)))
mux.Handle("/tracks/", requireAccount(serveTracks(app)))
mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/static/", staticHandler())
mux.Handle("/", requireAccount(AdminIndexHandler(app))) mux.Handle("/", requireAccount(AdminIndexHandler(app)))
@ -470,7 +471,8 @@ var staticFS embed.FS
func staticHandler() http.Handler { func staticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(r.URL.Path))) uri := strings.TrimPrefix(r.URL.Path, "/static")
file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(uri)))
if err != nil { if err != nil {
http.NotFound(w, r) http.NotFound(w, r)
return return

View file

@ -3,6 +3,7 @@ package admin
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"os"
"strings" "strings"
"arimelody-web/admin/templates" "arimelody-web/admin/templates"
@ -10,11 +11,52 @@ import (
"arimelody-web/model" "arimelody-web/model"
) )
func serveRelease(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) {
slices := strings.Split(r.URL.Path[1:], "/") slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/releases")[1:], "/")
releaseID := slices[0] 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
}
session := r.Context().Value("session").(*model.Session)
type ReleasesData struct {
adminPageData
Releases []*model.Release
}
releases, err := controller.GetAllReleases(app.DB, false, 0, true)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = templates.ReleasesTemplate.Execute(w, ReleasesData{
adminPageData: adminPageData{
Path: r.URL.Path,
Session: session,
},
Releases: releases,
})
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render 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) session := r.Context().Value("session").(*model.Session)
release, err := controller.GetRelease(app.DB, releaseID, true) release, err := controller.GetRelease(app.DB, releaseID, true)
@ -28,8 +70,8 @@ func serveRelease(app *model.AppState) http.Handler {
return return
} }
if len(slices) > 1 { if len(action) > 0 {
switch slices[1] { switch action {
case "editcredits": case "editcredits":
serveEditCredits(release).ServeHTTP(w, r) serveEditCredits(release).ServeHTTP(w, r)
return return

View file

@ -66,6 +66,13 @@
} }
} }
@media (prefers-color-scheme: dark) {
img.icon {
-webkit-filter: invert(1);
filter: invert(1);
}
}
body { body {
width: calc(100% - 180px); width: calc(100% - 180px);
height: calc(100vh - 1em); height: calc(100vh - 1em);
@ -184,8 +191,9 @@ a:hover {
} }
*/ */
a img.icon { img.icon {
height: .8em; height: .8em;
transition: filter .1s ease-out;
} }
code { code {

View file

@ -81,7 +81,7 @@ removeAvatarBtn.addEventListener("click", () => {
}); });
document.addEventListener('readystatechange', () => { document.addEventListener('readystatechange', () => {
document.querySelectorAll('.card#releases .credit').forEach(el => { document.querySelectorAll('#releases .credit').forEach(el => {
hijackClickEvent(el, el.querySelector('.credit-name a')); hijackClickEvent(el, el.querySelector('.credit-name a'));
}); });
}); });

View file

@ -14,6 +14,8 @@ input[type="text"] {
border-radius: 8px; border-radius: 8px;
background: 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;
} }
.release-artwork { .release-artwork {
@ -31,6 +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: var(--bg-3);
} }
.release-info { .release-info {
@ -118,6 +121,7 @@ input[type="text"] {
gap: .5em; gap: .5em;
flex-direction: row; flex-direction: row;
justify-content: right; justify-content: right;
color: var(--fg-3);
} }
.release-actions button, .release-actions button,
@ -163,7 +167,7 @@ dialog div.dialog-actions {
* RELEASE CREDITS * RELEASE CREDITS
*/ */
.card#credits .credit { #credits .credit {
margin-bottom: .5em; margin-bottom: .5em;
padding: .5em; padding: .5em;
display: flex; display: flex;
@ -178,24 +182,24 @@ dialog div.dialog-actions {
cursor: pointer; cursor: pointer;
transition: background .1s ease-out; transition: background .1s ease-out;
} }
.card#credits .credit:hover { #credits .credit:hover {
background-color: var(--bg-1); background-color: var(--bg-1);
} }
.card#credits .credit p { #credits .credit p {
margin: 0; margin: 0;
} }
.card#credits .credit .artist-avatar { #credits .credit .artist-avatar {
border-radius: 12px; border-radius: 12px;
} }
.card#credits .credit .artist-name { #credits .credit .artist-name {
color: var(--fg-3); color: var(--fg-3);
font-weight: bold; font-weight: bold;
} }
.card#credits .credit .artist-role small { #credits .credit .artist-role small {
font-size: inherit; font-size: inherit;
opacity: .66; opacity: .66;
} }
@ -314,33 +318,38 @@ dialog div.dialog-actions {
* RELEASE LINKS * RELEASE LINKS
*/ */
.card#links ul { #links ul {
padding: 0; padding: 0;
display: flex; display: flex;
gap: .2em; gap: .2em;
} }
.card#links a.button:hover { #links a img.icon {
-webkit-filter: none;
filter: none;
}
#links a.button:hover {
color: var(--bg-3) !important; color: var(--bg-3) !important;
background-color: var(--fg-3) !important; background-color: var(--fg-3) !important;
} }
.card#links a.button[data-name="spotify"] { #links a.button[data-name="spotify"] {
color: #101010; color: #101010;
background-color: #8cff83 background-color: #8cff83
} }
.card#links a.button[data-name="apple music"] { #links a.button[data-name="apple music"] {
color: #101010; color: #101010;
background-color: #8cd9ff background-color: #8cd9ff
} }
.card#links a.button[data-name="soundcloud"] { #links a.button[data-name="soundcloud"] {
color: #101010; color: #101010;
background-color: #fdaa6d background-color: #fdaa6d
} }
.card#links a.button[data-name="youtube"] { #links a.button[data-name="youtube"] {
color: #101010; color: #101010;
background-color: #ff6e6e background-color: #ff6e6e
} }
@ -428,7 +437,7 @@ dialog div.dialog-actions {
* RELEASE TRACKS * RELEASE TRACKS
*/ */
.card#tracks .track { #tracks .track {
margin-bottom: 1em; margin-bottom: 1em;
padding: 1em; padding: 1em;
display: flex; display: flex;
@ -438,49 +447,51 @@ dialog div.dialog-actions {
border-radius: 16px; border-radius: 16px;
background: 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;
} }
.card#tracks .track h3, #tracks .track h3,
.card#tracks .track p { #tracks .track p {
margin: 0; margin: 0;
} }
.card#tracks h2.track-title { #tracks h2.track-title {
margin: 0; margin: 0;
display: flex; display: flex;
gap: .5em; gap: .5em;
} }
.card#tracks h2.track-title .track-number { #tracks h2.track-title .track-number {
opacity: .5; opacity: .5;
} }
.card#tracks a:hover { #tracks a:hover {
text-decoration: underline; text-decoration: underline;
} }
.card#tracks .track-album { #tracks .track-album {
margin-left: auto; margin-left: auto;
font-style: italic; font-style: italic;
font-size: .75em; font-size: .75em;
opacity: .5; opacity: .5;
} }
.card#tracks .track-album.empty { #tracks .track-album.empty {
color: #ff2020; color: #ff2020;
opacity: 1; opacity: 1;
} }
.card#tracks .track-description { #tracks .track-description {
font-style: italic; font-style: italic;
} }
.card#tracks .track-lyrics { #tracks .track-lyrics {
max-height: 10em; max-height: 10em;
overflow-y: scroll; overflow-y: scroll;
} }
.card#tracks .track .empty { #tracks .track .empty {
opacity: 0.75; opacity: 0.75;
} }

View file

@ -100,7 +100,7 @@ removeArtworkBtn.addEventListener("click", () => {
}); });
document.addEventListener("readystatechange", () => { document.addEventListener("readystatechange", () => {
document.querySelectorAll(".card#credits .credit").forEach(el => { document.querySelectorAll("#credits .credit").forEach(el => {
hijackClickEvent(el, el.querySelector(".artist-name a")); hijackClickEvent(el, el.querySelector(".artist-name a"));
}); });
}); });

View file

@ -14,7 +14,7 @@ newReleaseBtn.addEventListener("click", event => {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({id}) body: JSON.stringify({id})
}).then(res => { }).then(res => {
if (res.ok) location = "/admin/release/" + id; if (res.ok) location = "/admin/releases/" + id;
else { else {
res.text().then(err => { res.text().then(err => {
alert("Request failed: " + err); alert("Request failed: " + err);
@ -39,7 +39,7 @@ newArtistBtn.addEventListener("click", event => {
}).then(res => { }).then(res => {
res.text().then(text => { res.text().then(text => {
if (res.ok) { if (res.ok) {
location = "/admin/artist/" + id; location = "/admin/artists/" + id;
} else { } else {
alert("Request failed: " + text); alert("Request failed: " + text);
console.error(text); console.error(text);
@ -63,7 +63,7 @@ newTrackBtn.addEventListener("click", event => {
}).then(res => { }).then(res => {
res.text().then(text => { res.text().then(text => {
if (res.ok) { if (res.ok) {
location = "/admin/track/" + text; location = "/admin/tracks/" + text;
} else { } else {
alert("Request failed: " + text); alert("Request failed: " + text);
console.error(text); console.error(text);

View file

@ -51,6 +51,7 @@
.release-actions { .release-actions {
margin-top: .5em; margin-top: .5em;
user-select: none; user-select: none;
color: var(--fg-3);
} }
.release-actions a { .release-actions a {

View file

@ -0,0 +1,30 @@
{{define "head"}}
<title>Artists - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/index.css">
{{end}}
{{define "content"}}
<main>
<h1>Artists</h1>
<div class="card-header">
<h2><a href="/admin/artists/">Artists</a></h2>
<a class="button new" id="create-artist">Create New</a>
</div>
{{if .Artists}}
<div class="artists-group">
{{range $Artist := .Artists}}
<div class="artist">
<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}}
</div>
{{else}}
<p>There are no artists.</p>
{{end}}
</main>
<script type="module" src="/admin/static/admin.js"></script>
{{end}}

View file

@ -7,7 +7,7 @@
{{range $Artist := .Artists}} {{range $Artist := .Artists}}
<li class="new-artist" <li class="new-artist"
data-id="{{$Artist.ID}}" data-id="{{$Artist.ID}}"
hx-get="/admin/release/{{$.ReleaseID}}/newcredit/{{$Artist.ID}}" hx-get="/admin/releases/{{$.ReleaseID}}/newcredit/{{$Artist.ID}}"
hx-target="#editcredits ul" hx-target="#editcredits ul"
hx-swap="beforeend" hx-swap="beforeend"
> >

View file

@ -3,8 +3,8 @@
<h2>Editing: Credits</h2> <h2>Editing: Credits</h2>
<a id="add-credit" <a id="add-credit"
class="button new" class="button new"
href="/admin/release/{{.ID}}/addcredit" href="/admin/releases/{{.ID}}/addcredit"
hx-get="/admin/release/{{.ID}}/addcredit" hx-get="/admin/releases/{{.ID}}/addcredit"
hx-target="body" hx-target="body"
hx-swap="beforeend" hx-swap="beforeend"
>Add</a> >Add</a>

View file

@ -5,7 +5,7 @@
</div> </div>
<div class="release-info"> <div class="release-info">
<h3 class="release-title"> <h3 class="release-title">
<a href="/admin/release/{{.ID}}">{{.Title}}</a> <a href="/admin/releases/{{.ID}}">{{.Title}}</a>
<small> <small>
<span title="{{.PrintReleaseDate}}">{{.ReleaseDate.Year}}</span> <span title="{{.PrintReleaseDate}}">{{.ReleaseDate.Year}}</span>
{{if not .Visible}}(hidden){{end}} {{if not .Visible}}(hidden){{end}}
@ -13,9 +13,9 @@
</h3> </h3>
<p class="release-artists">{{.PrintArtists true true}}</p> <p class="release-artists">{{.PrintArtists true true}}</p>
<p class="release-type-single">{{.ReleaseType}} <p class="release-type-single">{{.ReleaseType}}
(<a href="/admin/release/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p> (<a href="/admin/releases/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p>
<div class="release-actions"> <div class="release-actions">
<a href="/admin/release/{{.ID}}">Edit</a> <a href="/admin/releases/{{.ID}}">Edit</a>
<a href="/music/{{.ID}}" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a> <a href="/music/{{.ID}}" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a>
</div> </div>
</div> </div>

View file

@ -8,7 +8,7 @@
</li> </li>
<li class="new-track" <li class="new-track"
data-id="{{$Track.ID}}" data-id="{{$Track.ID}}"
hx-get="/admin/release/{{$.ReleaseID}}/newtrack/{{$Track.ID}}" hx-get="/admin/releases/{{$.ReleaseID}}/newtrack/{{$Track.ID}}"
hx-target="#edittracks ul" hx-target="#edittracks ul"
hx-swap="beforeend" hx-swap="beforeend"
> >

View file

@ -3,8 +3,8 @@
<h2>Editing: Tracks</h2> <h2>Editing: Tracks</h2>
<a id="add-track" <a id="add-track"
class="button new" class="button new"
href="/admin/release/{{.Release.ID}}/addtrack" href="/admin/releases/{{.Release.ID}}/addtrack"
hx-get="/admin/release/{{.Release.ID}}/addtrack" hx-get="/admin/releases/{{.Release.ID}}/addtrack"
hx-target="body" hx-target="body"
hx-swap="beforeend" hx-swap="beforeend"
>Add</a> >Add</a>

View file

@ -38,7 +38,7 @@
<div class="credit"> <div class="credit">
<img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork"> <img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork">
<div class="credit-info"> <div class="credit-info">
<h3 class="credit-name"><a href="/admin/release/{{.Release.ID}}">{{.Release.Title}}</a></h3> <h3 class="credit-name"><a href="/admin/releases/{{.Release.ID}}">{{.Release.Title}}</a></h3>
<p class="credit-artists">{{.Release.PrintArtists true true}}</p> <p class="credit-artists">{{.Release.PrintArtists true true}}</p>
<p class="artist-role"> <p class="artist-role">
Role: {{.Role}} Role: {{.Role}}

View file

@ -99,10 +99,10 @@
<div class="card" id="credits"> <div class="card" id="credits">
<div class="card-header"> <div class="card-header">
<h2>Credits ({{len .Release.Credits}})</h2> <h2>Credits <small>({{len .Release.Credits}} total)</small></h2>
<a class="button edit" <a class="button edit"
href="/admin/release/{{.Release.ID}}/editcredits" href="/admin/releases/{{.Release.ID}}/editcredits"
hx-get="/admin/release/{{.Release.ID}}/editcredits" hx-get="/admin/releases/{{.Release.ID}}/editcredits"
hx-target="body" hx-target="body"
hx-swap="beforeend" hx-swap="beforeend"
>Edit</a> >Edit</a>
@ -111,7 +111,7 @@
<div class="credit"> <div class="credit">
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> <img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<div class="credit-info"> <div class="credit-info">
<p class="artist-name"><a href="/admin/artist/{{.Artist.ID}}">{{.Artist.Name}}</a></p> <p class="artist-name"><a href="/admin/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
<p class="artist-role"> <p class="artist-role">
{{.Role}} {{.Role}}
{{if .Primary}} {{if .Primary}}
@ -130,8 +130,8 @@
<div class="card-header"> <div class="card-header">
<h2>Links ({{len .Release.Links}})</h2> <h2>Links ({{len .Release.Links}})</h2>
<a class="button edit" <a class="button edit"
href="/admin/release/{{.Release.ID}}/editlinks" href="/admin/releases/{{.Release.ID}}/editlinks"
hx-get="/admin/release/{{.Release.ID}}/editlinks" hx-get="/admin/releases/{{.Release.ID}}/editlinks"
hx-target="body" hx-target="body"
hx-swap="beforeend" hx-swap="beforeend"
>Edit</a> >Edit</a>
@ -147,8 +147,8 @@
<div class="card-header" id="tracks"> <div class="card-header" id="tracks">
<h2>Tracklist ({{len .Release.Tracks}})</h2> <h2>Tracklist ({{len .Release.Tracks}})</h2>
<a class="button edit" <a class="button edit"
href="/admin/release/{{.Release.ID}}/edittracks" href="/admin/releases/{{.Release.ID}}/edittracks"
hx-get="/admin/release/{{.Release.ID}}/edittracks" hx-get="/admin/releases/{{.Release.ID}}/edittracks"
hx-target="body" hx-target="body"
hx-swap="beforeend" hx-swap="beforeend"
>Edit</a> >Edit</a>
@ -157,7 +157,7 @@
<div class="track" data-id="{{$track.ID}}"> <div class="track" data-id="{{$track.ID}}">
<h2 class="track-title"> <h2 class="track-title">
<span class="track-number">{{.Add $i 1}}</span> <span class="track-number">{{.Add $i 1}}</span>
<a href="/admin/track/{{$track.ID}}">{{$track.Title}}</a> <a href="/admin/tracks/{{$track.ID}}">{{$track.Title}}</a>
</h2> </h2>
<h3>Description</h3> <h3>Description</h3>

View file

@ -32,7 +32,7 @@
{{range $Artist := .Artists}} {{range $Artist := .Artists}}
<div class="artist"> <div class="artist">
<img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> <img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<a href="/admin/artist/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> <a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
</div> </div>
{{end}} {{end}}
</div> </div>
@ -51,7 +51,7 @@
{{range $Track := .Tracks}} {{range $Track := .Tracks}}
<div class="track"> <div class="track">
<h2 class="track-title"> <h2 class="track-title">
<a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a> <a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a>
</h2> </h2>
{{if $Track.Description}} {{if $Track.Description}}
<p class="track-description">{{$Track.GetDescriptionHTML}}</p> <p class="track-description">{{$Track.GetDescriptionHTML}}</p>

View file

@ -28,14 +28,14 @@
</div> </div>
<hr> <hr>
<p class="section-label">music</p> <p class="section-label">music</p>
<div class="nav-item{{if eq .Path "/releases"}} active{{end}}"> <div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}">
<a href="/admin/releases">releases</a> <a href="/admin/releases/">releases</a>
</div> </div>
<div class="nav-item{{if eq .Path "/artists"}} active{{end}}"> <div class="nav-item{{if hasPrefix .Path "/artists"}} active{{end}}">
<a href="/admin/artists">artists</a> <a href="/admin/artists/">artists</a>
</div> </div>
<div class="nav-item{{if eq .Path "/tracks"}} active{{end}}"> <div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}">
<a href="/admin/tracks">tracks</a> <a href="/admin/tracks/">tracks</a>
</div> </div>
{{end}} {{end}}

View file

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

View file

@ -0,0 +1,40 @@
{{define "head"}}
<title>Releases - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/index.css">
{{end}}
{{define "content"}}
<main>
<h1>Releases</h1>
<div class="card-header">
<h2><a href="/admin/tracks/">Tracks</a></h2>
<a class="button new" id="create-track">Create New</a>
</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>
<script type="module" src="/admin/static/admin.js"></script>
{{end}}

View file

@ -2,11 +2,11 @@ package templates
import ( import (
"arimelody-web/log" "arimelody-web/log"
_ "embed"
"fmt" "fmt"
"html/template" "html/template"
"strings" "strings"
"time" "time"
_ "embed"
) )
//go:embed "html/layout.html" //go:embed "html/layout.html"
@ -35,10 +35,18 @@ var logsHTML string
//go:embed "html/edit-account.html" //go:embed "html/edit-account.html"
var editAccountHTML string var editAccountHTML string
//go:embed "html/edit-artist.html"
var editArtistHTML 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" //go:embed "html/edit-release.html"
var editReleaseHTML string var editReleaseHTML string
//go:embed "html/edit-artist.html"
var editArtistHTML string
//go:embed "html/edit-track.html" //go:embed "html/edit-track.html"
var editTrackHTML string var editTrackHTML string
@ -62,9 +70,18 @@ var componentAddTrackHTML string
//go:embed "html/components/tracks/edittracks.html" //go:embed "html/components/tracks/edittracks.html"
var componentEditTracksHTML string var componentEditTracksHTML string
var BaseTemplate = template.Must(template.New("base").Parse( var BaseTemplate = template.Must(
strings.Join([]string{ layoutHTML, prideflagHTML }, "\n"), template.New("base").Funcs(
)) template.FuncMap{
"hasPrefix": func(s string, prefix string) bool {
fmt.Printf("does \"%s\" start with \"%s\"?\n", s, prefix)
return strings.HasPrefix(s, prefix)
},
},
).Parse(strings.Join([]string{
layoutHTML,
prideflagHTML,
}, "\n")))
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{ strings.Join([]string{
@ -108,6 +125,15 @@ var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(templ
}, },
}).Parse(logsHTML)) }).Parse(logsHTML))
var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{
releasesHTML,
componentReleaseListItemHTML,
}, "\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(editReleaseHTML))
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(

View file

@ -10,9 +10,9 @@ import (
"arimelody-web/model" "arimelody-web/model"
) )
func serveTrack(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) {
slices := strings.Split(r.URL.Path[1:], "/") slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/tracks")[1:], "/")
id := slices[0] id := slices[0]
track, err := controller.GetTrack(app.DB, id) track, err := controller.GetTrack(app.DB, id)
if err != nil { if err != nil {

View file

@ -16,10 +16,10 @@ func IndexHandler(app *model.AppState) http.Handler {
return return
} }
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
type IndexData struct { type IndexData struct {
TwitchStatus *model.TwitchStreamInfo TwitchStatus *model.TwitchStreamInfo
} }
var err error var err error
var twitchStatus *model.TwitchStreamInfo = nil var twitchStatus *model.TwitchStreamInfo = nil
if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 { if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 {
@ -28,9 +28,7 @@ func IndexHandler(app *model.AppState) http.Handler {
fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err) fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
} }
} }
err = templates.IndexTemplate.Execute(w, IndexData{
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
err := templates.IndexTemplate.Execute(w, IndexData{
TwitchStatus: twitchStatus, TwitchStatus: twitchStatus,
}) })
if err != nil { if err != nil {