home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

tracks can be edited! + major template overhaul

+672 -219
+1 -1
.air.toml
··· 14 14 follow_symlink = false 15 15 full_bin = "" 16 16 include_dir = [] 17 - include_ext = ["go", "tpl", "tmpl"] 17 + include_ext = ["go", "tpl", "tmpl", "html"] 18 18 include_file = [] 19 19 kill_delay = "0s" 20 20 log = "build-errors.log"
+23
admin/components/release/release-list-item.html
··· 1 + {{define "release"}} 2 + <div class="release"> 3 + <div class="release-artwork"> 4 + <img src="{{.Artwork}}" alt="" width="128" loading="lazy"> 5 + </div> 6 + <div class="release-info"> 7 + <h3 class="release-title"> 8 + <a href="/admin/release/{{.ID}}">{{.Title}}</a> 9 + <small> 10 + {{.GetReleaseYear}} 11 + {{if not .Visible}}(hidden){{end}} 12 + </small> 13 + </h3> 14 + <p class="release-artists">{{.PrintArtists true true}}</p> 15 + <p class="release-type-single">{{.ReleaseType}} 16 + (<a href="/admin/release/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p> 17 + <div class="release-actions"> 18 + <a href="/admin/release/{{.ID}}">Edit</a> 19 + <a href="/music/{{.ID}}" target="_blank">Gateway</a> 20 + </div> 21 + </div> 22 + </div> 23 + {{end}}
+34 -73
admin/http.go
··· 12 12 13 13 "arimelody.me/arimelody.me/discord" 14 14 "arimelody.me/arimelody.me/global" 15 + musicController "arimelody.me/arimelody.me/music/controller" 15 16 musicModel "arimelody.me/arimelody.me/music/model" 16 17 ) 17 18 ··· 27 28 mux.Handle("/logout", MustAuthorise(LogoutHandler())) 28 29 mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) 29 30 mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) 31 + mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack()))) 32 + mux.Handle("/createtrack", MustAuthorise(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 + track := musicModel.Track{ Title: "Untitled Track" } 34 + trackID, err := musicController.CreateTrackDB(global.DB, &track) 35 + if err != nil { 36 + fmt.Printf("Failed to create track: %s\n", err) 37 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 38 + return 39 + } 40 + track.ID = trackID 41 + global.Tracks = append(global.Tracks, &track) 42 + http.Redirect(w, r, fmt.Sprintf("/admin/track/%s", trackID), http.StatusTemporaryRedirect) 43 + }))) 30 44 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 45 if r.URL.Path != "/" { 32 46 http.NotFound(w, r) ··· 61 75 }) 62 76 } 63 77 64 - serveTemplate("index.html", IndexData{ 78 + err := pages["index"].Execute(w, IndexData{ 65 79 Releases: global.Releases, 66 80 Artists: global.Artists, 67 81 Tracks: tracks, 68 - }).ServeHTTP(w, r) 82 + }) 83 + if err != nil { 84 + fmt.Printf("Error executing template: %s\n", err) 85 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 86 + return 87 + } 69 88 })) 70 89 71 90 return mux ··· 144 163 code := r.URL.Query().Get("code") 145 164 146 165 if code == "" { 147 - serveTemplate("login.html", loginData{DiscordURI: discord.REDIRECT_URI}).ServeHTTP(w, r) 166 + pages["login"].Execute(w, loginData{DiscordURI: discord.REDIRECT_URI}) 148 167 return 149 168 } 150 169 ··· 183 202 cookie.Path = "/" 184 203 http.SetCookie(w, &cookie) 185 204 186 - serveTemplate("login.html", loginData{Token: session.Token}).ServeHTTP(w, r) 205 + err = pages["login"].Execute(w, loginData{Token: session.Token}) 206 + if err != nil { 207 + fmt.Printf("Error rendering admin login page: %s\n", err) 208 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 209 + return 210 + } 187 211 }) 188 212 } 189 213 ··· 207 231 return new_sessions 208 232 }(session.Token) 209 233 210 - serveTemplate("logout.html", nil).ServeHTTP(w, r) 234 + err := pages["logout"].Execute(w, nil) 235 + if err != nil { 236 + fmt.Printf("Error rendering admin logout page: %s\n", err) 237 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 238 + return 239 + } 211 240 }) 212 - } 213 - 214 - func serveTemplate(page string, data any) http.Handler { 215 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 216 - lp_layout := filepath.Join("admin", "views", "layout.html") 217 - lp_prideflag := filepath.Join("views", "prideflag.html") 218 - fp := filepath.Join("admin", "views", filepath.Clean(page)) 219 - 220 - info, err := os.Stat(fp) 221 - if err != nil { 222 - if os.IsNotExist(err) { 223 - http.NotFound(w, r) 224 - return 225 - } 226 - } 227 - 228 - if info.IsDir() { 229 - http.NotFound(w, r) 230 - return 231 - } 232 - 233 - template, err := template.ParseFiles(lp_layout, lp_prideflag, fp) 234 - if err != nil { 235 - fmt.Printf("Error parsing template files: %s\n", err) 236 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 237 - return 238 - } 239 - 240 - err = template.ExecuteTemplate(w, "layout.html", data) 241 - if err != nil { 242 - fmt.Printf("Error executing template: %s\n", err) 243 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 244 - return 245 - } 246 - }) 247 - } 248 - 249 - func serveComponent(page string, data any) http.Handler { 250 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 251 - fp := filepath.Join("admin", "components", filepath.Clean(page)) 252 - 253 - info, err := os.Stat(fp) 254 - if err != nil { 255 - if os.IsNotExist(err) { 256 - http.NotFound(w, r) 257 - return 258 - } 259 - } 260 - 261 - if info.IsDir() { 262 - http.NotFound(w, r) 263 - return 264 - } 265 - 266 - template, err := template.ParseFiles(fp) 267 - if err != nil { 268 - fmt.Printf("Error parsing template files: %s\n", err) 269 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 270 - return 271 - } 272 - 273 - err = template.Execute(w, data); 274 - if err != nil { 275 - fmt.Printf("Error executing template: %s\n", err) 276 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 277 - return 278 - } 279 - }) 280 241 } 281 242 282 243 func staticHandler() http.Handler {
+42 -20
admin/releasehttp.go
··· 4 4 "fmt" 5 5 "html/template" 6 6 "net/http" 7 - "path" 8 7 "strings" 9 8 10 9 "arimelody.me/arimelody.me/global" ··· 71 70 }) 72 71 } 73 72 74 - lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} 75 - 76 - serveTemplate("edit-release.html", gatewayRelease{release, tracks}).ServeHTTP(&lrw, r) 77 - 78 - if lrw.Code != http.StatusOK { 79 - fmt.Printf("Error rendering admin release page for %s\n", id) 80 - return 73 + err := pages["release"].Execute(w, gatewayRelease{release, tracks}) 74 + if err != nil { 75 + fmt.Printf("Error rendering admin release page for %s: %s\n", id, err) 76 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 81 77 } 82 78 }) 83 79 } ··· 85 81 func serveEditCredits(release *model.Release) http.Handler { 86 82 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 83 w.Header().Set("Content-Type", "text/html") 88 - serveComponent(path.Join("credits", "editcredits.html"), release).ServeHTTP(w, r) 89 - return 84 + err := components["editcredits"].Execute(w, release) 85 + if err != nil { 86 + fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) 87 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 88 + } 90 89 }) 91 90 } 92 91 ··· 112 111 } 113 112 114 113 w.Header().Set("Content-Type", "text/html") 115 - serveComponent(path.Join("credits", "addcredit.html"), response{ 114 + err := components["addcredit"].Execute(w, response{ 116 115 ReleaseID: release.ID, 117 116 Artists: artists, 118 - }).ServeHTTP(w, r) 119 - return 117 + }) 118 + if err != nil { 119 + fmt.Printf("Error rendering add credits component for %s: %s\n", release.ID, err) 120 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 121 + } 120 122 }) 121 123 } 122 124 ··· 129 131 } 130 132 131 133 w.Header().Set("Content-Type", "text/html") 132 - serveComponent(path.Join("credits", "newcredit.html"), artist).ServeHTTP(w, r) 134 + err := components["newcredit"].Execute(w, artist) 135 + if err != nil { 136 + fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) 137 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 138 + } 133 139 return 134 140 }) 135 141 } ··· 137 143 func serveEditLinks(release *model.Release) http.Handler { 138 144 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 139 145 w.Header().Set("Content-Type", "text/html") 140 - serveComponent(path.Join("links", "editlinks.html"), release).ServeHTTP(w, r) 146 + err := components["editlinks"].Execute(w, release) 147 + if err != nil { 148 + fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) 149 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 150 + } 141 151 return 142 152 }) 143 153 } ··· 158 168 data.Tracks = append(data.Tracks, Track{track, i + 1}) 159 169 } 160 170 161 - serveComponent(path.Join("tracks", "edittracks.html"), data).ServeHTTP(w, r) 171 + err := components["edittracks"].Execute(w, data) 172 + if err != nil { 173 + fmt.Printf("Error rendering edit tracks component for %s: %s\n", release.ID, err) 174 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 175 + } 162 176 return 163 177 }) 164 178 } ··· 185 199 } 186 200 187 201 w.Header().Set("Content-Type", "text/html") 188 - serveComponent(path.Join("tracks", "addtrack.html"), response{ 202 + err := components["addtrack"].Execute(w, response{ 189 203 ReleaseID: release.ID, 190 204 Tracks: tracks, 191 - }).ServeHTTP(w, r) 205 + }) 206 + if err != nil { 207 + fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err) 208 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 209 + } 192 210 return 193 211 }) 194 212 } ··· 207 225 } 208 226 209 227 w.Header().Set("Content-Type", "text/html") 210 - serveComponent(path.Join("tracks", "newtrack.html"), Track{ 228 + err := components["newtrack"].Execute(w, Track{ 211 229 track, 212 230 len(release.Tracks) + 1, 213 - }).ServeHTTP(w, r) 231 + }) 232 + if err != nil { 233 + fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) 234 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 235 + } 214 236 return 215 237 }) 216 238 }
+2
admin/static/admin.css
··· 93 93 margin: 0 0 .5em 0; 94 94 } 95 95 96 + /* 96 97 .card h3, 97 98 .card p { 98 99 margin: 0; 99 100 } 101 + */ 100 102 101 103 .card-title { 102 104 margin-bottom: 1em;
+10 -2
admin/static/edit-release.css
··· 87 87 .release-info table td select, 88 88 .release-info table td input, 89 89 .release-info table td textarea { 90 + width: 100%; 90 91 padding: .2em; 91 - resize: none; 92 - width: 100%; 93 92 font-family: inherit; 94 93 font-size: inherit; 95 94 color: inherit; ··· 210 209 border-radius: .5em; 211 210 background: #f8f8f8f8; 212 211 border: 1px solid #808080; 212 + } 213 + 214 + .card.credits .credit p { 215 + margin: 0; 213 216 } 214 217 215 218 .card.credits .credit .artist-avatar { ··· 439 442 border-radius: .5em; 440 443 background: #f8f8f8f8; 441 444 border: 1px solid #808080; 445 + } 446 + 447 + .card.tracks .track h3, 448 + .card.tracks .track p { 449 + margin: 0; 442 450 } 443 451 444 452 .card.tracks h2.track-title {
+86 -71
admin/static/edit-release.js
··· 1 1 import Stateful from "/script/silver.min.js" 2 2 3 3 const releaseID = document.getElementById("release").dataset.id; 4 - const title_input = document.getElementById("title"); 5 - const artwork_img = document.getElementById("artwork"); 6 - const artwork_input = document.getElementById("artwork-file"); 7 - const type_input = document.getElementById("type"); 8 - const desc_input = document.getElementById("description"); 9 - const date_input = document.getElementById("release-date"); 10 - const buyname_input = document.getElementById("buyname"); 11 - const buylink_input = document.getElementById("buylink"); 12 - const vis_input = document.getElementById("visibility"); 13 - const save_btn = document.getElementById("save"); 14 - 15 - var artwork_data = artwork_img.attributes.src.value; 16 - 17 - var token = atob(localStorage.getItem("arime-token")); 4 + const titleInput = document.getElementById("title"); 5 + const artworkImg = document.getElementById("artwork"); 6 + const artworkInput = document.getElementById("artwork-file"); 7 + const typeInput = document.getElementById("type"); 8 + const descInput = document.getElementById("description"); 9 + const dateInput = document.getElementById("release-date"); 10 + const buynameInput = document.getElementById("buyname"); 11 + const buylinkInput = document.getElementById("buylink"); 12 + const visInput = document.getElementById("visibility"); 13 + const saveBtn = document.getElementById("save"); 14 + const deleteBtn = document.getElementById("delete"); 18 15 16 + var artworkData = artworkImg.attributes.src.value; 19 17 var edited = new Stateful(false); 18 + var releaseData = updateData(undefined); 20 19 21 - var release_data = update_data(undefined); 22 - 23 - function update_data(old) { 24 - var release_data = { 25 - visible: vis_input.value === "true", 26 - title: title_input.value, 27 - description: desc_input.value, 28 - type: type_input.value, 29 - releaseDate: date_input.value, 30 - artwork: artwork_data, 31 - buyname: buyname_input.value, 32 - buylink: buylink_input.value, 20 + function updateData(old) { 21 + var releaseData = { 22 + visible: visInput.value === "true", 23 + title: titleInput.value, 24 + description: descInput.value, 25 + type: typeInput.value, 26 + releaseDate: dateInput.value, 27 + artwork: artworkData, 28 + buyname: buynameInput.value, 29 + buylink: buylinkInput.value, 33 30 }; 34 31 35 - if (release_data && release_data != old) { 32 + if (releaseData && releaseData != old) { 36 33 edited.set(true); 37 34 } 38 35 39 - return release_data; 36 + return releaseData; 40 37 } 41 38 42 - function save_release() { 43 - console.table(release_data); 39 + function saveRelease() { 40 + console.table(releaseData); 41 + 42 + fetch("/api/v1/music/" + releaseID, { 43 + method: "PUT", 44 + body: JSON.stringify(releaseData), 45 + headers: { "Content-Type": "application/json" } 46 + }).then(res => { 47 + if (!res.ok) { 48 + res.text().then(error => { 49 + console.error(error); 50 + alert("Failed to update release: " + error); 51 + }); 52 + return; 53 + } 44 54 45 - (async () => { 46 - const res = await fetch( 47 - "/api/v1/music/" + releaseID, { 48 - method: "PUT", 49 - body: JSON.stringify(release_data), 50 - headers: { 51 - "Content-Type": "application/json", 52 - "Authorisation": "Bearer " + token, 53 - }, 54 - }); 55 + location = location; 56 + }); 57 + } 55 58 59 + function deleteRelease() { 60 + fetch("/api/v1/music/" + releaseID, { 61 + method: "DELETE", 62 + }).then(res => { 56 63 if (!res.ok) { 57 - const text = await res.text(); 58 - console.error(text); 59 - alert(text); 64 + res.text().then(error => { 65 + console.error(error); 66 + alert("Failed to delete release: " + error); 67 + }); 60 68 return; 61 69 } 62 70 63 - location = location; 64 - })(); 71 + location = "/admin"; 72 + }); 65 73 } 66 74 67 75 edited.onUpdate(edited => { 68 - save_btn.disabled = !edited; 76 + saveBtn.disabled = !edited; 69 77 }) 70 78 71 - title_input.addEventListener("change", () => { 72 - release_data = update_data(release_data); 79 + titleInput.addEventListener("change", () => { 80 + releaseData = updateData(releaseData); 73 81 }); 74 - artwork_img.addEventListener("click", () => { 75 - artwork_input.addEventListener("change", () => { 76 - if (artwork_input.files.length > 0) { 82 + artworkImg.addEventListener("click", () => { 83 + artworkInput.addEventListener("change", () => { 84 + if (artworkInput.files.length > 0) { 77 85 const reader = new FileReader(); 78 86 reader.onload = e => { 79 87 const data = e.target.result; 80 - artwork_img.src = data; 81 - artwork_data = data; 82 - release_data = update_data(release_data); 88 + artworkImg.src = data; 89 + artworkData = data; 90 + releaseData = updateData(releaseData); 83 91 }; 84 - reader.readAsDataURL(artwork_input.files[0]); 92 + reader.readAsDataURL(artworkInput.files[0]); 85 93 } 86 94 }); 87 - artwork_input.click(); 95 + artworkInput.click(); 88 96 }); 89 - type_input.addEventListener("change", () => { 90 - release_data = update_data(release_data); 97 + typeInput.addEventListener("change", () => { 98 + releaseData = updateData(releaseData); 91 99 }); 92 - desc_input.addEventListener("change", () => { 93 - release_data = update_data(release_data); 100 + descInput.addEventListener("change", () => { 101 + releaseData = updateData(releaseData); 94 102 }); 95 - date_input.addEventListener("change", () => { 96 - release_data = update_data(release_data); 103 + dateInput.addEventListener("change", () => { 104 + releaseData = updateData(releaseData); 97 105 }); 98 - buyname_input.addEventListener("change", () => { 99 - release_data = update_data(release_data); 106 + buynameInput.addEventListener("change", () => { 107 + releaseData = updateData(releaseData); 100 108 }); 101 - buylink_input.addEventListener("change", () => { 102 - release_data = update_data(release_data); 109 + buylinkInput.addEventListener("change", () => { 110 + releaseData = updateData(releaseData); 103 111 }); 104 - vis_input.addEventListener("change", () => { 105 - release_data = update_data(release_data); 112 + visInput.addEventListener("change", () => { 113 + releaseData = updateData(releaseData); 106 114 }); 107 115 108 - save_btn.addEventListener("click", () => { 116 + saveBtn.addEventListener("click", () => { 109 117 if (!edited.get()) return; 118 + saveRelease(); 119 + }); 110 120 111 - save_release(); 112 - }) 121 + deleteBtn.addEventListener("click", () => { 122 + if (releaseID != prompt( 123 + "You are about to permanently delete " + releaseID + ". " + 124 + "This action is irreversible. " + 125 + "Please enter \"" + releaseID + "\" to continue.")) return; 126 + deleteRelease(); 127 + });
+219
admin/static/edit-track.css
··· 1 + h1 { 2 + margin: 0 0 .5em 0; 3 + } 4 + 5 + #track { 6 + margin-bottom: 1em; 7 + padding: 1.5em; 8 + display: flex; 9 + flex-direction: row; 10 + gap: 1.2em; 11 + 12 + border-radius: .5em; 13 + background: #f8f8f8f8; 14 + border: 1px solid #808080; 15 + } 16 + 17 + .track-info { 18 + width: 100%; 19 + margin: 0; 20 + flex-grow: 1; 21 + display: flex; 22 + flex-direction: column; 23 + } 24 + 25 + .track-title-header { 26 + margin: 0; 27 + opacity: .5; 28 + } 29 + 30 + .track-title { 31 + margin: 0; 32 + } 33 + 34 + #title { 35 + width: 100%; 36 + margin: -.1em -.2em; 37 + padding: .1em .2em; 38 + font-weight: bold; 39 + font-size: inherit; 40 + border-radius: 4px; 41 + border: 1px solid transparent; 42 + background: transparent; 43 + outline: none; 44 + } 45 + 46 + #title:hover { 47 + background: #ffffff; 48 + border-color: #80808080; 49 + } 50 + 51 + #title:active, 52 + #title:focus { 53 + background: #ffffff; 54 + border-color: #808080; 55 + } 56 + 57 + .track-title small { 58 + opacity: .75; 59 + } 60 + 61 + .track-info h2 { 62 + margin-bottom: .4em; 63 + } 64 + 65 + .track-info textarea { 66 + width: 100%; 67 + padding: .5em; 68 + font-family: inherit; 69 + font-size: inherit; 70 + color: inherit; 71 + border: none; 72 + outline: none; 73 + resize: vertical; 74 + border-radius: 4px; 75 + } 76 + 77 + button, .button { 78 + padding: .5em .8em; 79 + font-family: inherit; 80 + font-size: inherit; 81 + border-radius: .5em; 82 + border: 1px solid #a0a0a0; 83 + background: #f0f0f0; 84 + color: inherit; 85 + } 86 + button:hover, .button:hover { 87 + background: #fff; 88 + border-color: #d0d0d0; 89 + } 90 + button:active, .button:active { 91 + background: #d0d0d0; 92 + border-color: #808080; 93 + } 94 + 95 + button { 96 + color: inherit; 97 + } 98 + button.save { 99 + background: #6fd7ff; 100 + border-color: #6f9eb0; 101 + } 102 + button.delete { 103 + background: #ff7171; 104 + border-color: #7d3535; 105 + } 106 + button:hover { 107 + background: #fff; 108 + border-color: #d0d0d0; 109 + } 110 + button:active { 111 + background: #d0d0d0; 112 + border-color: #808080; 113 + } 114 + button[disabled] { 115 + background: #d0d0d0 !important; 116 + border-color: #808080 !important; 117 + opacity: .5; 118 + cursor: not-allowed !important; 119 + } 120 + 121 + a.delete { 122 + color: #d22828; 123 + } 124 + 125 + .track-actions { 126 + margin-top: 1em; 127 + display: flex; 128 + gap: .5em; 129 + flex-direction: row; 130 + justify-content: right; 131 + } 132 + 133 + .release { 134 + margin-bottom: 1em; 135 + padding: 1em; 136 + display: flex; 137 + flex-direction: row; 138 + gap: 1em; 139 + 140 + border-radius: .5em; 141 + background: #f8f8f8f8; 142 + border: 1px solid #808080; 143 + } 144 + 145 + .release h3, 146 + .release p { 147 + margin: 0; 148 + } 149 + 150 + .release-artwork { 151 + width: 96px; 152 + 153 + display: flex; 154 + justify-content: center; 155 + align-items: center; 156 + } 157 + 158 + .release-artwork img { 159 + width: 100%; 160 + aspect-ratio: 1; 161 + } 162 + 163 + .release-title small { 164 + opacity: .75; 165 + } 166 + 167 + .release-links { 168 + margin: .5em 0; 169 + padding: 0; 170 + display: flex; 171 + flex-direction: row; 172 + list-style: none; 173 + flex-wrap: wrap; 174 + gap: .5em; 175 + } 176 + 177 + .release-links li { 178 + flex-grow: 1; 179 + } 180 + 181 + .release-links a { 182 + padding: .5em; 183 + display: block; 184 + 185 + border-radius: .5em; 186 + text-decoration: none; 187 + color: #f0f0f0; 188 + background: #303030; 189 + text-align: center; 190 + 191 + transition: color .1s, background .1s; 192 + } 193 + 194 + .release-links a:hover { 195 + color: #303030; 196 + background: #f0f0f0; 197 + } 198 + 199 + .release-actions { 200 + margin-top: .5em; 201 + } 202 + 203 + .release-actions a { 204 + margin-right: .3em; 205 + padding: .3em .5em; 206 + display: inline-block; 207 + 208 + border-radius: .3em; 209 + background: #e0e0e0; 210 + 211 + transition: color .1s, background .1s; 212 + } 213 + 214 + .release-actions a:hover { 215 + color: #303030; 216 + background: #f0f0f0; 217 + 218 + text-decoration: none; 219 + }
+58
admin/static/edit-track.js
··· 1 + const trackID = document.getElementById("track").dataset.id; 2 + const titleInput = document.getElementById("title"); 3 + const descInput = document.getElementById("description"); 4 + const lyricsInput = document.getElementById("lyrics"); 5 + const saveBtn = document.getElementById("save"); 6 + const deleteBtn = document.getElementById("delete"); 7 + 8 + saveBtn.addEventListener("click", () => { 9 + fetch("/api/v1/track/" + trackID, { 10 + method: "PUT", 11 + body: JSON.stringify({ 12 + title: titleInput.value, 13 + description: descInput.value, 14 + lyrics: lyricsInput.value, 15 + }), 16 + headers: { "Content-Type": "application/json" } 17 + }).then(res => { 18 + if (!res.ok) { 19 + res.text().then(error => { 20 + console.error(error); 21 + alert("Failed to update track: " + error); 22 + }); 23 + return; 24 + } 25 + 26 + location = location; 27 + }); 28 + }); 29 + 30 + deleteBtn.addEventListener("click", () => { 31 + if (!confirm( 32 + "You are about to permanently delete \"" + titleInput.value + "\".\n" + 33 + "This action is irreversible. Do you wish to continue?")) return; 34 + 35 + fetch("/api/v1/track/" + trackID, { 36 + method: "DELETE", 37 + }).then(res => { 38 + if (!res.ok) { 39 + res.text().then(error => { 40 + console.error(error); 41 + alert("Failed to delete track: " + error); 42 + }); 43 + return; 44 + } 45 + 46 + location = "/admin"; 47 + }); 48 + }); 49 + 50 + [titleInput, descInput, lyricsInput].forEach(input => { 51 + input.addEventListener("change", () => { 52 + saveBtn.disabled = false; 53 + }); 54 + input.addEventListener("keypress", () => { 55 + saveBtn.disabled = false; 56 + }); 57 + }); 58 +
+9
admin/static/index.css
··· 28 28 border: 1px solid #808080; 29 29 } 30 30 31 + .release h3, 32 + .release p { 33 + margin: 0; 34 + } 35 + 31 36 .release-artwork { 32 37 width: 96px; 33 38 ··· 138 143 border-radius: .5em; 139 144 background: #f8f8f8f8; 140 145 border: 1px solid #808080; 146 + } 147 + 148 + .track p { 149 + margin: 0; 141 150 } 142 151 143 152 .card h2.track-title {
+50
admin/templates.go
··· 1 + package admin 2 + 3 + import ( 4 + "html/template" 5 + "path/filepath" 6 + ) 7 + 8 + var pages = map[string]*template.Template{ 9 + "index": template.Must(template.ParseFiles( 10 + filepath.Join("admin", "views", "layout.html"), 11 + filepath.Join("views", "prideflag.html"), 12 + filepath.Join("admin", "components", "release", "release-list-item.html"), 13 + filepath.Join("admin", "views", "index.html"), 14 + )), 15 + 16 + "login": template.Must(template.ParseFiles( 17 + filepath.Join("admin", "views", "layout.html"), 18 + filepath.Join("views", "prideflag.html"), 19 + filepath.Join("admin", "views", "login.html"), 20 + )), 21 + "logout": template.Must(template.ParseFiles( 22 + filepath.Join("admin", "views", "layout.html"), 23 + filepath.Join("views", "prideflag.html"), 24 + filepath.Join("admin", "views", "logout.html"), 25 + )), 26 + 27 + "release": template.Must(template.ParseFiles( 28 + filepath.Join("admin", "views", "layout.html"), 29 + filepath.Join("views", "prideflag.html"), 30 + filepath.Join("admin", "views", "edit-release.html"), 31 + )), 32 + "track": template.Must(template.ParseFiles( 33 + filepath.Join("admin", "views", "layout.html"), 34 + filepath.Join("views", "prideflag.html"), 35 + filepath.Join("admin", "components", "release", "release-list-item.html"), 36 + filepath.Join("admin", "views", "edit-track.html"), 37 + )), 38 + } 39 + 40 + var components = map[string]*template.Template{ 41 + "editcredits": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "editcredits.html"))), 42 + "addcredit": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "addcredit.html"))), 43 + "newcredit": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "newcredit.html"))), 44 + 45 + "editlinks": template.Must(template.ParseFiles(filepath.Join("admin", "components", "links", "editlinks.html"))), 46 + 47 + "edittracks": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "addtrack.html"))), 48 + "addtrack": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "addtrack.html"))), 49 + "newtrack": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "addtrack.html"))), 50 + }
+28
admin/trackhttp.go
··· 1 + package admin 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "strings" 7 + 8 + "arimelody.me/arimelody.me/global" 9 + ) 10 + 11 + func serveTrack() http.Handler { 12 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 + slices := strings.Split(r.URL.Path[1:], "/") 14 + id := slices[0] 15 + track := global.GetTrack(id) 16 + if track == nil { 17 + http.NotFound(w, r) 18 + return 19 + } 20 + 21 + err := pages["track"].Execute(w, track) 22 + if err != nil { 23 + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) 24 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 25 + } 26 + }) 27 + } 28 +
+14 -2
admin/views/edit-release.html
··· 1 1 {{define "head"}} 2 - <title>editing {{.Title}} - ari melody 💫</title> 2 + <title>Editing {{.Title}} - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="{{.GetArtwork}}" type="image/x-icon"> 4 4 5 5 <link rel="stylesheet" href="/admin/static/edit-release.css"> ··· 129 129 {{end}} 130 130 </div> 131 131 132 - <div class="card-title"> 132 + <div class="card-title" id="tracks"> 133 133 <h2>Tracklist ({{len .Tracks}})</h2> 134 134 <a class="button edit" 135 135 href="/admin/release/{{.ID}}/edittracks" ··· 161 161 {{end}} 162 162 </div> 163 163 {{end}} 164 + </div> 165 + 166 + <div class="card-title"> 167 + <h2>Danger Zone</h2> 168 + </div> 169 + <div class="card danger"> 170 + <p> 171 + Clicking the button below will delete this release. 172 + This action is <strong>irreversible</strong>. 173 + You will be prompted to confirm this decision. 174 + </p> 175 + <button class="delete" id="delete">Delete Release</button> 164 176 </div> 165 177 166 178 </main>
+68
admin/views/edit-track.html
··· 1 + {{define "head"}} 2 + <title>Editing Track - ari melody 💫</title> 3 + 4 + <link rel="stylesheet" href="/admin/static/edit-track.css"> 5 + {{end}} 6 + 7 + {{define "content"}} 8 + <main> 9 + <h1>Editing Track "{{.Title}}"</h1> 10 + 11 + <div id="track" data-id="{{.ID}}"> 12 + <div class="track-info"> 13 + <p class="track-title-header">Title</p> 14 + <h2 class="track-title"> 15 + <input type="text" id="title" name="Title" value="{{.Title}}"> 16 + </h2> 17 + 18 + <h2>Description</h2> 19 + <textarea 20 + name="Description" 21 + value="{{.Description}}" 22 + placeholder="No description provided." 23 + rows="5" 24 + id="description" 25 + >{{.Description}}</textarea> 26 + 27 + <h2>Lyrics</h2> 28 + <textarea 29 + name="Lyrics" 30 + value="{{.Lyrics}}" 31 + placeholder="There are no lyrics." 32 + rows="5" 33 + id="lyrics" 34 + >{{.Lyrics}}</textarea> 35 + 36 + <div class="track-actions"> 37 + <button type="submit" class="save" id="save" disabled>Save</button> 38 + </div> 39 + </div> 40 + </div> 41 + 42 + <div class="card-title"> 43 + <h2>Featured in</h2> 44 + </div> 45 + <div class="card releases"> 46 + {{if .Release}} 47 + {{block "release" .Release}}{{end}} 48 + {{else}} 49 + <p>This track isn't bound to a release.</p> 50 + {{end}} 51 + </div> 52 + 53 + <div class="card-title"> 54 + <h2>Danger Zone</h2> 55 + </div> 56 + <div class="card danger"> 57 + <p> 58 + Clicking the button below will delete this track. 59 + This action is <strong>irreversible</strong>. 60 + You will be prompted to confirm this decision. 61 + </p> 62 + <button class="delete" id="delete">Delete Track</button> 63 + </div> 64 + 65 + </main> 66 + 67 + <script type="module" src="/admin/static/edit-track.js" defer></script> 68 + {{end}}
+4 -25
admin/views/index.html
··· 1 1 {{define "head"}} 2 - <title>admin - ari melody 💫</title> 2 + <title>Admin - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 4 <link rel="stylesheet" href="/admin/static/index.css"> 5 5 {{end}} ··· 12 12 <a href="/admin/createrelease" class="create-btn" id="create-release">Create New</a> 13 13 </div> 14 14 <div class="card releases"> 15 - {{range $Release := .Releases}} 16 - <div class="release"> 17 - <div class="release-artwork"> 18 - <img src="{{$Release.Artwork}}" alt="" width="128" loading="lazy"> 19 - </div> 20 - <div class="release-info"> 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 - <p class="release-artists">{{$Release.PrintArtists true true}}</p> 29 - <p class="release-type-single">{{$Release.ReleaseType}} 30 - ({{len $Release.Tracks}} track{{if not (eq (len $Release.Tracks) 1)}}s{{end}})</p> 31 - <div class="release-actions"> 32 - <a href="/admin/release/{{$Release.ID}}">Edit</a> 33 - <a href="/music/{{$Release.ID}}" target="_blank">Gateway</a> 34 - </div> 35 - </div> 36 - </div> 15 + {{range .Releases}} 16 + {{block "release" .}}{{end}} 37 17 {{end}} 38 18 {{if not .Releases}} 39 19 <p>There are no releases.</p> ··· 66 46 {{range $Track := .Tracks}} 67 47 <div class="track"> 68 48 <h2 class="track-title"> 69 - {{$Track.Title}} 49 + <a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a> 70 50 {{if $Track.Release}} 71 51 <small class="track-album">{{$Track.Release.Title}}</small> 72 52 {{else}} 73 53 <small class="track-album empty">(no release)</small> 74 54 {{end}} 75 55 </h2> 76 - <p class="track-id">{{$Track.ID}}</p> 77 56 {{if $Track.Description}} 78 57 <p class="track-description">{{$Track.Description}}</p> 79 58 {{else}}
+1 -2
admin/views/layout.html
··· 23 23 </nav> 24 24 </header> 25 25 26 - {{block "content" .}} 27 - {{end}} 26 + {{block "content" .}}{{end}} 28 27 29 28 {{template "prideflag"}} 30 29 </body>
+5 -8
admin/views/login.html
··· 1 1 {{define "head"}} 2 - <title>login - ari melody 💫</title> 2 + <title>Login - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 4 5 5 <style> ··· 15 15 16 16 {{define "content"}} 17 17 <main> 18 - 19 18 {{if .Token}} 19 + 20 20 <meta http-equiv="refresh" content="5;url=/admin/" /> 21 - <meta name="token" content="{{.Token}}" /> 22 21 <p> 23 22 Logged in successfully. 24 23 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 24 </p> 25 + 30 26 {{else}} 27 + 31 28 <p>Log in with <a href="{{.DiscordURI}}" class="discord">Discord</a>.</p> 29 + 32 30 {{end}} 33 - 34 31 </main> 35 32 {{end}}
+1 -1
admin/views/logout.html
··· 1 1 {{define "head"}} 2 - <title>admin - ari melody 💫</title> 2 + <title>Admin - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 4 5 5 <style>
+2 -7
api/release.go
··· 262 262 update.Artwork = *data.Artwork 263 263 } 264 264 } 265 - if data.Buyname != nil { 266 - if *data.Buyname == "" { 267 - http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest) 268 - return 269 - } 270 - update.Buyname = *data.Buyname 271 - } 265 + 266 + if data.Buyname != nil { update.Buyname = *data.Buyname } 272 267 if data.Buylink != nil { update.Buylink = *data.Buylink } 273 268 274 269 err = controller.UpdateReleaseDB(global.DB, &update)
+3 -4
global/data.go
··· 35 35 return args 36 36 }() 37 37 38 - 39 38 var HTTP_DOMAIN = func() string { 40 - envvar := os.Getenv("HTTP_DOMAIN") 41 - if envvar != "" { 42 - return envvar 39 + domain := Args["httpDomain"] 40 + if domain != "" { 41 + return domain 43 42 } 44 43 return "https://arimelody.me" 45 44 }()
+12 -3
views/music-gateway.html
··· 80 80 </ul> 81 81 {{end}} 82 82 83 - {{if .Description}} 84 - <p id="description"> 85 - {{.Description}} 83 + {{if .IsSingle}} 84 + 85 + {{$Track := index .Tracks 0}} 86 + {{if $Track.Description}} 87 + <p id="description">{{$Track.Description}}</p> 88 + {{end}} 89 + 90 + {{else}} 91 + 92 + {{if .Description}}<p id="description">{{.Description}} 86 93 </p> 94 + {{end}} 95 + 87 96 {{end}} 88 97 89 98 <button id="share">share</button>