home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

release edit page! + a lot of other stuff oml

Signed-off-by: ari melody <ari@arimelody.me>

+979 -345
+32 -31
admin/http.go
··· 15 15 musicModel "arimelody.me/arimelody.me/music/model" 16 16 ) 17 17 18 + type loginData struct { 19 + DiscordURI string 20 + Token string 21 + } 22 + 18 23 func Handler() http.Handler { 19 24 mux := http.NewServeMux() 20 25 21 26 mux.Handle("/login", LoginHandler()) 22 27 mux.Handle("/logout", MustAuthorise(LogoutHandler())) 23 28 mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) 29 + mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) 24 30 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 31 if r.URL.Path != "/" { 26 32 http.NotFound(w, r) ··· 46 52 } 47 53 ) 48 54 49 - 50 55 var tracks = []Track{} 51 56 for _, track := range global.Tracks { 57 + if track.Release != nil { continue } 52 58 tracks = append(tracks, Track{ 53 59 Track: track, 54 60 Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), ··· 79 85 } 80 86 81 87 func GetSession(r *http.Request) *Session { 82 - // TODO: remove later- this bypasses auth! 83 - return &Session{} 88 + if os.Getenv("ARIMELODY_ADMIN_BYPASS") == "true" { 89 + return &Session{} 90 + } 84 91 85 92 var token = "" 86 93 // is the session token in context? ··· 137 144 code := r.URL.Query().Get("code") 138 145 139 146 if code == "" { 140 - serveTemplate("login.html", discord.REDIRECT_URI).ServeHTTP(w, r) 147 + serveTemplate("login.html", loginData{DiscordURI: discord.REDIRECT_URI}).ServeHTTP(w, r) 141 148 return 142 149 } 143 150 ··· 155 162 return 156 163 } 157 164 158 - if discord_user.Id != ADMIN_ID_DISCORD { 165 + if discord_user.ID != ADMIN_ID_DISCORD { 159 166 // TODO: unauthorized user; revoke the token 167 + fmt.Printf("Unauthorized login attempted: %s\n", discord_user.ID) 160 168 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 161 169 return 162 170 } ··· 174 182 cookie.Path = "/" 175 183 http.SetCookie(w, &cookie) 176 184 177 - w.WriteHeader(http.StatusOK) 178 - w.Header().Add("Content-Type", "text/html") 179 - w.Write([]byte( 180 - "<!DOCTYPE html><html><head>"+ 181 - "<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+ 182 - "</head><body>"+ 183 - "Logged in successfully. "+ 184 - "You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+ 185 - "</body></html>"), 186 - ) 185 + serveTemplate("login.html", loginData{Token: session.Token}).ServeHTTP(w, r) 186 + // w.WriteHeader(http.StatusOK) 187 + // w.Header().Add("Content-Type", "text/html") 188 + // w.Write([]byte( 189 + // "<!DOCTYPE html><html><head>"+ 190 + // "<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+ 191 + // "</head><body>"+ 192 + // "Logged in successfully. "+ 193 + // "You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+ 194 + // "</body></html>"), 195 + // ) 187 196 }) 188 197 } 189 198 ··· 194 203 return 195 204 } 196 205 197 - token := r.Context().Value("token").(string) 198 - 199 - if token == "" { 200 - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 201 - return 202 - } 206 + session := GetSession(r) 203 207 204 208 // remove this session from the list 205 209 sessions = func (token string) []*Session { 206 210 new_sessions := []*Session{} 207 211 for _, session := range sessions { 208 - new_sessions = append(new_sessions, session) 212 + if session.Token != token { 213 + new_sessions = append(new_sessions, session) 214 + } 209 215 } 210 216 return new_sessions 211 - }(token) 217 + }(session.Token) 212 218 213 - w.WriteHeader(http.StatusOK) 214 - w.Write([]byte( 215 - "<meta http-equiv=\"refresh\" content=\"5;url=/\" />"+ 216 - "Logged out successfully. "+ 217 - "You should be redirected to <a href=\"/\">/</a> in 5 seconds."), 218 - ) 219 + serveTemplate("logout.html", nil).ServeHTTP(w, r) 219 220 }) 220 221 } 221 222 222 223 func serveTemplate(page string, data any) http.Handler { 223 224 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 224 - lp_layout := filepath.Join("views", "admin", "layout.html") 225 + lp_layout := filepath.Join("admin", "views", "layout.html") 225 226 lp_prideflag := filepath.Join("views", "prideflag.html") 226 - fp := filepath.Join("views", "admin", filepath.Clean(page)) 227 + fp := filepath.Join("admin", "views", filepath.Clean(page)) 227 228 228 229 info, err := os.Stat(fp) 229 230 if err != nil {
+58
admin/releasehttp.go
··· 1 + package admin 2 + 3 + import ( 4 + "fmt" 5 + "html/template" 6 + "net/http" 7 + "strings" 8 + 9 + "arimelody.me/arimelody.me/global" 10 + "arimelody.me/arimelody.me/music/model" 11 + ) 12 + 13 + type ( 14 + gatewayTrack struct { 15 + *model.Track 16 + Lyrics template.HTML 17 + Number int 18 + } 19 + 20 + gatewayRelease struct { 21 + *model.Release 22 + Tracks []gatewayTrack 23 + } 24 + ) 25 + 26 + func serveRelease() http.Handler { 27 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 + if r.URL.Path == "/" { 29 + http.NotFound(w, r) 30 + return 31 + } 32 + 33 + id := r.URL.Path[1:] 34 + release := global.GetRelease(id) 35 + if release == nil { 36 + http.NotFound(w, r) 37 + return 38 + } 39 + 40 + tracks := []gatewayTrack{} 41 + for i, track := range release.Tracks { 42 + tracks = append([]gatewayTrack{{ 43 + Track: track, 44 + Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), 45 + Number: len(release.Tracks) - i, 46 + }}, tracks...) 47 + } 48 + 49 + lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} 50 + 51 + serveTemplate("edit-release.html", gatewayRelease{release, tracks}).ServeHTTP(&lrw, r) 52 + 53 + if lrw.Code != http.StatusOK { 54 + fmt.Printf("Error rendering admin release page for %s\n", id) 55 + return 56 + } 57 + }) 58 + }
+26 -175
admin/static/admin.css
··· 21 21 margin: 1em auto; 22 22 display: flex; 23 23 flex-direction: row; 24 - justify-content: center; 24 + justify-content: left; 25 25 26 26 background: #f8f8f8; 27 27 border-radius: .5em; ··· 29 29 } 30 30 header .icon { 31 31 height: 100%; 32 + } 33 + header .title { 34 + width: auto; 35 + height: 100%; 32 36 33 - margin-right: 1em; 34 - } 37 + margin: 0 1em 0 0; 35 38 39 + display: flex; 40 + 41 + line-height: 2em; 42 + text-decoration: none; 43 + 44 + color: inherit; 45 + } 36 46 header a { 37 - height: 100%; 38 47 width: auto; 48 + height: 100%; 39 49 40 50 margin: 0px; 41 51 padding: 0 1em; ··· 47 57 48 58 color: inherit; 49 59 } 50 - 51 60 header a:hover { 52 61 background: #00000010; 62 + text-decoration: none; 63 + } 64 + header #logout { 65 + margin-left: auto; 53 66 } 54 67 55 68 main { ··· 67 80 text-decoration: underline; 68 81 } 69 82 83 + .card { 84 + margin-bottom: 2em; 85 + } 86 + 70 87 .card h2 { 71 88 margin: 0 0 .5em 0; 72 89 } ··· 77 94 } 78 95 79 96 .card-title { 80 - display: flex; 81 - flex-direction: row; 82 - align-items: center; 83 - justify-content: space-between; 84 - } 85 - 86 - .create-btn { 87 - background: #c4ff6a; 88 - padding: .5em .8em; 89 - border-radius: .5em; 90 - border: 1px solid #84b141; 91 - text-decoration: none; 92 - } 93 - .create-btn:hover { 94 - background: #fff; 95 - border-color: #d0d0d0; 96 - text-decoration: inherit; 97 - } 98 - .create-btn:active { 99 - background: #d0d0d0; 100 - border-color: #808080; 101 - text-decoration: inherit; 102 - } 103 - 104 - .release { 105 97 margin-bottom: 1em; 106 - padding: 1em; 107 98 display: flex; 108 - flex-direction: row; 109 99 gap: 1em; 110 - 111 - border-radius: .5em; 112 - background: #f8f8f8f8; 113 - border: 1px solid #808080; 114 - } 115 - 116 - .release-artwork { 117 - width: 96px; 118 - 119 - display: flex; 120 - justify-content: center; 121 - align-items: center; 122 - } 123 - 124 - .release-artwork img { 125 - width: 100%; 126 - aspect-ratio: 1; 127 - } 128 - 129 - .latest-release .release-info { 130 - width: 300px; 131 - flex-direction: column; 132 - } 133 - 134 - .release-title small { 135 - opacity: .75; 136 - } 137 - 138 - .release-links { 139 - margin: .5em 0; 140 - padding: 0; 141 - display: flex; 142 - flex-direction: row; 143 - list-style: none; 144 - flex-wrap: wrap; 145 - gap: .5em; 146 - } 147 - 148 - .release-links li { 149 - flex-grow: 1; 150 - } 151 - 152 - .release-links a { 153 - padding: .5em; 154 - display: block; 155 - 156 - border-radius: .5em; 157 - text-decoration: none; 158 - color: #f0f0f0; 159 - background: #303030; 160 - text-align: center; 161 - 162 - transition: color .1s, background .1s; 163 - } 164 - 165 - .release-links a:hover { 166 - color: #303030; 167 - background: #f0f0f0; 168 - } 169 - 170 - .release-actions { 171 - margin-top: .5em; 172 - } 173 - 174 - .release-actions a { 175 - margin-right: .3em; 176 - padding: .3em .5em; 177 - display: inline-block; 178 - 179 - border-radius: .3em; 180 - background: #e0e0e0; 181 - 182 - transition: color .1s, background .1s; 183 - } 184 - 185 - .release-actions a:hover { 186 - color: #303030; 187 - background: #f0f0f0; 188 - 189 - text-decoration: none; 190 - } 191 - 192 - .artist { 193 - margin-bottom: .5em; 194 - padding: .5em; 195 - display: flex; 196 100 flex-direction: row; 197 101 align-items: center; 198 - gap: .5em; 199 - 200 - border-radius: .5em; 201 - background: #f8f8f8f8; 202 - border: 1px solid #808080; 203 - } 204 - 205 - .artist:hover { 206 - text-decoration: hover; 207 - } 208 - 209 - .artist-avatar { 210 - width: 32px; 211 - height: 32px; 212 - object-fit: cover; 213 - border-radius: 100%; 214 - } 215 - 216 - .track { 217 - margin-bottom: 1em; 218 - padding: 1em; 219 - display: flex; 220 - flex-direction: column; 221 - gap: .5em; 222 - 223 - border-radius: .5em; 224 - background: #f8f8f8f8; 225 - border: 1px solid #808080; 226 - } 227 - 228 - h2.track-title { 229 - margin: 0; 230 - display: flex; 231 - flex-direction: row; 232 102 justify-content: space-between; 233 103 } 234 104 235 - .track-album { 236 - margin-left: auto; 237 - font-style: italic; 238 - font-size: .75em; 239 - opacity: .5; 240 - } 241 - 242 - .track-album.empty { 243 - color: #ff2020; 244 - opacity: 1; 245 - } 246 - 247 - .track-description { 248 - font-style: italic; 249 - } 250 - 251 - .track-lyrics { 252 - max-height: 10em; 253 - overflow-y: scroll; 254 - } 255 - 256 - .track .empty { 257 - opacity: 0.75; 105 + .card-title h1, 106 + .card-title h2, 107 + .card-title h3 { 108 + margin: 0; 258 109 } 259 110 260 111 @media screen and (max-width: 520px) {
+96
admin/static/edit-release.js
··· 1 + import Stateful from "/script/silver.min.js" 2 + 3 + const releaseID = document.getElementById("release").dataset.id; 4 + const artwork_input = document.getElementById("artwork"); 5 + const type_input = document.getElementById("type"); 6 + const desc_input = document.getElementById("description"); 7 + const date_input = document.getElementById("release-date"); 8 + const buyname_input = document.getElementById("buyname"); 9 + const buylink_input = document.getElementById("buylink"); 10 + const vis_input = document.getElementById("visibility"); 11 + const save_btn = document.getElementById("save"); 12 + 13 + let token = atob(localStorage.getItem("arime-token")); 14 + 15 + let edited = new Stateful(false); 16 + 17 + let release_data = update_data(undefined); 18 + 19 + function update_data(old) { 20 + let release_data = { 21 + visible: vis_input.value === "true", 22 + title: undefined, 23 + description: desc_input.value, 24 + type: type_input.value, 25 + releaseDate: date_input.value, 26 + artwork: artwork_input.attributes.src.value, 27 + buyname: buyname_input.value, 28 + buylink: buylink_input.value, 29 + }; 30 + 31 + if (release_data && release_data != old) { 32 + edited.set(true); 33 + } 34 + 35 + return release_data; 36 + } 37 + 38 + function save_release() { 39 + console.table(release_data); 40 + 41 + edited.set(false); 42 + 43 + (async () => { 44 + const res = await fetch( 45 + "/api/v1/music/" + releaseID, { 46 + method: "PUT", 47 + body: JSON.stringify(release_data), 48 + headers: { 49 + "Content-Type": "application/json", 50 + "Authorisation": "Bearer " + token, 51 + }, 52 + }); 53 + 54 + if (!res.ok) { 55 + const text = await res.text(); 56 + console.error(text); 57 + alert(text); 58 + return; 59 + } 60 + 61 + location = location; 62 + })(); 63 + } 64 + window.save_release = save_release; 65 + 66 + edited.onUpdate(edited => { 67 + save_btn.disabled = !edited; 68 + }) 69 + 70 + artwork_input.addEventListener("click", () => { 71 + release_data = update_data(release_data); 72 + }); 73 + type_input.addEventListener("change", () => { 74 + release_data = update_data(release_data); 75 + }); 76 + desc_input.addEventListener("change", () => { 77 + release_data = update_data(release_data); 78 + }); 79 + date_input.addEventListener("change", () => { 80 + release_data = update_data(release_data); 81 + }); 82 + buyname_input.addEventListener("change", () => { 83 + release_data = update_data(release_data); 84 + }); 85 + buylink_input.addEventListener("change", () => { 86 + release_data = update_data(release_data); 87 + }); 88 + vis_input.addEventListener("change", () => { 89 + release_data = update_data(release_data); 90 + }); 91 + 92 + save_btn.addEventListener("click", () => { 93 + if (!edited.get()) return; 94 + 95 + save_release(); 96 + })
+183
admin/static/index.css
··· 1 + .create-btn { 2 + background: #c4ff6a; 3 + padding: .5em .8em; 4 + border-radius: .5em; 5 + border: 1px solid #84b141; 6 + text-decoration: none; 7 + } 8 + .create-btn:hover { 9 + background: #fff; 10 + border-color: #d0d0d0; 11 + text-decoration: inherit; 12 + } 13 + .create-btn:active { 14 + background: #d0d0d0; 15 + border-color: #808080; 16 + text-decoration: inherit; 17 + } 18 + 19 + .release { 20 + margin-bottom: 1em; 21 + padding: 1em; 22 + display: flex; 23 + flex-direction: row; 24 + gap: 1em; 25 + 26 + border-radius: .5em; 27 + background: #f8f8f8f8; 28 + border: 1px solid #808080; 29 + } 30 + 31 + .release-artwork { 32 + width: 96px; 33 + 34 + display: flex; 35 + justify-content: center; 36 + align-items: center; 37 + } 38 + 39 + .release-artwork img { 40 + width: 100%; 41 + aspect-ratio: 1; 42 + } 43 + 44 + .latest-release .release-info { 45 + width: 300px; 46 + flex-direction: column; 47 + } 48 + 49 + .release-title small { 50 + opacity: .75; 51 + } 52 + 53 + .release-links { 54 + margin: .5em 0; 55 + padding: 0; 56 + display: flex; 57 + flex-direction: row; 58 + list-style: none; 59 + flex-wrap: wrap; 60 + gap: .5em; 61 + } 62 + 63 + .release-links li { 64 + flex-grow: 1; 65 + } 66 + 67 + .release-links a { 68 + padding: .5em; 69 + display: block; 70 + 71 + border-radius: .5em; 72 + text-decoration: none; 73 + color: #f0f0f0; 74 + background: #303030; 75 + text-align: center; 76 + 77 + transition: color .1s, background .1s; 78 + } 79 + 80 + .release-links a:hover { 81 + color: #303030; 82 + background: #f0f0f0; 83 + } 84 + 85 + .release-actions { 86 + margin-top: .5em; 87 + } 88 + 89 + .release-actions a { 90 + margin-right: .3em; 91 + padding: .3em .5em; 92 + display: inline-block; 93 + 94 + border-radius: .3em; 95 + background: #e0e0e0; 96 + 97 + transition: color .1s, background .1s; 98 + } 99 + 100 + .release-actions a:hover { 101 + color: #303030; 102 + background: #f0f0f0; 103 + 104 + text-decoration: none; 105 + } 106 + 107 + .artist { 108 + margin-bottom: .5em; 109 + padding: .5em; 110 + display: flex; 111 + flex-direction: row; 112 + align-items: center; 113 + gap: .5em; 114 + 115 + border-radius: .5em; 116 + background: #f8f8f8f8; 117 + border: 1px solid #808080; 118 + } 119 + 120 + .artist:hover { 121 + text-decoration: hover; 122 + } 123 + 124 + .artist-avatar { 125 + width: 32px; 126 + height: 32px; 127 + object-fit: cover; 128 + border-radius: 100%; 129 + } 130 + 131 + .track { 132 + margin-bottom: 1em; 133 + padding: 1em; 134 + display: flex; 135 + flex-direction: column; 136 + gap: .5em; 137 + 138 + border-radius: .5em; 139 + background: #f8f8f8f8; 140 + border: 1px solid #808080; 141 + } 142 + 143 + .card h2.track-title { 144 + margin: 0; 145 + display: flex; 146 + flex-direction: row; 147 + justify-content: space-between; 148 + } 149 + 150 + .track-id { 151 + width: fit-content; 152 + font-family: "Monaspace Argon", monospace; 153 + font-size: .8em; 154 + font-style: italic; 155 + line-height: 1em; 156 + user-select: all; 157 + } 158 + 159 + .track-album { 160 + margin-left: auto; 161 + font-style: italic; 162 + font-size: .75em; 163 + opacity: .5; 164 + } 165 + 166 + .track-album.empty { 167 + color: #ff2020; 168 + opacity: 1; 169 + } 170 + 171 + .track-description { 172 + font-style: italic; 173 + } 174 + 175 + .track-lyrics { 176 + max-height: 10em; 177 + overflow-y: scroll; 178 + } 179 + 180 + .track .empty { 181 + opacity: 0.75; 182 + } 183 +
+219
admin/static/release.css
··· 1 + #release { 2 + margin-bottom: 1em; 3 + padding: 1em; 4 + display: flex; 5 + flex-direction: row; 6 + gap: 1.2em; 7 + 8 + border-radius: .5em; 9 + background: #f8f8f8f8; 10 + border: 1px solid #808080; 11 + } 12 + 13 + .release-artwork { 14 + width: 200px; 15 + 16 + display: flex; 17 + justify-content: center; 18 + align-items: start; 19 + } 20 + 21 + .release-artwork img { 22 + width: 100%; 23 + aspect-ratio: 1; 24 + } 25 + .release-artwork img:hover { 26 + outline: 1px solid #808080; 27 + cursor: pointer; 28 + } 29 + 30 + .release-info { 31 + margin: .5em 0; 32 + flex-grow: 1; 33 + display: flex; 34 + flex-direction: column; 35 + } 36 + 37 + .release-title { 38 + margin: 0; 39 + } 40 + 41 + .release-title small { 42 + opacity: .75; 43 + } 44 + 45 + .release-info table { 46 + width: 100%; 47 + margin: .5em 0; 48 + border-collapse: collapse; 49 + } 50 + .release-info table td { 51 + padding: .2em; 52 + border-bottom: 1px solid #d0d0d0; 53 + } 54 + .release-info table tr td:first-child { 55 + vertical-align: top; 56 + opacity: .66; 57 + } 58 + .release-info table tr td:not(:first-child):hover { 59 + background: #e8e8e8; 60 + cursor: pointer; 61 + } 62 + .release-info table td select, 63 + .release-info table td input, 64 + .release-info table td textarea { 65 + padding: .2em; 66 + resize: none; 67 + width: 100%; 68 + font-family: inherit; 69 + font-size: inherit; 70 + color: inherit; 71 + border: none; 72 + background: none; 73 + outline: none; 74 + } 75 + .release-info table td:has(select), 76 + .release-info table td:has(input), 77 + .release-info table td:has(textarea) { 78 + padding: 0; 79 + } 80 + 81 + button, .button { 82 + padding: .5em .8em; 83 + font-family: inherit; 84 + font-size: inherit; 85 + border-radius: .5em; 86 + border: 1px solid #a0a0a0; 87 + background: #f0f0f0; 88 + color: inherit; 89 + } 90 + button:hover, .button:hover { 91 + background: #fff; 92 + border-color: #d0d0d0; 93 + } 94 + button:active, .button:active { 95 + background: #d0d0d0; 96 + border-color: #808080; 97 + } 98 + 99 + button.edit { 100 + color: inherit; 101 + background: #c4ff6a; 102 + border-color: #84b141; 103 + } 104 + button.edit:hover { 105 + background: #fff; 106 + border-color: #d0d0d0; 107 + } 108 + button.edit:active { 109 + background: #d0d0d0; 110 + border-color: #808080; 111 + } 112 + 113 + button.save { 114 + background: #6fd7ff; 115 + border-color: #6f9eb0; 116 + } 117 + button.save:hover { 118 + background: #fff; 119 + border-color: #d0d0d0; 120 + } 121 + button.save:active { 122 + background: #d0d0d0; 123 + border-color: #808080; 124 + } 125 + button[disabled] { 126 + background: #d0d0d0 !important; 127 + border-color: #808080 !important; 128 + opacity: .5; 129 + cursor: not-allowed !important; 130 + } 131 + 132 + .release-actions { 133 + margin-top: auto; 134 + display: flex; 135 + gap: .5em; 136 + flex-direction: row; 137 + justify-content: right; 138 + } 139 + 140 + .credit { 141 + margin-bottom: .5em; 142 + padding: .5em; 143 + display: flex; 144 + flex-direction: row; 145 + align-items: center; 146 + gap: 1em; 147 + 148 + border-radius: .5em; 149 + background: #f8f8f8f8; 150 + border: 1px solid #808080; 151 + } 152 + 153 + .credit .artist-avatar { 154 + border-radius: .5em; 155 + } 156 + 157 + .credit .artist-name { 158 + font-weight: bold; 159 + } 160 + 161 + .credit .artist-role small { 162 + font-size: inherit; 163 + opacity: .66; 164 + } 165 + 166 + .track { 167 + margin-bottom: 1em; 168 + padding: 1em; 169 + display: flex; 170 + flex-direction: column; 171 + gap: .5em; 172 + 173 + border-radius: .5em; 174 + background: #f8f8f8f8; 175 + border: 1px solid #808080; 176 + } 177 + 178 + .card h2.track-title { 179 + margin: 0; 180 + display: flex; 181 + flex-direction: row; 182 + justify-content: space-between; 183 + } 184 + 185 + .track-id { 186 + width: fit-content; 187 + font-family: "Monaspace Argon", monospace; 188 + font-size: .8em; 189 + font-style: italic; 190 + line-height: 1em; 191 + user-select: all; 192 + -webkit-user-select: all; 193 + } 194 + 195 + .track-album { 196 + margin-left: auto; 197 + font-style: italic; 198 + font-size: .75em; 199 + opacity: .5; 200 + } 201 + 202 + .track-album.empty { 203 + color: #ff2020; 204 + opacity: 1; 205 + } 206 + 207 + .track-description { 208 + font-style: italic; 209 + } 210 + 211 + .track-lyrics { 212 + max-height: 10em; 213 + overflow-y: scroll; 214 + } 215 + 216 + .track .empty { 217 + opacity: 0.75; 218 + } 219 +
+143
admin/views/edit-release.html
··· 1 + {{define "head"}} 2 + <title>editing {{.Title}} - ari melody 💫</title> 3 + <link rel="shortcut icon" href="{{.GetArtwork}}" type="image/x-icon"> 4 + 5 + <link rel="stylesheet" href="/admin/static/release.css"> 6 + {{end}} 7 + 8 + {{define "content"}} 9 + <main> 10 + 11 + <div id="release" data-id="{{.ID}}"> 12 + <div class="release-artwork"> 13 + <img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork"> 14 + </div> 15 + <div class="release-info"> 16 + <h1 class="release-title"> 17 + <!-- <input type="text" name="Title" value="{{.Title}}"> --> 18 + <span id="title" editable="true">{{.Title}}</span> 19 + <small>{{.GetReleaseYear}}</small> 20 + </h1> 21 + <table> 22 + <tr> 23 + <td>Artists</td> 24 + <td>{{.PrintArtists true true}}</td> 25 + </tr> 26 + <tr> 27 + <td>Type</td> 28 + <td> 29 + {{$t := .ReleaseType}} 30 + <select name="Type" id="type"> 31 + <option value="single" {{if eq $t "single"}}selected{{end}}> 32 + Single 33 + </option> 34 + <option value="album" {{if eq $t "album"}}selected{{end}}> 35 + Album 36 + </option> 37 + <option value="ep" {{if eq $t "ep"}}selected{{end}}> 38 + EP 39 + </option> 40 + <option value="compilation" {{if eq $t "compilation"}}selected{{end}}> 41 + Compilation 42 + </option> 43 + </select> 44 + </td> 45 + </tr> 46 + <tr> 47 + <td>Description</td> 48 + <td> 49 + <textarea 50 + name="Description" 51 + value="{{.Description}}" 52 + placeholder="No description provided." 53 + rows="3" 54 + id="description" 55 + >{{.Description}}</textarea> 56 + </td> 57 + </tr> 58 + <tr> 59 + <td>Release Date</td> 60 + <td> 61 + <input type="datetime-local" name="Release Date" id="release-date" value="{{.TextReleaseDate}}"> 62 + </td> 63 + </tr> 64 + <tr> 65 + <td>Buy Name</td> 66 + <td> 67 + <input type="text" name="Buy Name" id="buyname" value="{{.Buyname}}"> 68 + </td> 69 + </tr> 70 + <tr> 71 + <td>Buy Link</td> 72 + <td> 73 + <input type="text" name="Buy Link" id="buylink" value="{{.Buylink}}"> 74 + </td> 75 + </tr> 76 + <tr> 77 + <td>Visible</td> 78 + <td> 79 + <select name="Visibility" id="visibility"> 80 + <option value="true" {{if .Visible}}selected{{end}}>True</option> 81 + <option value="false" {{if not .Visible}}selected{{end}}>False</option> 82 + </select> 83 + </td> 84 + </tr> 85 + </table> 86 + <div class="release-actions"> 87 + <a href="/music/{{.ID}}" class="button">Gateway</a> 88 + <button type="submit" class="save" id="save" disabled>Save</button> 89 + </div> 90 + </div> 91 + </div> 92 + 93 + <div class="card-title"> 94 + <h2>Credits ({{len .Credits}})</h2> 95 + <button id="update-credits" class="edit">Edit</button> 96 + </div> 97 + <div class="card credits"> 98 + {{range $Credit := .Credits}} 99 + <div class="credit"> 100 + <img src="{{$Credit.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 101 + <div class="credit-info"> 102 + <p class="artist-name"><a href="/admin/artists/{{$Credit.Artist.ID}}">{{$Credit.Artist.Name}}</a></p> 103 + <p class="artist-role"> 104 + {{$Credit.Role}} 105 + {{if $Credit.Primary}} 106 + <small>(Primary)</small> 107 + {{end}} 108 + </p> 109 + </div> 110 + </div> 111 + {{end}} 112 + {{if not .Credits}} 113 + <p>There are no credits.</p> 114 + {{end}} 115 + </div> 116 + 117 + <div class="card-title"> 118 + <h2>Tracklist ({{len .Tracks}})</h2> 119 + <button id="update-tracks" class="edit">Edit</button> 120 + </div> 121 + <div class="card tracks"> 122 + {{range $Track := .Tracks}} 123 + <div class="track" data-id="{{$Track.ID}}"> 124 + <h2 class="track-title">{{$Track.Number}}. {{$Track.Title}}</h2> 125 + <p class="track-id">{{$Track.ID}}</p> 126 + {{if $Track.Description}} 127 + <p class="track-description">{{$Track.Description}}</p> 128 + {{else}} 129 + <p class="track-description empty">No description provided.</p> 130 + {{end}} 131 + {{if $Track.Lyrics}} 132 + <p class="track-lyrics">{{$Track.Lyrics}}</p> 133 + {{else}} 134 + <p class="track-lyrics empty">There are no lyrics.</p> 135 + {{end}} 136 + </div> 137 + {{end}} 138 + </div> 139 + 140 + </main> 141 + 142 + <script type="module" src="/admin/static/edit-release.js" defer></script> 143 + {{end}}
+35
admin/views/login.html
··· 1 + {{define "head"}} 2 + <title>login - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + 5 + <style> 6 + p a { 7 + color: #2a67c8; 8 + } 9 + 10 + a.discord { 11 + color: #5865F2; 12 + } 13 + </style> 14 + {{end}} 15 + 16 + {{define "content"}} 17 + <main> 18 + 19 + {{if .Token}} 20 + <meta http-equiv="refresh" content="5;url=/admin/" /> 21 + <meta name="token" content="{{.Token}}" /> 22 + <p> 23 + Logged in successfully. 24 + You should be redirected to <a href="/admin">/admin</a> in 5 seconds. 25 + <script> 26 + const token = document.querySelector("meta[name=token]").content; 27 + localStorage.setItem("arime-token", btoa(token)); 28 + </script> 29 + </p> 30 + {{else}} 31 + <p>Log in with <a href="{{.DiscordURI}}" class="discord">Discord</a>.</p> 32 + {{end}} 33 + 34 + </main> 35 + {{end}}
+25
admin/views/logout.html
··· 1 + {{define "head"}} 2 + <title>admin - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + 5 + <style> 6 + p a { 7 + color: #2a67c8; 8 + } 9 + </style> 10 + {{end}} 11 + 12 + {{define "content"}} 13 + <main> 14 + 15 + <meta http-equiv="refresh" content="5;url=/" /> 16 + <p> 17 + Logged out successfully. 18 + You should be redirected to <a href="/">/</a> in 5 seconds. 19 + <script> 20 + localStorage.removeItem("arime-token"); 21 + </script> 22 + </p> 23 + 24 + </main> 25 + {{end}}
+23 -16
api/artist.go
··· 10 10 controller "arimelody.me/arimelody.me/music/controller" 11 11 ) 12 12 13 + type artistJSON struct { 14 + ID string `json:"id"` 15 + Name *string `json:"name"` 16 + Website *string `json:"website"` 17 + Avatar *string `json:"avatar"` 18 + } 19 + 13 20 func ServeAllArtists() http.Handler { 14 21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 22 w.Header().Add("Content-Type", "application/json") ··· 78 85 return 79 86 } 80 87 81 - var data model.Artist 88 + var data artistJSON 82 89 err := json.NewDecoder(r.Body).Decode(&data) 83 90 if err != nil { 84 91 fmt.Printf("Failed to create artist: %s\n", err) ··· 90 97 http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest) 91 98 return 92 99 } 93 - if data.Name == "" { 100 + if data.Name == nil || *data.Name == "" { 94 101 http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest) 95 102 return 96 103 } ··· 102 109 103 110 var artist = model.Artist{ 104 111 ID: data.ID, 105 - Name: data.Name, 106 - Website: data.Website, 107 - Avatar: data.Avatar, 112 + Name: *data.Name, 113 + Website: *data.Website, 114 + Avatar: *data.Avatar, 108 115 } 109 116 110 117 err = controller.CreateArtistDB(global.DB, &artist) ··· 138 145 return 139 146 } 140 147 141 - var data model.Artist 148 + var data artistJSON 142 149 err := json.NewDecoder(r.Body).Decode(&data) 143 150 if err != nil { 144 151 fmt.Printf("Failed to update artist: %s\n", err) ··· 153 160 return 154 161 } 155 162 156 - if data.ID == "" { data.ID = artist.ID } 163 + var update = *artist 157 164 158 - if data.Name == "" { 159 - http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest) 160 - return 161 - } 165 + if data.ID != "" { update.ID = data.ID } 166 + if data.Name != nil { update.Name = *data.Name } 167 + if data.Website != nil { update.Website = *data.Website } 168 + if data.Avatar != nil { update.Avatar = *data.Avatar } 162 169 163 - err = controller.UpdateArtistDB(global.DB, &data) 170 + err = controller.UpdateArtistDB(global.DB, &update) 164 171 if err != nil { 165 172 fmt.Printf("Failed to update artist %s: %s\n", artist.ID, err) 166 173 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 167 174 return 168 175 } 169 176 170 - artist.ID = data.ID 171 - artist.Name = data.Name 172 - artist.Website = data.Website 173 - artist.Avatar = data.Avatar 177 + artist.ID = update.ID 178 + artist.Name = update.Name 179 + artist.Website = update.Website 180 + artist.Avatar = update.Avatar 174 181 175 182 w.Header().Add("Content-Type", "application/json") 176 183 err = json.NewEncoder(w).Encode(artist)
+64 -44
api/release.go
··· 15 15 16 16 type releaseBodyJSON struct { 17 17 ID string `json:"id"` 18 - Visible bool `json:"visible"` 19 - Title string `json:"title"` 20 - Description string `json:"description"` 21 - ReleaseType model.ReleaseType `json:"type"` 22 - ReleaseDate time.Time `json:"releaseDate"` 23 - Artwork string `json:"artwork"` 24 - Buyname string `json:"buyname"` 25 - Buylink string `json:"buylink"` 18 + Visible *bool `json:"visible"` 19 + Title *string `json:"title"` 20 + Description *string `json:"description"` 21 + ReleaseType *model.ReleaseType `json:"type"` 22 + ReleaseDate *string `json:"releaseDate"` 23 + Artwork *string `json:"artwork"` 24 + Buyname *string `json:"buyname"` 25 + Buylink *string `json:"buylink"` 26 26 } 27 27 28 28 func ServeCatalog() http.Handler { ··· 36 36 Artwork string `json:"artwork"` 37 37 Buyname string `json:"buyname"` 38 38 Buylink string `json:"buylink"` 39 - Links []*model.Link `json:"links"` 39 + Links []*model.Link `json:"links"` 40 40 } 41 41 42 42 catalog := []CatalogItem{} ··· 85 85 http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) 86 86 return 87 87 } 88 - if data.Title == "" { 88 + if *data.Title == "" { 89 89 http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest) 90 90 return 91 91 } 92 + if data.Buyname == nil || *data.Buyname == "" { *data.Buyname = "buy" } 93 + if data.Buylink == nil || *data.Buylink == "" { *data.Buylink = "https://arimelody.me" } 92 94 93 95 if global.GetRelease(data.ID) != nil { 94 96 http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) 95 97 return 96 98 } 97 99 100 + releaseDate := time.Time{} 101 + if *data.ReleaseDate == "" { 102 + http.Error(w, "Release date cannot be empty\n", http.StatusBadRequest) 103 + return 104 + } else if data.ReleaseDate != nil { 105 + releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) 106 + if err != nil { 107 + http.Error(w, "Invalid release date", http.StatusBadRequest) 108 + return 109 + } 110 + } 111 + 98 112 var release = model.Release{ 99 113 ID: data.ID, 100 - Visible: data.Visible, 101 - Title: data.Title, 102 - Description: data.Description, 103 - ReleaseType: data.ReleaseType, 104 - ReleaseDate: data.ReleaseDate, 105 - Artwork: data.Artwork, 106 - Buyname: data.Buyname, 107 - Buylink: data.Buylink, 114 + Visible: *data.Visible, 115 + Title: *data.Title, 116 + Description: *data.Description, 117 + ReleaseType: *data.ReleaseType, 118 + ReleaseDate: releaseDate, 119 + Artwork: *data.Artwork, 120 + Buyname: *data.Buyname, 121 + Buylink: *data.Buylink, 108 122 Links: []*model.Link{}, 109 123 Credits: []*model.Credit{}, 110 124 Tracks: []*model.Track{}, ··· 153 167 return 154 168 } 155 169 156 - if data.ID == "" { data.ID = release.ID } 157 - 158 - if data.Title == "" { 159 - http.Error(w, "Release title cannot be blank\n", http.StatusBadRequest) 160 - return 170 + var update = *release 171 + if data.ID != "" { update.ID = data.ID } 172 + if data.Visible != nil { update.Visible = *data.Visible } 173 + if data.Title != nil { update.Title = *data.Title } 174 + if data.Description != nil { update.Description = *data.Description } 175 + if data.ReleaseType != nil { update.ReleaseType = *data.ReleaseType } 176 + if data.ReleaseDate != nil { 177 + newDate, err := time.Parse("2006-01-02T15:04", *data.ReleaseDate) 178 + if err != nil { 179 + http.Error(w, "Invalid release date", http.StatusBadRequest) 180 + return 181 + } 182 + update.ReleaseDate = newDate 161 183 } 162 - 163 - var new_release = model.Release{ 164 - ID: data.ID, 165 - Visible: data.Visible, 166 - Title: data.Title, 167 - Description: data.Description, 168 - ReleaseType: data.ReleaseType, 169 - ReleaseDate: data.ReleaseDate, 170 - Artwork: data.Artwork, 171 - Buyname: data.Buyname, 172 - Buylink: data.Buylink, 184 + if data.Artwork != nil { update.Artwork = *data.Artwork } 185 + if data.Buyname != nil { 186 + if *data.Buyname == "" { 187 + http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest) 188 + return 189 + } 190 + update.Buyname = *data.Buyname 173 191 } 192 + if data.Buylink != nil { update.Buylink = *data.Buylink } 174 193 175 - err = controller.UpdateReleaseDB(global.DB, release) 194 + err = controller.UpdateReleaseDB(global.DB, &update) 176 195 if err != nil { 177 196 fmt.Printf("Failed to update release %s: %s\n", release.ID, err) 178 197 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 179 198 return 180 199 } 181 200 182 - release.ID = new_release.ID 183 - release.Visible = new_release.Visible 184 - release.Title = new_release.Title 185 - release.Description = new_release.Description 186 - release.ReleaseType = new_release.ReleaseType 187 - release.ReleaseDate = new_release.ReleaseDate 188 - release.Artwork = new_release.Artwork 189 - release.Buyname = new_release.Buyname 190 - release.Buylink = new_release.Buylink 201 + release.ID = update.ID 202 + release.Visible = update.Visible 203 + release.Title = update.Title 204 + release.Description = update.Description 205 + release.ReleaseType = update.ReleaseType 206 + release.ReleaseDate = update.ReleaseDate 207 + release.Artwork = update.Artwork 208 + release.Buyname = update.Buyname 209 + release.Buylink = update.Buylink 191 210 192 211 w.Header().Add("Content-Type", "application/json") 193 212 err = json.NewEncoder(w).Encode(release) ··· 195 214 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 196 215 return 197 216 } 217 + return 198 218 } 199 219 200 220 if len(segments) == 2 {
+1 -4
api/track.go
··· 122 122 123 123 data.ID = trackID 124 124 125 - if data.Title == "" { 126 - http.Error(w, "Track title cannot be blank\n", http.StatusBadRequest) 127 - return 128 - } 125 + if data.Title == "" { data.Title = track.Title } 129 126 130 127 err = controller.UpdateTrackDB(global.DB, &data) 131 128 if err != nil {
+2 -2
discord/discord.go
··· 57 57 58 58 AuthInfoResponse struct { 59 59 Application struct { 60 - Id string `json:"id"` 60 + ID string `json:"id"` 61 61 Name string `json:"name"` 62 62 Icon string `json:"icon"` 63 63 Description string `json:"description"` ··· 72 72 } 73 73 74 74 DiscordUser struct { 75 - Id string `json:"id"` 75 + ID string `json:"id"` 76 76 Username string `json:"username"` 77 77 Avatar string `json:"avatar"` 78 78 Discriminator string `json:"discriminator"`
+3 -4
music/controller/release.go
··· 76 76 release.Title, 77 77 release.Description, 78 78 release.ReleaseType, 79 - release.ReleaseDate.Format("2-Jan-2006"), 79 + release.ReleaseDate.Format("2006-01-02 15:04:05"), 80 80 release.Artwork, 81 81 release.Buyname, 82 82 release.Buylink, ··· 91 91 func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { 92 92 _, err := db.Exec( 93 93 "UPDATE musicrelease SET "+ 94 - "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9) "+ 95 - "VALUES ($2, $3, $4, $5, $6, $7, $8, $9) "+ 94 + "visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9 "+ 96 95 "WHERE id=$1", 97 96 release.ID, 98 97 release.Visible, 99 98 release.Title, 100 99 release.Description, 101 100 release.ReleaseType, 102 - release.ReleaseDate.Format("2-Jan-2006"), 101 + release.ReleaseDate.Format("2006-01-02 15:04:05"), 103 102 release.Artwork, 104 103 release.Buyname, 105 104 release.Buylink,
+5 -1
music/model/artist.go
··· 2 2 3 3 type ( 4 4 Artist struct { 5 - ID string `json:"id"` 5 + ID string `json:"id"` 6 6 Name string `json:"name"` 7 7 Website string `json:"website"` 8 8 Avatar string `json:"avatar"` 9 9 } 10 10 ) 11 + 12 + func (artist Artist) GetWebsite() string { 13 + return artist.Website 14 + } 11 15 12 16 func (artist Artist) GetAvatar() string { 13 17 if artist.Avatar == "" {
+21 -17
music/model/release.go
··· 8 8 type ( 9 9 ReleaseType string 10 10 Release struct { 11 - ID string `json:"id"` 12 - Visible bool `json:"visible"` 13 - Title string `json:"title"` 14 - Description string `json:"description"` 15 - ReleaseType ReleaseType `json:"type" db:"type"` 16 - ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 17 - Artwork string `json:"artwork"` 18 - Buyname string `json:"buyname"` 19 - Buylink string `json:"buylink"` 20 - Links []*Link `json:"links"` 21 - Credits []*Credit `json:"credits"` 22 - Tracks []*Track `json:"tracks"` 11 + ID string `json:"id"` 12 + Visible bool `json:"visible"` 13 + Title string `json:"title"` 14 + Description string `json:"description"` 15 + ReleaseType ReleaseType `json:"type" db:"type"` 16 + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 17 + Artwork string `json:"artwork"` 18 + Buyname string `json:"buyname"` 19 + Buylink string `json:"buylink"` 20 + Links []*Link `json:"links"` 21 + Credits []*Credit `json:"credits"` 22 + Tracks []*Track `json:"tracks"` 23 23 } 24 24 ) 25 25 ··· 32 32 33 33 // GETTERS 34 34 35 - func (release Release) GetArtwork() string { 36 - if release.Artwork == "" { 37 - return "/img/default-cover-art.png" 38 - } 39 - return release.Artwork 35 + func (release Release) TextReleaseDate() string { 36 + return release.ReleaseDate.Format("2006-01-02T15:04") 40 37 } 41 38 42 39 func (release Release) PrintReleaseDate() string { ··· 45 42 46 43 func (release Release) GetReleaseYear() int { 47 44 return release.ReleaseDate.Year() 45 + } 46 + 47 + func (release Release) GetArtwork() string { 48 + if release.Artwork == "" { 49 + return "/img/default-cover-art.png" 50 + } 51 + return release.Artwork 48 52 } 49 53 50 54 func (release Release) IsSingle() bool {
+17 -17
music/view/release.go
··· 12 12 "arimelody.me/arimelody.me/music/model" 13 13 ) 14 14 15 + type ( 16 + gatewayTrack struct { 17 + *model.Track 18 + Lyrics template.HTML 19 + Number int 20 + } 21 + 22 + gatewayRelease struct { 23 + *model.Release 24 + Tracks []gatewayTrack 25 + } 26 + ) 27 + 15 28 // HTTP HANDLERS 16 29 17 30 func ServeRelease() http.Handler { ··· 51 64 return 52 65 } 53 66 54 - type ( 55 - GatewayTrack struct { 56 - *model.Track 57 - Lyrics template.HTML 58 - Number int 59 - } 60 - 61 - GatewayRelease struct { 62 - *model.Release 63 - Tracks []GatewayTrack 64 - } 65 - ) 66 - 67 67 id := r.URL.Path[1:] 68 68 release := global.GetRelease(id) 69 69 if release == nil { ··· 71 71 return 72 72 } 73 73 74 - tracks := []GatewayTrack{} 74 + tracks := []gatewayTrack{} 75 75 for i, track := range release.Tracks { 76 - tracks = append([]GatewayTrack{GatewayTrack{ 76 + tracks = append([]gatewayTrack{{ 77 77 Track: track, 78 78 Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)), 79 79 Number: len(release.Tracks) - i, ··· 87 87 return 88 88 } 89 89 90 - lrw := global.LoggingResponseWriter{w, http.StatusOK} 90 + lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} 91 91 92 - global.ServeTemplate("music-gateway.html", GatewayRelease{release, tracks}).ServeHTTP(&lrw, r) 92 + global.ServeTemplate("music-gateway.html", gatewayRelease{release, tracks}).ServeHTTP(&lrw, r) 93 93 94 94 if lrw.Code != http.StatusOK { 95 95 fmt.Printf("Error rendering music gateway for %s\n", id)
+1
public/script/silver.min.js
··· 1 + export default class Stateful{#e;#t=[];constructor(e){this.#e=e}get(){return this.#e}set(e){let t=this.#e;this.#e=e;for(let s in this.#t)this.#t[s](e,t)}update(e){this.set(e(this.#e))}onUpdate(e){return this.#t.push(e),e}removeListener(e){this.#t=this.#t.filter((t=>t!==e))}}
+1 -1
schema.sql
··· 18 18 title text NOT NULL, 19 19 description text, 20 20 type text, 21 - release_date DATE NOT NULL, 21 + release_date TIMESTAMP NOT NULL, 22 22 artwork text, 23 23 buyname text, 24 24 buylink text
+15 -11
views/admin/index.html admin/views/index.html
··· 1 1 {{define "head"}} 2 2 <title>admin - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + <link rel="stylesheet" href="/admin/static/index.css"> 4 5 {{end}} 5 6 6 7 {{define "content"}} 7 - <header> 8 - <img src="/img/favicon.png" alt="" class="icon"> 9 - <a href="/admin">home</a> 10 - <a href="/admin/releases">releases</a> 11 - <a href="/admin/artists">artists</a> 12 - </header> 13 - 14 8 <main> 15 9 16 10 <div class="card-title"> ··· 24 18 <img src="{{$Release.Artwork}}" alt="" width="128" loading="lazy"> 25 19 </div> 26 20 <div class="release-info"> 27 - <h3 class="release-title">{{$Release.Title}} <small>{{$Release.GetReleaseYear}}</small></h3> 21 + <h3 class="release-title"> 22 + {{$Release.Title}} 23 + <small> 24 + {{$Release.GetReleaseYear}} 25 + {{if not $Release.Visible}}(hidden){{end}} 26 + </small> 27 + </h3> 28 28 <p class="release-artists">{{$Release.PrintArtists true true}}</p> 29 - <p class="release-type-single">{{$Release.ReleaseType}} ({{len $Release.Tracks}} tracks)</p> 29 + <p class="release-type-single">{{$Release.ReleaseType}} 30 + ({{len $Release.Tracks}} track{{if not (eq (len $Release.Tracks) 1)}}s{{end}})</p> 30 31 <div class="release-actions"> 31 - <a href="/admin/releases/{{$Release.ID}}">Edit</a> 32 + <a href="/admin/release/{{$Release.ID}}">Edit</a> 32 33 <a href="/music/{{$Release.ID}}" target="_blank">Gateway</a> 33 34 </div> 34 35 </div> ··· 60 61 <a href="/admin/createtrack" class="create-btn">Create New</a> 61 62 </div> 62 63 <div class="card tracks"> 64 + <p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p> 65 + <br> 63 66 {{range $Track := .Tracks}} 64 67 <div class="track"> 65 68 <h2 class="track-title"> ··· 67 70 {{if $Track.Release}} 68 71 <small class="track-album">{{$Track.Release.Title}}</small> 69 72 {{else}} 70 - <small class="track-album empty">(no album)</small> 73 + <small class="track-album empty">(no release)</small> 71 74 {{end}} 72 75 </h2> 76 + <p class="track-id">{{$Track.ID}}</p> 73 77 {{if $Track.Description}} 74 78 <p class="track-description">{{$Track.Description}}</p> 75 79 {{else}}
+7
views/admin/layout.html admin/views/layout.html
··· 13 13 </head> 14 14 15 15 <body> 16 + <header> 17 + <img src="/img/favicon.png" alt="" class="icon"> 18 + <a href="/">arimelody.me</a> 19 + <a href="/admin">home</a> 20 + <a href="/admin/logout" id="logout">log out</a> 21 + </header> 22 + 16 23 {{block "content" .}} 17 24 {{end}} 18 25
-20
views/admin/login.html
··· 1 - {{define "head"}} 2 - <title>admin - ari melody 💫</title> 3 - <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 - 5 - <style> 6 - .discord { 7 - color: #5865F2; 8 - } 9 - </style> 10 - {{end}} 11 - 12 - {{define "content"}} 13 - <main> 14 - 15 - <p>Log in with <a href="{{.}}" class="discord">Discord</a>.</p> 16 - 17 - </main> 18 - 19 - <script type="module" src="/admin/static/admin.js" defer></script> 20 - {{end}}
+2 -2
views/music-gateway.html
··· 88 88 <ul> 89 89 {{range .Credits}} 90 90 {{$Artist := .Artist}} 91 - {{if $Artist.Website}} 92 - <li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li> 91 + {{if $Artist.GetWebsite}} 92 + <li><strong><a href="{{$Artist.GetWebsite}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li> 93 93 {{else}} 94 94 <li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li> 95 95 {{end}}