full release edit capabilities oh my goodness gracious
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
34cddcfdb2
commit
604e2a4a7c
|
@ -12,8 +12,7 @@
|
|||
hx-swap="beforeend"
|
||||
>
|
||||
<img src="{{$Artist.GetAvatar}}" alt="" width="16" loading="lazy" class="artist-avatar">
|
||||
<span class="artist-name">{{$Artist.Name}}</span>
|
||||
<span class="artist-id">({{$Artist.ID}})</span>
|
||||
<span class="artist-name">{{$Artist.Name}} <span class="artist-id">({{$Artist.ID}})</span></span>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<input type="checkbox" name="primary" {{if .Primary}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="delete">Delete</button>
|
||||
<a class="delete">Delete</a>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
|
@ -40,6 +40,8 @@
|
|||
</form>
|
||||
|
||||
<script type="module">
|
||||
import { makeMagicList } from "/admin/static/admin.js";
|
||||
|
||||
(() => {
|
||||
const container = document.getElementById("editcredits");
|
||||
const form = document.querySelector("#editcredits form");
|
||||
|
@ -47,39 +49,21 @@
|
|||
const addCreditBtn = document.getElementById("add-credit");
|
||||
const discardBtn = form.querySelector("button#discard");
|
||||
|
||||
function creditFromElement(el) {
|
||||
const artistID = el.dataset.artist;
|
||||
const roleInput = el.querySelector(`input[name="role"]`)
|
||||
const primaryInput = el.querySelector(`input[name="primary"]`)
|
||||
const deleteBtn = el.querySelector("button.delete");
|
||||
|
||||
let credit = {
|
||||
"artist": artistID,
|
||||
"role": roleInput.value,
|
||||
"primary": primaryInput.checked,
|
||||
};
|
||||
|
||||
roleInput.addEventListener("change", () => {
|
||||
credit.role = roleInput.value;
|
||||
});
|
||||
primaryInput.addEventListener("change", () => {
|
||||
credit.primary = primaryInput.checked;
|
||||
});
|
||||
deleteBtn.addEventListener("click", e => {
|
||||
if (!confirm("Are you sure you want to delete " + artistID + "'s credit?")) return;
|
||||
el.remove();
|
||||
credits = credits.filter(credit => credit.artist != artistID);
|
||||
});
|
||||
|
||||
return credit;
|
||||
}
|
||||
|
||||
let credits = [...form.querySelectorAll(".credit")].map(el => creditFromElement(el));
|
||||
makeMagicList(creditList, ".credit");
|
||||
|
||||
creditList.addEventListener("htmx:afterSwap", e => {
|
||||
const el = creditList.children[creditList.children.length - 1];
|
||||
const credit = creditFromElement(el);
|
||||
credits.push(credit);
|
||||
|
||||
const artistID = el.dataset.artist;
|
||||
const deleteBtn = el.querySelector("a.delete");
|
||||
|
||||
deleteBtn.addEventListener("click", e => {
|
||||
if (!confirm("Are you sure you want to delete " + artistID + "'s credit?")) return;
|
||||
el.remove();
|
||||
});
|
||||
|
||||
el.addEventListener("dragstart", () => { el.classList.add("moving") });
|
||||
el.addEventListener("dragend", () => { el.classList.remove("moving") });
|
||||
});
|
||||
|
||||
container.showModal();
|
||||
|
@ -89,12 +73,18 @@
|
|||
});
|
||||
|
||||
form.addEventListener("submit", e => {
|
||||
const credits = [...creditList.querySelectorAll(".credit")].map(el => {
|
||||
return {
|
||||
"artist": el.dataset.artist,
|
||||
"role": el.querySelector(`input[name="role"]`).value,
|
||||
"primary": el.querySelector(`input[name="primary"]`).checked,
|
||||
};
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
fetch(form.action, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(credits)
|
||||
}).then(res => {
|
||||
if (res.ok) location = location;
|
||||
|
@ -105,7 +95,7 @@
|
|||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
alert("Failed to update credits. Check the console for details");
|
||||
alert("Failed to update credits. Check the console for details.");
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
<input type="checkbox" name="primary">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="delete">Delete</button>
|
||||
<a class="delete">Delete</a>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
<dialog id="editlinks">
|
||||
<header>
|
||||
<h2>Editing: Links</h2>
|
||||
<button id="add-link" class="button new">Add</button>
|
||||
</header>
|
||||
|
||||
<form action="/api/v1/music/{{.ID}}/links">
|
||||
<table>
|
||||
<tr>
|
||||
<th class="grabber"></th>
|
||||
<th class="link-name">Name</th>
|
||||
<th class="link-url">URL</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{{range .Links}}
|
||||
<tr class="link">
|
||||
<td class="grabber"><img src="/img/list-grabber.svg"/></td>
|
||||
<td class="link-name">
|
||||
<input type="text" name="name" value="{{.Name}}">
|
||||
</td>
|
||||
<td class="link-url">
|
||||
<input type="text" name="url" value="{{.URL}}">
|
||||
</td>
|
||||
<td>
|
||||
<a class="delete">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button id="discard" type="button">Discard</button>
|
||||
<button id="save" type="submit" class="save">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
import { makeMagicList } from "/admin/static/admin.js";
|
||||
(() => {
|
||||
const container = document.getElementById("editlinks");
|
||||
const form = document.querySelector("#editlinks form");
|
||||
const linkTable = form.querySelector("table tbody");
|
||||
const addLinkBtn = document.getElementById("add-link");
|
||||
const discardBtn = form.querySelector("button#discard");
|
||||
|
||||
makeMagicList(linkTable, "tr.link");
|
||||
|
||||
function rigLinkItem(el) {
|
||||
const nameInput = el.querySelector(`input[name="name"]`)
|
||||
const deleteBtn = el.querySelector("a.delete");
|
||||
|
||||
deleteBtn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
if (nameInput.value != "" &&
|
||||
!confirm("Are you sure you want to delete \"" + nameInput.value + "\"?"))
|
||||
return;
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
[...linkTable.querySelectorAll("tr.link")].map(rigLinkItem);
|
||||
|
||||
addLinkBtn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
const row = document.createElement("tr");
|
||||
row.className = "link";
|
||||
|
||||
const grabberCell = document.createElement("td");
|
||||
grabberCell.className = "grabber";
|
||||
const grabberImg = document.createElement("img");
|
||||
grabberImg.src = "/img/list-grabber.svg";
|
||||
grabberCell.appendChild(grabberImg);
|
||||
row.appendChild(grabberCell);
|
||||
|
||||
const nameCell = document.createElement("td");
|
||||
nameCell.className = "link-name";
|
||||
const nameInput = document.createElement("input");
|
||||
nameInput.type = "text";
|
||||
nameInput.name = "name";
|
||||
nameCell.appendChild(nameInput);
|
||||
row.appendChild(nameCell);
|
||||
|
||||
const urlCell = document.createElement("td");
|
||||
urlCell.className = "link-url";
|
||||
const urlInput = document.createElement("input");
|
||||
urlInput.type = "text";
|
||||
urlInput.name = "url";
|
||||
urlCell.appendChild(urlInput);
|
||||
row.appendChild(urlCell);
|
||||
|
||||
const deleteCell = document.createElement("td");
|
||||
const deleteBtn = document.createElement("a");
|
||||
deleteBtn.className = "delete";
|
||||
deleteBtn.innerText = "Delete";
|
||||
deleteCell.appendChild(deleteBtn);
|
||||
row.appendChild(deleteCell);
|
||||
|
||||
linkTable.appendChild(row);
|
||||
|
||||
row.draggable = true;
|
||||
row.addEventListener("dragstart", () => { row.classList.add("moving") });
|
||||
row.addEventListener("dragend", () => { row.classList.remove("moving") });
|
||||
row.querySelectorAll("input").forEach(el => {
|
||||
el.addEventListener("mousedown", () => { row.draggable = false });
|
||||
el.addEventListener("mouseup", () => { row.draggable = true });
|
||||
el.addEventListener("dragstart", e => { e.stopPropagation() });
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
if (nameInput.value != "" && !confirm("Are you sure you want to delete \"" + nameInput.value + "\"?")) return;
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
|
||||
container.showModal();
|
||||
|
||||
container.addEventListener("close", () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", e => {
|
||||
var links = [];
|
||||
[...linkTable.querySelectorAll("tr.link")].map(el => {
|
||||
const name = el.querySelector(`input[name="name"]`).value;
|
||||
const url = el.querySelector(`input[name="url"]`).value;
|
||||
if (name == "" || url == "") return;
|
||||
links.push({
|
||||
"name": name,
|
||||
"url": url,
|
||||
});
|
||||
})
|
||||
|
||||
e.preventDefault();
|
||||
fetch(form.action, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(links)
|
||||
}).then(res => {
|
||||
if (res.ok) location = location;
|
||||
else {
|
||||
res.text().then(err => {
|
||||
alert(err);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
alert("Failed to update links. Check the console for details.");
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
discardBtn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
container.close();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</dialog>
|
|
@ -0,0 +1,47 @@
|
|||
<dialog id="addtrack">
|
||||
<header>
|
||||
<h2>Add track</h2>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
{{range $Track := .Tracks}}
|
||||
</li>
|
||||
<li class="new-track"
|
||||
data-id="{{$Track.ID}}"
|
||||
hx-get="/admin/release/{{$.ReleaseID}}/newtrack/{{$Track.ID}}"
|
||||
hx-target="#edittracks ul"
|
||||
hx-swap="beforeend"
|
||||
>
|
||||
{{.Title}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
{{if not .Tracks}}
|
||||
<p class="empty">There are no more tracks to add.</p>
|
||||
{{end}}
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button id="cancel" type="button">Cancel</button>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
(() => {
|
||||
const newTrackModal = document.getElementById("addtrack")
|
||||
const editTracksModal = document.getElementById("edittracks")
|
||||
const cancelBtn = newTrackModal.querySelector("#cancel");
|
||||
|
||||
editTracksModal.addEventListener("htmx:afterSwap", () => {
|
||||
newTrackModal.close();
|
||||
newTrackModal.remove();
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => {
|
||||
newTrackModal.close();
|
||||
newTrackModal.remove();
|
||||
});
|
||||
|
||||
newTrackModal.showModal();
|
||||
})();
|
||||
</script>
|
||||
</dialog>
|
|
@ -0,0 +1,112 @@
|
|||
<dialog id="edittracks">
|
||||
<header>
|
||||
<h2>Editing: Tracks</h2>
|
||||
<a id="add-track"
|
||||
class="button new"
|
||||
href="/admin/release/{{.ID}}/addtrack"
|
||||
hx-get="/admin/release/{{.ID}}/addtrack"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Add</a>
|
||||
</header>
|
||||
|
||||
<form action="/api/v1/music/{{.ID}}/tracks">
|
||||
<ul>
|
||||
{{range .Tracks}}
|
||||
<li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="{{.Number}}" draggable="true">
|
||||
<div>
|
||||
<p class="track-name">
|
||||
<span class="track-number">{{.Number}}</span>
|
||||
{{.Title}}
|
||||
</p>
|
||||
<a class="delete">Delete</a>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button id="discard" type="button">Discard</button>
|
||||
<button id="save" type="submit" class="save">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
import { makeMagicList } from "/admin/static/admin.js";
|
||||
(() => {
|
||||
const container = document.getElementById("edittracks");
|
||||
const form = document.querySelector("#edittracks form");
|
||||
const trackList = form.querySelector("ul");
|
||||
const addTrackBtn = document.getElementById("add-track");
|
||||
const discardBtn = form.querySelector("button#discard");
|
||||
|
||||
makeMagicList(trackList, ".track", refreshTrackNumbers);
|
||||
|
||||
function rigTrackItem(trackItem) {
|
||||
const trackID = trackItem.dataset.track;
|
||||
const trackTitle = trackItem.dataset.title;
|
||||
const deleteBtn = trackItem.querySelector("a.delete");
|
||||
|
||||
deleteBtn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
if (!confirm("Are you sure you want to remove " + trackTitle + "?")) return;
|
||||
trackItem.remove();
|
||||
refreshTrackNumbers();
|
||||
});
|
||||
}
|
||||
|
||||
function refreshTrackNumbers() {
|
||||
trackList.querySelectorAll("li").forEach((trackItem, i) => {
|
||||
trackItem.querySelector(".track-number").innerText = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
trackList.addEventListener("htmx:afterSwap", e => {
|
||||
const trackItem = trackList.children[trackList.children.length - 1];
|
||||
trackList.appendChild(trackItem);
|
||||
trackItem.addEventListener("dragstart", () => { trackItem.classList.add("moving") });
|
||||
trackItem.addEventListener("dragend", () => { trackItem.classList.remove("moving") });
|
||||
rigTrackItem(trackItem);
|
||||
refreshTrackNumbers();
|
||||
});
|
||||
|
||||
trackList.querySelectorAll("li").forEach(trackItem => {
|
||||
rigTrackItem(trackItem);
|
||||
});
|
||||
|
||||
container.showModal();
|
||||
|
||||
container.addEventListener("close", () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", e => {
|
||||
e.preventDefault();
|
||||
|
||||
let tracks = [...trackList.querySelectorAll(".track")].map(trackItem => trackItem.dataset.track);
|
||||
|
||||
fetch(form.action, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(tracks)
|
||||
}).then(res => {
|
||||
if (res.ok) location = location;
|
||||
else {
|
||||
res.text().then(err => {
|
||||
alert(err);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
alert("Failed to update tracks. Check the console for details.");
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
discardBtn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
container.close();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</dialog>
|
|
@ -0,0 +1,9 @@
|
|||
<li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="{{.Number}}" draggable="true">
|
||||
<div>
|
||||
<p class="track-name">
|
||||
<span class="track-number">{{.Number}}</span>
|
||||
{{.Title}}
|
||||
</p>
|
||||
<a class="delete">Delete</a>
|
||||
</div>
|
||||
</li>
|
|
@ -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", "<br>", -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ a img {
|
|||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Creates a "magic" reorderable list from `container`.
|
||||
* This function is absolute magic and I love it
|
||||
*
|
||||
* Example:
|
||||
* ```html
|
||||
* <ul id="list">
|
||||
* <li>Item 1</li>
|
||||
* <li>Item 2</li>
|
||||
* <li>Item 3</li>
|
||||
* </ul>
|
||||
* ```
|
||||
* ```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();
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
24
admin/static/index.js
Normal file
24
admin/static/index.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@
|
|||
<title>editing {{.Title}} - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="{{.GetArtwork}}" type="image/x-icon">
|
||||
|
||||
<link rel="stylesheet" href="/admin/static/release.css">
|
||||
<link rel="stylesheet" href="/admin/static/edit-release.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
|
@ -11,18 +11,13 @@
|
|||
<div id="release" data-id="{{.ID}}">
|
||||
<div class="release-artwork">
|
||||
<img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
||||
<input type="file" id="artwork-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||
</div>
|
||||
<div class="release-info">
|
||||
<h1 class="release-title">
|
||||
<!-- <input type="text" name="Title" value="{{.Title}}"> -->
|
||||
<span id="title" editable="true">{{.Title}}</span>
|
||||
<small>{{.GetReleaseYear}}</small>
|
||||
<input type="text" id="title" name="Title" value="{{.Title}}">
|
||||
</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Artists</td>
|
||||
<td>{{.PrintArtists true true}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
|
@ -50,7 +45,7 @@
|
|||
name="Description"
|
||||
value="{{.Description}}"
|
||||
placeholder="No description provided."
|
||||
rows="3"
|
||||
rows="1"
|
||||
id="description"
|
||||
>{{.Description}}</textarea>
|
||||
</td>
|
||||
|
@ -130,9 +125,7 @@
|
|||
</div>
|
||||
<div class="card links">
|
||||
{{range .Links}}
|
||||
<div class="release-link" data-id="{{.Name}}">
|
||||
<a href="{{.URL}}" class="button">{{.Name}} <img src="/img/external-link.svg"/></a></p>
|
||||
</div>
|
||||
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img src="/img/external-link.svg"/></a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
|
@ -148,13 +141,19 @@
|
|||
<div class="card tracks">
|
||||
{{range .Tracks}}
|
||||
<div class="track" data-id="{{.ID}}">
|
||||
<h2 class="track-title">{{.Number}}. {{.Title}}</h2>
|
||||
<p class="track-id">{{.ID}}</p>
|
||||
<h2 class="track-title">
|
||||
<span class="track-number">{{.Number}}</span>
|
||||
<a href="/admin/track/{{.ID}}">{{.Title}}</a>
|
||||
</h2>
|
||||
|
||||
<h3>Description</h3>
|
||||
{{if .Description}}
|
||||
<p class="track-description">{{.Description}}</p>
|
||||
{{else}}
|
||||
<p class="track-description empty">No description provided.</p>
|
||||
{{end}}
|
||||
|
||||
<h3>Lyrics</h3>
|
||||
{{if .Lyrics}}
|
||||
<p class="track-lyrics">{{.Lyrics}}</p>
|
||||
{{else}}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="card-title">
|
||||
<h1>Releases</h1>
|
||||
<a href="/admin/createrelease" class="create-btn">Create New</a>
|
||||
<a href="/admin/createrelease" class="create-btn" id="create-release">Create New</a>
|
||||
</div>
|
||||
<div class="card releases">
|
||||
{{range $Release := .Releases}}
|
||||
|
@ -93,5 +93,6 @@
|
|||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/admin.js" defer></script>
|
||||
<script type="module" src="/admin/static/admin.js"></script>
|
||||
<script type="module" src="/admin/static/index.js"></script>
|
||||
{{end}}
|
||||
|
|
128
api/release.go
128
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)
|
||||
|
||||
title := data.ID
|
||||
if data.Title != nil && *data.Title != "" {
|
||||
title = *data.Title
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if data.Buyname == nil || *data.Buyname == "" { *data.Buyname = "buy" }
|
||||
if data.Buylink == nil || *data.Buylink == "" { *data.Buylink = "https://arimelody.me" }
|
||||
} 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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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", "<br>", -1)),
|
||||
Number: len(release.Tracks) - i,
|
||||
}}, tracks...)
|
||||
Number: i + 1,
|
||||
})
|
||||
}
|
||||
|
||||
lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK}
|
||||
|
|
6
public/img/list-grabber.svg
Normal file
6
public/img/list-grabber.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 144 144" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M136,66L136,78C136,81.311 133.311,84 130,84L14,84C10.689,84 8,81.311 8,78L8,66C8,62.689 10.689,60 14,60L130,60C133.311,60 136,62.689 136,66ZM136,100L136,112C136,115.311 133.311,118 130,118L14,118C10.689,118 8,115.311 8,112L8,100C8,96.689 10.689,94 14,94L130,94C133.311,94 136,96.689 136,100ZM136,32L136,44C136,47.311 133.311,50 130,50L14,50C10.689,50 8,47.311 8,44L8,32C8,28.689 10.689,26 14,26L130,26C133.311,26 136,28.689 136,32Z"/>
|
||||
<path d="M136,32L136,44C136,47.311 133.311,50 130,50L14,50C10.689,50 8,47.311 8,44L8,32C8,28.689 10.689,26 14,26L130,26C133.311,26 136,28.689 136,32ZM136,66L136,78C136,81.311 133.311,84 130,84L14,84C10.689,84 8,81.311 8,78L8,66C8,62.689 10.689,60 14,60L130,60C133.311,60 136,62.689 136,66ZM136,100L136,112C136,115.311 133.311,118 130,118L14,118C10.689,118 8,115.311 8,112L8,100C8,96.689 10.689,94 14,94L130,94C133.311,94 136,96.689 136,100Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
res/list-grabber.afdesign
Normal file
BIN
res/list-grabber.afdesign
Normal file
Binary file not shown.
BIN
res/list-grabber.afdesign~lock~
Normal file
BIN
res/list-grabber.afdesign~lock~
Normal file
Binary file not shown.
|
@ -64,6 +64,7 @@
|
|||
<p>Releases: {{.PrintReleaseDate}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if .IsReleased}}
|
||||
<ul id="links">
|
||||
{{if .Buylink}}
|
||||
<li>
|
||||
|
@ -77,6 +78,7 @@
|
|||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Description}}
|
||||
<p id="description">
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
</h1>
|
||||
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
|
||||
<h3 class="music-type-{{$Release.ReleaseType}}">{{$Release.ReleaseType}}</h3>
|
||||
{{if $Release.IsReleased}}
|
||||
<ul class="music-links">
|
||||
{{range $Link := $Release.Links}}
|
||||
<li>
|
||||
|
@ -45,6 +46,7 @@
|
|||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue