From 604e2a4a7c7bcfc1574bf4d3c2ec5a1d9b14258d Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 31 Aug 2024 01:30:30 +0100 Subject: [PATCH] full release edit capabilities oh my goodness gracious Signed-off-by: ari melody --- admin/components/credits/addcredit.html | 3 +- admin/components/credits/editcredits.html | 60 +-- admin/components/credits/newcredit.html | 2 +- admin/components/links/addlink.html | 0 admin/components/links/editlinks.html | 159 +++++++ admin/components/links/newlink.html | 0 admin/components/tracks/addtrack.html | 47 ++ admin/components/tracks/edittracks.html | 112 +++++ admin/components/tracks/newtrack.html | 9 + admin/releasehttp.go | 99 +++- admin/static/admin.css | 2 +- admin/static/admin.js | 68 +++ .../static/{release.css => edit-release.css} | 442 +++++++++++++----- admin/static/edit-release.js | 38 +- admin/static/index.js | 24 + admin/views/edit-release.html | 27 +- admin/views/index.html | 5 +- api/release.go | 130 +++++- music/controller/release.go | 2 +- music/view/release.go | 6 +- public/img/list-grabber.svg | 6 + res/list-grabber.afdesign | Bin 0 -> 15222 bytes res/list-grabber.afdesign~lock~ | Bin 0 -> 96 bytes views/music-gateway.html | 2 + views/music.html | 2 + 25 files changed, 1043 insertions(+), 202 deletions(-) delete mode 100644 admin/components/links/addlink.html delete mode 100644 admin/components/links/newlink.html rename admin/static/{release.css => edit-release.css} (50%) create mode 100644 admin/static/index.js create mode 100644 public/img/list-grabber.svg create mode 100644 res/list-grabber.afdesign create mode 100644 res/list-grabber.afdesign~lock~ diff --git a/admin/components/credits/addcredit.html b/admin/components/credits/addcredit.html index ff92595..32c43fa 100644 --- a/admin/components/credits/addcredit.html +++ b/admin/components/credits/addcredit.html @@ -12,8 +12,7 @@ hx-swap="beforeend" > - {{$Artist.Name}} - ({{$Artist.ID}}) + {{$Artist.Name}} ({{$Artist.ID}}) {{end}} diff --git a/admin/components/credits/editcredits.html b/admin/components/credits/editcredits.html index 7c51e5b..1ab9cb2 100644 --- a/admin/components/credits/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -27,7 +27,7 @@ - + Delete {{end}} @@ -40,6 +40,8 @@ + diff --git a/admin/components/links/newlink.html b/admin/components/links/newlink.html deleted file mode 100644 index e69de29..0000000 diff --git a/admin/components/tracks/addtrack.html b/admin/components/tracks/addtrack.html index e69de29..f402763 100644 --- a/admin/components/tracks/addtrack.html +++ b/admin/components/tracks/addtrack.html @@ -0,0 +1,47 @@ + +
+

Add track

+
+ +
    + {{range $Track := .Tracks}} + +
  • + {{.Title}} +
  • + {{end}} +
+ + {{if not .Tracks}} +

There are no more tracks to add.

+ {{end}} + +
+ +
+ + +
diff --git a/admin/components/tracks/edittracks.html b/admin/components/tracks/edittracks.html index e69de29..dba57a6 100644 --- a/admin/components/tracks/edittracks.html +++ b/admin/components/tracks/edittracks.html @@ -0,0 +1,112 @@ + +
+

Editing: Tracks

+ Add +
+ +
+
    + {{range .Tracks}} +
  • +
    +

    + {{.Number}} + {{.Title}} +

    + Delete +
    +
  • + {{end}} +
+ +
+ + +
+
+ + +
diff --git a/admin/components/tracks/newtrack.html b/admin/components/tracks/newtrack.html index e69de29..3e76959 100644 --- a/admin/components/tracks/newtrack.html +++ b/admin/components/tracks/newtrack.html @@ -0,0 +1,9 @@ +
  • +
    +

    + {{.Number}} + {{.Title}} +

    + Delete +
    +
  • diff --git a/admin/releasehttp.go b/admin/releasehttp.go index c16bbec..8ca55b2 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -45,6 +45,18 @@ func serveRelease() http.Handler { case "newcredit": serveNewCredit().ServeHTTP(w, r) return + case "editlinks": + serveEditLinks(release).ServeHTTP(w, r) + return + case "edittracks": + serveEditTracks(release).ServeHTTP(w, r) + return + case "addtrack": + serveAddTrack(release).ServeHTTP(w, r) + return + case "newtrack": + serveNewTrack(release).ServeHTTP(w, r) + return } http.NotFound(w, r) return @@ -52,11 +64,11 @@ func serveRelease() http.Handler { tracks := []gatewayTrack{} for i, track := range release.Tracks { - tracks = append([]gatewayTrack{{ + tracks = append(tracks, gatewayTrack{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), - Number: len(release.Tracks) - i, - }}, tracks...) + Number: i + 1, + }) } lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} @@ -121,3 +133,84 @@ func serveNewCredit() http.Handler { return }) } + +func serveEditLinks(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + serveComponent(path.Join("links", "editlinks.html"), release).ServeHTTP(w, r) + return + }) +} + +func serveEditTracks(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + type Track struct { + *model.Track + Number int + } + type Release struct { + *model.Release + Tracks []Track + } + var data = Release{ release, []Track{} } + for i, track := range release.Tracks { + data.Tracks = append(data.Tracks, Track{track, i + 1}) + } + + serveComponent(path.Join("tracks", "edittracks.html"), data).ServeHTTP(w, r) + return + }) +} + +func serveAddTrack(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var tracks = []*model.Track{} + for _, track := range global.Tracks { + var exists = false + for _, t := range release.Tracks { + if t == track { + exists = true + break + } + } + if !exists { + tracks = append(tracks, track) + } + } + + type response struct { + ReleaseID string; + Tracks []*model.Track + } + + w.Header().Set("Content-Type", "text/html") + serveComponent(path.Join("tracks", "addtrack.html"), response{ + ReleaseID: release.ID, + Tracks: tracks, + }).ServeHTTP(w, r) + return + }) +} + +func serveNewTrack(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + track := global.GetTrack(strings.Split(r.URL.Path, "/")[3]) + if track == nil { + http.NotFound(w, r) + return + } + + type Track struct { + *model.Track + Number int + } + + w.Header().Set("Content-Type", "text/html") + serveComponent(path.Join("tracks", "newtrack.html"), Track{ + track, + len(release.Tracks) + 1, + }).ServeHTTP(w, r) + return + }) +} diff --git a/admin/static/admin.css b/admin/static/admin.css index 2d6e306..9073d27 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -86,7 +86,7 @@ a img { } .card { - margin-bottom: 2em; + margin-bottom: 1em; } .card h2 { diff --git a/admin/static/admin.js b/admin/static/admin.js index e69de29..77fc383 100644 --- a/admin/static/admin.js +++ b/admin/static/admin.js @@ -0,0 +1,68 @@ +/** + * Creates a "magic" reorderable list from `container`. + * This function is absolute magic and I love it + * + * Example: + * ```html + * + * ``` + * ```js + * // javascript + * makeMagicList(document.getElementById("list"), "li"); + * ``` + * + * @param {HTMLElement} container The parent container to use as a list. + * @param {string} itemSelector The selector name of list item elements. + * @param {Function} callback A function to call after each reordering. + */ +export function makeMagicList(container, itemSelector, callback) { + if (!container) + throw new Error("container not provided"); + if (!itemSelector) + throw new Error("itemSelector not provided"); + + container.querySelectorAll(itemSelector).forEach(item => { + item.draggable = true; + item.addEventListener("dragstart", () => { item.classList.add("moving") }); + item.addEventListener("dragend", () => { item.classList.remove("moving") }); + item.querySelectorAll("input").forEach(el => { + el.addEventListener("mousedown", () => { item.draggable = false }); + el.addEventListener("mouseup", () => { item.draggable = true }); + el.addEventListener("dragstart", e => { e.stopPropagation() }); + }); + }); + + var lastCursorY; + container.addEventListener("dragover", event => { + const dragging = container.querySelector(itemSelector + ".moving"); + if (!dragging) return; + + let cursorY = event.touches ? event.touches[0].clientY : event.clientY; + + // don't bother processing if we haven't moved + if (lastCursorY === cursorY) return + lastCursorY = cursorY; + + // get the element positioned ahead of the cursor + const notMoving = [...container.querySelectorAll(itemSelector + ":not(.moving)")]; + const afterElement = notMoving.reduce((previous, current) => { + const box = current.getBoundingClientRect(); + const offset = cursorY - box.top - box.height / 2; + if (offset < 0 && offset > previous.offset) + return { offset: offset, element: current }; + return previous; + }, { offset: Number.NEGATIVE_INFINITY }).element; + + if (afterElement) { + container.insertBefore(dragging, afterElement); + } else { + container.appendChild(dragging); + } + + if (callback) callback(); + }); +} diff --git a/admin/static/release.css b/admin/static/edit-release.css similarity index 50% rename from admin/static/release.css rename to admin/static/edit-release.css index 17dabdf..30c96cb 100644 --- a/admin/static/release.css +++ b/admin/static/edit-release.css @@ -1,3 +1,9 @@ +input[type="text"] { + font-size: inherit; + font-family: inherit; + color: inherit; +} + #release { margin-bottom: 1em; padding: 1.5em; @@ -12,10 +18,6 @@ .release-artwork { width: 200px; - - display: flex; - justify-content: center; - align-items: start; } .release-artwork img { @@ -28,6 +30,7 @@ } .release-info { + width: 0; margin: 0; flex-grow: 1; display: flex; @@ -38,6 +41,28 @@ margin: 0; } +#title { + width: 100%; + margin: 0 -.2em; + padding: 0 .2em; + font-weight: bold; + border-radius: 4px; + border: 1px solid transparent; + background: transparent; + outline: none; +} + +#title:hover { + background: #ffffff; + border-color: #80808080; +} + +#title:active, +#title:focus { + background: #ffffff; + border-color: #808080; +} + .release-title small { opacity: .75; } @@ -71,6 +96,7 @@ border: none; background: none; outline: none; + resize: vertical; } .release-info table td:has(select), .release-info table td:has(input), @@ -126,6 +152,10 @@ button[disabled] { cursor: not-allowed !important; } +a.delete { + color: #d22828; +} + .release-actions { margin-top: auto; display: flex; @@ -134,90 +164,6 @@ button[disabled] { justify-content: right; } -.card.credits .credit { - margin-bottom: .5em; - padding: .5em; - display: flex; - flex-direction: row; - align-items: center; - gap: 1em; - - border-radius: .5em; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.card.credits .credit .artist-avatar { - border-radius: .5em; -} - -.card.credits .credit .artist-name { - font-weight: bold; -} - -.card.credits .credit .artist-role small { - font-size: inherit; - opacity: .66; -} - -.track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: .5em; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.card h2.track-title { - margin: 0; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.card-title a.button { - text-decoration: none; -} - -.track-id { - width: fit-content; - font-family: "Monaspace Argon", monospace; - font-size: .8em; - font-style: italic; - line-height: 1em; - user-select: all; - -webkit-user-select: all; -} - -.track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -.track-album.empty { - color: #ff2020; - opacity: 1; -} - -.track-description { - font-style: italic; -} - -.track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -.track .empty { - opacity: 0.75; -} - dialog { width: min(720px, calc(100% - 2em)); padding: 2em; @@ -245,13 +191,15 @@ dialog div.dialog-actions { gap: .5em; } -dialog#editcredits ul { - margin: 0; - padding: 0; - list-style: none; +.card-title a.button { + text-decoration: none; } -dialog#editcredits .credit>div { +/* + * RELEASE CREDITS + */ + +.card.credits .credit { margin-bottom: .5em; padding: .5em; display: flex; @@ -264,24 +212,70 @@ dialog#editcredits .credit>div { border: 1px solid #808080; } -dialog#editcredits .credit p { - margin: 0; -} - -dialog#editcredits .credit .artist-avatar { +.card.credits .credit .artist-avatar { border-radius: .5em; } -dialog#editcredits .credit .credit-info { +.card.credits .credit .artist-name { + font-weight: bold; +} + +.card.credits .credit .artist-role small { + font-size: inherit; + opacity: .66; +} + +#editcredits ul { + margin: 0; + padding: 0; + list-style: none; +} + +#editcredits .credit>div { + margin-bottom: .5em; + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: 1em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +#editcredits .credit { + transition: transform .2s ease-out, opacity .2s; +} + +#editcredits .credit.moving { + transform: scale(1.05); + opacity: .5; +} + +#editcredits .credit p { + margin: 0; +} + +#editcredits .credit .artist-avatar { + border-radius: .5em; +} + +#editcredits .credit .credit-info { width: 100%; } -dialog#editcredits .credit .credit-info .credit-attribute { +#editcredits .credit .credit-info .credit-attribute { width: 100%; display: flex; } -dialog#editcredits .credit .credit-info .credit-attribute input[type="text"] { +#editcredits .credit .credit-info .credit-attribute label { + display: flex; + align-items: center; +} + +#editcredits .credit .credit-info .credit-attribute input[type="text"] { margin-left: .25em; padding: .2em .4em; flex-grow: 1; @@ -291,15 +285,255 @@ dialog#editcredits .credit .credit-info .credit-attribute input[type="text"] { color: inherit; } -dialog#editcredits .credit .artist-name { +#editcredits .credit .artist-name { font-weight: bold; } -dialog#editcredits .credit .artist-role small { +#editcredits .credit .artist-role small { font-size: inherit; opacity: .66; } -dialog#editcredits .credit button.delete { +#editcredits .credit button.delete { margin-left: auto; } + +#addcredit ul { + padding: 0; + list-style: none; + background: #f8f8f8; +} + +#addcredit ul li.new-artist { + padding: .5em; + display: flex; + gap: .5em; + cursor: pointer; +} + +#addcredit ul li.new-artist:nth-child(even) { + background: #f0f0f0; +} + +#addcredit ul li.new-artist:hover { + background: #e0e0e0; +} + +#addcredit .new-artist .artist-id { + opacity: .5; +} + +/* + * RELEASE LINKS + */ + +.card.links { + display: flex; + gap: .2em; +} + +.card.links a.button[data-name="spotify"] { + background-color: #8cff83 +} + +.card.links a.button[data-name="applemusic"] { + background-color: #8cd9ff +} + +.card.links a.button[data-name="soundcloud"] { + background-color: #fdaa6d +} + +.card.links a.button[data-name="youtube"] { + background-color: #ff6e6e +} + +#editlinks table { + width: 100%; +} + +#editlinks tr { + display: flex; +} + +#editlinks th { + padding: 0 .1em; + display: flex; + align-items: center; + text-align: left; +} + +#editlinks tr:nth-child(odd) { + background: #f8f8f8; +} + +#editlinks tr th, +#editlinks tr td { + height: 2em; +} + +#editlinks tr td { + padding: 0; +} + +#editlinks tr.link { + transition: transform .2s ease-out, opacity .2s; +} + +#editlinks tr.link.moving { + transform: scale(1.05); + opacity: .5; +} + +#editlinks tr .grabber { + width: 2em; + display: flex; + justify-content: center; + cursor: pointer; +} +#editlinks tr .grabber img { + width: 1em; + pointer-events: none; +} +#editlinks tr .link-name { + width: 8em; +} +#editlinks tr .link-url { + flex-grow: 1; +} + +#editlinks td a.delete { + display: flex; + height: 100%; + align-items: center; + padding: 0 .5em; +} + +#editlinks td input[type="text"] { + width: calc(100% - .6em); + height: 100%; + padding: 0 .3em; + border: none; + outline: none; + cursor: pointer; + background: none; +} +#editlinks td input[type="text"]:hover { + background: #0001; +} +#editlinks td input[type="text"]:focus { + outline: 1px solid #808080; +} + +/* + * RELEASE TRACKS + */ + +.card.tracks .track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.card.tracks h2.track-title { + margin: 0; + display: flex; + gap: .5em; +} + +.card.tracks h2.track-title .track-number { + opacity: .5; +} + +.card.tracks .track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.card.tracks .track-album.empty { + color: #ff2020; + opacity: 1; +} + +.card.tracks .track-description { + font-style: italic; +} + +.card.tracks .track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.card.tracks .track .empty { + opacity: 0.75; +} + +#edittracks ul { + padding: 0; + list-style: none; +} + +#edittracks .track { + transition: transform .2s ease-out, opacity .2s; +} + +#edittracks .track.moving { + transform: scale(1.05); + opacity: .5; +} + +#edittracks .track div { + padding: .5em; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + cursor: pointer; +} + +#edittracks .track div:active { + cursor: move; +} + +#edittracks .track:nth-child(even) { + background: #f0f0f0; +} + +#edittracks .track-number { + min-width: 1em; + display: inline-block; + opacity: .5; +} + +#edittracks .track-name { + margin: 0; +} + +#addtrack ul { + padding: 0; + list-style: none; + background: #f8f8f8; +} + +#addtrack ul li.new-track { + padding: .5em; + display: flex; + gap: .5em; + cursor: pointer; +} + +#addtrack ul li.new-track:nth-child(even) { + background: #f0f0f0; +} + +#addtrack ul li.new-track:hover { + background: #e0e0e0; +} diff --git a/admin/static/edit-release.js b/admin/static/edit-release.js index 7be9069..1e9fa29 100644 --- a/admin/static/edit-release.js +++ b/admin/static/edit-release.js @@ -1,7 +1,9 @@ import Stateful from "/script/silver.min.js" const releaseID = document.getElementById("release").dataset.id; -const artwork_input = document.getElementById("artwork"); +const title_input = document.getElementById("title"); +const artwork_img = document.getElementById("artwork"); +const artwork_input = document.getElementById("artwork-file"); const type_input = document.getElementById("type"); const desc_input = document.getElementById("description"); const date_input = document.getElementById("release-date"); @@ -10,20 +12,22 @@ const buylink_input = document.getElementById("buylink"); const vis_input = document.getElementById("visibility"); const save_btn = document.getElementById("save"); -let token = atob(localStorage.getItem("arime-token")); +var artwork_data = artwork_img.attributes.src.value; -let edited = new Stateful(false); +var token = atob(localStorage.getItem("arime-token")); -let release_data = update_data(undefined); +var edited = new Stateful(false); + +var release_data = update_data(undefined); function update_data(old) { - let release_data = { + var release_data = { visible: vis_input.value === "true", - title: undefined, + title: title_input.value, description: desc_input.value, type: type_input.value, releaseDate: date_input.value, - artwork: artwork_input.attributes.src.value, + artwork: artwork_data, buyname: buyname_input.value, buylink: buylink_input.value, }; @@ -38,8 +42,6 @@ function update_data(old) { function save_release() { console.table(release_data); - edited.set(false); - (async () => { const res = await fetch( "/api/v1/music/" + releaseID, { @@ -61,15 +63,29 @@ function save_release() { location = location; })(); } -window.save_release = save_release; edited.onUpdate(edited => { save_btn.disabled = !edited; }) -artwork_input.addEventListener("click", () => { +title_input.addEventListener("change", () => { release_data = update_data(release_data); }); +artwork_img.addEventListener("click", () => { + artwork_input.addEventListener("change", () => { + if (artwork_input.files.length > 0) { + const reader = new FileReader(); + reader.onload = e => { + const data = e.target.result; + artwork_img.src = data; + artwork_data = data; + release_data = update_data(release_data); + }; + reader.readAsDataURL(artwork_input.files[0]); + } + }); + artwork_input.click(); +}); type_input.addEventListener("change", () => { release_data = update_data(release_data); }); diff --git a/admin/static/index.js b/admin/static/index.js new file mode 100644 index 0000000..709f902 --- /dev/null +++ b/admin/static/index.js @@ -0,0 +1,24 @@ +const newReleaseBtn = document.getElementById("create-release"); + +newReleaseBtn.addEventListener("click", event => { + event.preventDefault(); + const id = prompt("Enter an ID for this release:"); + if (id == null || id == "") return; + + fetch("/api/v1/music", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({id}) + }).then(res => { + if (res.ok) location = "/admin/release/" + id; + else { + res.text().then(err => { + alert("Request failed: " + err); + console.error(err); + }); + } + }).catch(err => { + alert("Failed to create release. Check the console for details."); + console.error(err); + }); +}); diff --git a/admin/views/edit-release.html b/admin/views/edit-release.html index 375bbd2..d33c8e6 100644 --- a/admin/views/edit-release.html +++ b/admin/views/edit-release.html @@ -2,7 +2,7 @@ editing {{.Title}} - ari melody 💫 - + {{end}} {{define "content"}} @@ -11,18 +11,13 @@
    +

    - - {{.Title}} - {{.GetReleaseYear}} +

    - - - - @@ -130,9 +125,7 @@ @@ -148,13 +141,19 @@
    {{range .Tracks}}
    -

    {{.Number}}. {{.Title}}

    -

    {{.ID}}

    +

    + {{.Number}} + {{.Title}} +

    + +

    Description

    {{if .Description}}

    {{.Description}}

    {{else}}

    No description provided.

    {{end}} + +

    Lyrics

    {{if .Lyrics}}

    {{.Lyrics}}

    {{else}} diff --git a/admin/views/index.html b/admin/views/index.html index 8847492..36dff67 100644 --- a/admin/views/index.html +++ b/admin/views/index.html @@ -9,7 +9,7 @@

    Releases

    - Create New + Create New
    {{range $Release := .Releases}} @@ -93,5 +93,6 @@ - + + {{end}} diff --git a/api/release.go b/api/release.go index 6d02733..2064fb0 100644 --- a/api/release.go +++ b/api/release.go @@ -1,9 +1,14 @@ package api import ( + "bufio" + "encoding/base64" "encoding/json" "fmt" + "io/fs" "net/http" + "os" + "path/filepath" "strings" "time" @@ -85,40 +90,53 @@ func CreateRelease() http.Handler { http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) return } - if *data.Title == "" { - http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest) - return + + title := data.ID + if data.Title != nil && *data.Title != "" { + title = *data.Title } - if data.Buyname == nil || *data.Buyname == "" { *data.Buyname = "buy" } - if data.Buylink == nil || *data.Buylink == "" { *data.Buylink = "https://arimelody.me" } + + description := "" + if data.Description != nil && *data.Description != "" { description = *data.Description } + + releaseType := model.Single + if data.ReleaseType != nil && *data.ReleaseType != "" { releaseType = *data.ReleaseType } + + releaseDate := time.Time{} + if data.ReleaseDate != nil && *data.ReleaseDate != "" { + releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) + if err != nil { + http.Error(w, "Invalid release date", http.StatusBadRequest) + return + } + } else { + releaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) + } + + artwork := "/img/default-cover-art.png" + if data.Artwork != nil && *data.Artwork != "" { artwork = *data.Artwork } + + buyname := "" + if data.Buyname != nil && *data.Buyname != "" { buyname = *data.Buyname } + + buylink := "" + if data.Buylink != nil && *data.Buylink != "" { buylink = *data.Buylink } if global.GetRelease(data.ID) != nil { http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) return } - releaseDate := time.Time{} - if *data.ReleaseDate == "" { - http.Error(w, "Release date cannot be empty\n", http.StatusBadRequest) - return - } else if data.ReleaseDate != nil { - releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) - if err != nil { - http.Error(w, "Invalid release date", http.StatusBadRequest) - return - } - } - var release = model.Release{ ID: data.ID, - Visible: *data.Visible, - Title: *data.Title, - Description: *data.Description, - ReleaseType: *data.ReleaseType, + Visible: false, + Title: title, + Description: description, + ReleaseType: releaseType, ReleaseDate: releaseDate, - Artwork: *data.Artwork, - Buyname: *data.Buyname, - Buylink: *data.Buylink, + Artwork: artwork, + Buyname: buyname, + Buylink: buylink, Links: []*model.Link{}, Credits: []*model.Credit{}, Tracks: []*model.Track{}, @@ -181,7 +199,69 @@ func UpdateRelease() http.Handler { } update.ReleaseDate = newDate } - if data.Artwork != nil { update.Artwork = *data.Artwork } + if data.Artwork != nil { + if strings.Contains(*data.Artwork, ";base64,") { + split := strings.Split(*data.Artwork, ";base64,") + header := split[0] + imageData, err := base64.StdEncoding.DecodeString(split[1]) + ext, _ := strings.CutPrefix(header, "data:image/") + + switch ext { + case "png": + case "jpg": + case "jpeg": + default: + http.Error(w, "Invalid image type. Allowed: .png, .jpg, .jpeg", http.StatusBadRequest) + return + } + + artworkDirectory := filepath.Join("uploads", "musicart") + // ensure directory exists + os.MkdirAll(artworkDirectory, os.ModePerm) + + imagePath := filepath.Join(artworkDirectory, fmt.Sprintf("%s.%s", update.ID, ext)) + file, err := os.Create(imagePath) + if err != nil { + fmt.Printf("FATAL: Failed to create file %s: %s\n", imagePath, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + defer file.Close() + + buffer := bufio.NewWriter(file) + _, err = buffer.Write(imageData) + if err != nil { + fmt.Printf("FATAL: Failed to write to file %s: %s\n", imagePath, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if err := buffer.Flush(); err != nil { + fmt.Printf("FATAL: Failed to flush data to file %s: %s\n", imagePath, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + // clean up files with this ID and different extensions + err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { + if path == imagePath { return nil } + + withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) + if withoutExt != filepath.Join(artworkDirectory, update.ID) { return nil } + + return os.Remove(path) + }) + if err != nil { + fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) + } + + fmt.Printf("Artwork for %s updated.\n", update.ID) + update.Artwork = fmt.Sprintf("/uploads/musicart/%s.%s", update.ID, ext) + } else { + update.Artwork = *data.Artwork + } + } if data.Buyname != nil { if *data.Buyname == "" { http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest) diff --git a/music/controller/release.go b/music/controller/release.go index 9e70aa7..167f4b6 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -47,7 +47,7 @@ func PullReleaseTracksDB(db *sqlx.DB, release *model.Release) ([]*model.Track, e err := db.Select(&track_rows, "SELECT track FROM musicreleasetrack "+ "WHERE release=$1 "+ - "ORDER BY number DESC", + "ORDER BY number ASC", release.ID, ) if err != nil { diff --git a/music/view/release.go b/music/view/release.go index 634c481..6b9b181 100644 --- a/music/view/release.go +++ b/music/view/release.go @@ -86,11 +86,11 @@ func ServeGateway() http.Handler { tracks := []gatewayTrack{} for i, track := range release.Tracks { - tracks = append([]gatewayTrack{{ + tracks = append(tracks, gatewayTrack{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), - Number: len(release.Tracks) - i, - }}, tracks...) + Number: i + 1, + }) } lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} diff --git a/public/img/list-grabber.svg b/public/img/list-grabber.svg new file mode 100644 index 0000000..58eb280 --- /dev/null +++ b/public/img/list-grabber.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/list-grabber.afdesign b/res/list-grabber.afdesign new file mode 100644 index 0000000000000000000000000000000000000000..91841539c3709d0b0aef2f425393dcff99f4daca GIT binary patch literal 15222 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 z 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: {{.PrintReleaseDate}}

    {{end}} + {{if .IsReleased}} + {{end}} {{if .Description}}

    diff --git a/views/music.html b/views/music.html index 225a9a9..8e59903 100644 --- a/views/music.html +++ b/views/music.html @@ -38,6 +38,7 @@

    {{$Release.PrintArtists true true}}

    {{$Release.ReleaseType}}

    + {{if $Release.IsReleased}} + {{end}}
    {{end}}
    Artists{{.PrintArtists true true}}
    Type @@ -50,7 +45,7 @@ name="Description" value="{{.Description}}" placeholder="No description provided." - rows="3" + rows="1" id="description" >{{.Description}}