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"
|
hx-swap="beforeend"
|
||||||
>
|
>
|
||||||
<img src="{{$Artist.GetAvatar}}" alt="" width="16" loading="lazy" class="artist-avatar">
|
<img src="{{$Artist.GetAvatar}}" alt="" width="16" loading="lazy" class="artist-avatar">
|
||||||
<span class="artist-name">{{$Artist.Name}}</span>
|
<span class="artist-name">{{$Artist.Name}} <span class="artist-id">({{$Artist.ID}})</span></span>
|
||||||
<span class="artist-id">({{$Artist.ID}})</span>
|
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<input type="checkbox" name="primary" {{if .Primary}}checked{{end}}>
|
<input type="checkbox" name="primary" {{if .Primary}}checked{{end}}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="delete">Delete</button>
|
<a class="delete">Delete</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -40,6 +40,8 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
import { makeMagicList } from "/admin/static/admin.js";
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const container = document.getElementById("editcredits");
|
const container = document.getElementById("editcredits");
|
||||||
const form = document.querySelector("#editcredits form");
|
const form = document.querySelector("#editcredits form");
|
||||||
|
@ -47,39 +49,21 @@
|
||||||
const addCreditBtn = document.getElementById("add-credit");
|
const addCreditBtn = document.getElementById("add-credit");
|
||||||
const discardBtn = form.querySelector("button#discard");
|
const discardBtn = form.querySelector("button#discard");
|
||||||
|
|
||||||
function creditFromElement(el) {
|
makeMagicList(creditList, ".credit");
|
||||||
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));
|
|
||||||
|
|
||||||
creditList.addEventListener("htmx:afterSwap", e => {
|
creditList.addEventListener("htmx:afterSwap", e => {
|
||||||
const el = creditList.children[creditList.children.length - 1];
|
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();
|
container.showModal();
|
||||||
|
@ -89,12 +73,18 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
form.addEventListener("submit", e => {
|
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();
|
e.preventDefault();
|
||||||
fetch(form.action, {
|
fetch(form.action, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(credits)
|
body: JSON.stringify(credits)
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.ok) location = location;
|
if (res.ok) location = location;
|
||||||
|
@ -105,7 +95,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).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);
|
console.error(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,6 @@
|
||||||
<input type="checkbox" name="primary">
|
<input type="checkbox" name="primary">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="delete">Delete</button>
|
<a class="delete">Delete</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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":
|
case "newcredit":
|
||||||
serveNewCredit().ServeHTTP(w, r)
|
serveNewCredit().ServeHTTP(w, r)
|
||||||
return
|
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)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
@ -52,11 +64,11 @@ func serveRelease() http.Handler {
|
||||||
|
|
||||||
tracks := []gatewayTrack{}
|
tracks := []gatewayTrack{}
|
||||||
for i, track := range release.Tracks {
|
for i, track := range release.Tracks {
|
||||||
tracks = append([]gatewayTrack{{
|
tracks = append(tracks, gatewayTrack{
|
||||||
Track: track,
|
Track: track,
|
||||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||||
Number: len(release.Tracks) - i,
|
Number: i + 1,
|
||||||
}}, tracks...)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK}
|
lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK}
|
||||||
|
@ -121,3 +133,84 @@ func serveNewCredit() http.Handler {
|
||||||
return
|
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 {
|
.card {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card h2 {
|
.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 {
|
#release {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
|
@ -12,10 +18,6 @@
|
||||||
|
|
||||||
.release-artwork {
|
.release-artwork {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-artwork img {
|
.release-artwork img {
|
||||||
|
@ -28,6 +30,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-info {
|
.release-info {
|
||||||
|
width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -38,6 +41,28 @@
|
||||||
margin: 0;
|
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 {
|
.release-title small {
|
||||||
opacity: .75;
|
opacity: .75;
|
||||||
}
|
}
|
||||||
|
@ -71,6 +96,7 @@
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
}
|
}
|
||||||
.release-info table td:has(select),
|
.release-info table td:has(select),
|
||||||
.release-info table td:has(input),
|
.release-info table td:has(input),
|
||||||
|
@ -126,6 +152,10 @@ button[disabled] {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.delete {
|
||||||
|
color: #d22828;
|
||||||
|
}
|
||||||
|
|
||||||
.release-actions {
|
.release-actions {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -134,90 +164,6 @@ button[disabled] {
|
||||||
justify-content: right;
|
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 {
|
dialog {
|
||||||
width: min(720px, calc(100% - 2em));
|
width: min(720px, calc(100% - 2em));
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
|
@ -245,13 +191,15 @@ dialog div.dialog-actions {
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits ul {
|
.card-title a.button {
|
||||||
margin: 0;
|
text-decoration: none;
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits .credit>div {
|
/*
|
||||||
|
* RELEASE CREDITS
|
||||||
|
*/
|
||||||
|
|
||||||
|
.card.credits .credit {
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -264,24 +212,70 @@ dialog#editcredits .credit>div {
|
||||||
border: 1px solid #808080;
|
border: 1px solid #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits .credit p {
|
.card.credits .credit .artist-avatar {
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog#editcredits .credit .artist-avatar {
|
|
||||||
border-radius: .5em;
|
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%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits .credit .credit-info .credit-attribute {
|
#editcredits .credit .credit-info .credit-attribute {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
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;
|
margin-left: .25em;
|
||||||
padding: .2em .4em;
|
padding: .2em .4em;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -291,15 +285,255 @@ dialog#editcredits .credit .credit-info .credit-attribute input[type="text"] {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits .credit .artist-name {
|
#editcredits .credit .artist-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits .credit .artist-role small {
|
#editcredits .credit .artist-role small {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
opacity: .66;
|
opacity: .66;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog#editcredits .credit button.delete {
|
#editcredits .credit button.delete {
|
||||||
margin-left: auto;
|
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"
|
import Stateful from "/script/silver.min.js"
|
||||||
|
|
||||||
const releaseID = document.getElementById("release").dataset.id;
|
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 type_input = document.getElementById("type");
|
||||||
const desc_input = document.getElementById("description");
|
const desc_input = document.getElementById("description");
|
||||||
const date_input = document.getElementById("release-date");
|
const date_input = document.getElementById("release-date");
|
||||||
|
@ -10,20 +12,22 @@ const buylink_input = document.getElementById("buylink");
|
||||||
const vis_input = document.getElementById("visibility");
|
const vis_input = document.getElementById("visibility");
|
||||||
const save_btn = document.getElementById("save");
|
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) {
|
function update_data(old) {
|
||||||
let release_data = {
|
var release_data = {
|
||||||
visible: vis_input.value === "true",
|
visible: vis_input.value === "true",
|
||||||
title: undefined,
|
title: title_input.value,
|
||||||
description: desc_input.value,
|
description: desc_input.value,
|
||||||
type: type_input.value,
|
type: type_input.value,
|
||||||
releaseDate: date_input.value,
|
releaseDate: date_input.value,
|
||||||
artwork: artwork_input.attributes.src.value,
|
artwork: artwork_data,
|
||||||
buyname: buyname_input.value,
|
buyname: buyname_input.value,
|
||||||
buylink: buylink_input.value,
|
buylink: buylink_input.value,
|
||||||
};
|
};
|
||||||
|
@ -38,8 +42,6 @@ function update_data(old) {
|
||||||
function save_release() {
|
function save_release() {
|
||||||
console.table(release_data);
|
console.table(release_data);
|
||||||
|
|
||||||
edited.set(false);
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
"/api/v1/music/" + releaseID, {
|
"/api/v1/music/" + releaseID, {
|
||||||
|
@ -61,15 +63,29 @@ function save_release() {
|
||||||
location = location;
|
location = location;
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
window.save_release = save_release;
|
|
||||||
|
|
||||||
edited.onUpdate(edited => {
|
edited.onUpdate(edited => {
|
||||||
save_btn.disabled = !edited;
|
save_btn.disabled = !edited;
|
||||||
})
|
})
|
||||||
|
|
||||||
artwork_input.addEventListener("click", () => {
|
title_input.addEventListener("change", () => {
|
||||||
release_data = update_data(release_data);
|
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", () => {
|
type_input.addEventListener("change", () => {
|
||||||
release_data = update_data(release_data);
|
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>
|
<title>editing {{.Title}} - ari melody 💫</title>
|
||||||
<link rel="shortcut icon" href="{{.GetArtwork}}" type="image/x-icon">
|
<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}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
@ -11,18 +11,13 @@
|
||||||
<div id="release" data-id="{{.ID}}">
|
<div id="release" data-id="{{.ID}}">
|
||||||
<div class="release-artwork">
|
<div class="release-artwork">
|
||||||
<img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="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>
|
||||||
<div class="release-info">
|
<div class="release-info">
|
||||||
<h1 class="release-title">
|
<h1 class="release-title">
|
||||||
<!-- <input type="text" name="Title" value="{{.Title}}"> -->
|
<input type="text" id="title" name="Title" value="{{.Title}}">
|
||||||
<span id="title" editable="true">{{.Title}}</span>
|
|
||||||
<small>{{.GetReleaseYear}}</small>
|
|
||||||
</h1>
|
</h1>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
|
||||||
<td>Artists</td>
|
|
||||||
<td>{{.PrintArtists true true}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Type</td>
|
<td>Type</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -50,7 +45,7 @@
|
||||||
name="Description"
|
name="Description"
|
||||||
value="{{.Description}}"
|
value="{{.Description}}"
|
||||||
placeholder="No description provided."
|
placeholder="No description provided."
|
||||||
rows="3"
|
rows="1"
|
||||||
id="description"
|
id="description"
|
||||||
>{{.Description}}</textarea>
|
>{{.Description}}</textarea>
|
||||||
</td>
|
</td>
|
||||||
|
@ -130,9 +125,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card links">
|
<div class="card links">
|
||||||
{{range .Links}}
|
{{range .Links}}
|
||||||
<div class="release-link" data-id="{{.Name}}">
|
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img src="/img/external-link.svg"/></a>
|
||||||
<a href="{{.URL}}" class="button">{{.Name}} <img src="/img/external-link.svg"/></a></p>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -148,13 +141,19 @@
|
||||||
<div class="card tracks">
|
<div class="card tracks">
|
||||||
{{range .Tracks}}
|
{{range .Tracks}}
|
||||||
<div class="track" data-id="{{.ID}}">
|
<div class="track" data-id="{{.ID}}">
|
||||||
<h2 class="track-title">{{.Number}}. {{.Title}}</h2>
|
<h2 class="track-title">
|
||||||
<p class="track-id">{{.ID}}</p>
|
<span class="track-number">{{.Number}}</span>
|
||||||
|
<a href="/admin/track/{{.ID}}">{{.Title}}</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
{{if .Description}}
|
{{if .Description}}
|
||||||
<p class="track-description">{{.Description}}</p>
|
<p class="track-description">{{.Description}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="track-description empty">No description provided.</p>
|
<p class="track-description empty">No description provided.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<h3>Lyrics</h3>
|
||||||
{{if .Lyrics}}
|
{{if .Lyrics}}
|
||||||
<p class="track-lyrics">{{.Lyrics}}</p>
|
<p class="track-lyrics">{{.Lyrics}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h1>Releases</h1>
|
<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>
|
||||||
<div class="card releases">
|
<div class="card releases">
|
||||||
{{range $Release := .Releases}}
|
{{range $Release := .Releases}}
|
||||||
|
@ -93,5 +93,6 @@
|
||||||
|
|
||||||
</main>
|
</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}}
|
{{end}}
|
||||||
|
|
128
api/release.go
128
api/release.go
|
@ -1,9 +1,14 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -85,40 +90,53 @@ func CreateRelease() http.Handler {
|
||||||
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
if data.Buyname == nil || *data.Buyname == "" { *data.Buyname = "buy" }
|
} else {
|
||||||
if data.Buylink == nil || *data.Buylink == "" { *data.Buylink = "https://arimelody.me" }
|
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 {
|
if global.GetRelease(data.ID) != nil {
|
||||||
http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest)
|
||||||
return
|
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{
|
var release = model.Release{
|
||||||
ID: data.ID,
|
ID: data.ID,
|
||||||
Visible: *data.Visible,
|
Visible: false,
|
||||||
Title: *data.Title,
|
Title: title,
|
||||||
Description: *data.Description,
|
Description: description,
|
||||||
ReleaseType: *data.ReleaseType,
|
ReleaseType: releaseType,
|
||||||
ReleaseDate: releaseDate,
|
ReleaseDate: releaseDate,
|
||||||
Artwork: *data.Artwork,
|
Artwork: artwork,
|
||||||
Buyname: *data.Buyname,
|
Buyname: buyname,
|
||||||
Buylink: *data.Buylink,
|
Buylink: buylink,
|
||||||
Links: []*model.Link{},
|
Links: []*model.Link{},
|
||||||
Credits: []*model.Credit{},
|
Credits: []*model.Credit{},
|
||||||
Tracks: []*model.Track{},
|
Tracks: []*model.Track{},
|
||||||
|
@ -181,7 +199,69 @@ func UpdateRelease() http.Handler {
|
||||||
}
|
}
|
||||||
update.ReleaseDate = newDate
|
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 != nil {
|
||||||
if *data.Buyname == "" {
|
if *data.Buyname == "" {
|
||||||
http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest)
|
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,
|
err := db.Select(&track_rows,
|
||||||
"SELECT track FROM musicreleasetrack "+
|
"SELECT track FROM musicreleasetrack "+
|
||||||
"WHERE release=$1 "+
|
"WHERE release=$1 "+
|
||||||
"ORDER BY number DESC",
|
"ORDER BY number ASC",
|
||||||
release.ID,
|
release.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -86,11 +86,11 @@ func ServeGateway() http.Handler {
|
||||||
|
|
||||||
tracks := []gatewayTrack{}
|
tracks := []gatewayTrack{}
|
||||||
for i, track := range release.Tracks {
|
for i, track := range release.Tracks {
|
||||||
tracks = append([]gatewayTrack{{
|
tracks = append(tracks, gatewayTrack{
|
||||||
Track: track,
|
Track: track,
|
||||||
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)),
|
||||||
Number: len(release.Tracks) - i,
|
Number: i + 1,
|
||||||
}}, tracks...)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK}
|
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>
|
<p>Releases: {{.PrintReleaseDate}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .IsReleased}}
|
||||||
<ul id="links">
|
<ul id="links">
|
||||||
{{if .Buylink}}
|
{{if .Buylink}}
|
||||||
<li>
|
<li>
|
||||||
|
@ -77,6 +78,7 @@
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .Description}}
|
{{if .Description}}
|
||||||
<p id="description">
|
<p id="description">
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
|
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
|
||||||
<h3 class="music-type-{{$Release.ReleaseType}}">{{$Release.ReleaseType}}</h3>
|
<h3 class="music-type-{{$Release.ReleaseType}}">{{$Release.ReleaseType}}</h3>
|
||||||
|
{{if $Release.IsReleased}}
|
||||||
<ul class="music-links">
|
<ul class="music-links">
|
||||||
{{range $Link := $Release.Links}}
|
{{range $Link := $Release.Links}}
|
||||||
<li>
|
<li>
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in a new issue