embed html template and static files

This commit is contained in:
ari melody 2025-09-30 19:03:35 +01:00
parent b150fa491c
commit e5dcc4b884
Signed by: ari
GPG key ID: CF99829C92678188
44 changed files with 316 additions and 255 deletions

View file

@ -0,0 +1,47 @@
<dialog id="addcredit">
<header>
<h2>Add Artist Credit</h2>
</header>
<ul>
{{range $Artist := .Artists}}
<li class="new-artist"
data-id="{{$Artist.ID}}"
hx-get="/admin/release/{{$.ReleaseID}}/newcredit/{{$Artist.ID}}"
hx-target="#editcredits ul"
hx-swap="beforeend"
>
<img src="{{$Artist.GetAvatar}}" alt="" width="16" loading="lazy" class="artist-avatar">
<span class="artist-name">{{$Artist.Name}} <span class="artist-id">({{$Artist.ID}})</span></span>
</li>
{{end}}
</ul>
{{if not .Artists}}
<p class="empty">There are no more artists to add.</p>
{{end}}
<div class="dialog-actions">
<button id="cancel" type="button">Cancel</button>
</div>
<script type="text/javascript">
(() => {
const newCreditModal = document.getElementById("addcredit")
const editCreditsModal = document.getElementById("editcredits")
const cancelBtn = newCreditModal.querySelector("#cancel");
editCreditsModal.addEventListener("htmx:afterSwap", () => {
newCreditModal.close();
newCreditModal.remove();
});
cancelBtn.addEventListener("click", () => {
newCreditModal.close();
newCreditModal.remove();
});
newCreditModal.showModal();
})();
</script>
</dialog>

View file

@ -0,0 +1,114 @@
<dialog id="editcredits">
<header>
<h2>Editing: Credits</h2>
<a id="add-credit"
class="button new"
href="/admin/release/{{.ID}}/addcredit"
hx-get="/admin/release/{{.ID}}/addcredit"
hx-target="body"
hx-swap="beforeend"
>Add</a>
</header>
<form action="/api/v1/music/{{.ID}}/credits">
<ul>
{{range .Credits}}
<li class="credit" data-artist="{{.Artist.ID}}">
<div>
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<div class="credit-info">
<p class="artist-name">{{.Artist.Name}}</p>
<div class="credit-attribute">
<label for="role">Role:</label>
<input type="text" name="role" value="{{.Role}}">
</div>
<div class="credit-attribute">
<label for="primary">Primary:</label>
<input type="checkbox" name="primary" {{if .Primary}}checked{{end}}>
</div>
</div>
<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("editcredits");
const form = document.querySelector("#editcredits form");
const creditList = form.querySelector("ul");
const addCreditBtn = document.getElementById("add-credit");
const discardBtn = form.querySelector("button#discard");
makeMagicList(creditList, ".credit");
function rigCredit(el) {
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.draggable = true;
el.addEventListener("dragstart", () => { el.classList.add("moving") });
el.addEventListener("dragend", () => { el.classList.remove("moving") });
}
[...creditList.querySelectorAll(".credit")].map(rigCredit);
creditList.addEventListener("htmx:afterSwap", () => {
rigCredit(creditList.children[creditList.children.length - 1]);
});
container.showModal();
container.addEventListener("close", () => {
container.remove();
});
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" },
body: JSON.stringify(credits)
}).then(res => {
if (res.ok) location = location;
else {
res.text().then(err => {
alert(err);
console.error(err);
});
}
}).catch(err => {
alert("Failed to update credits. Check the console for details.");
console.error(err);
});
});
discardBtn.addEventListener("click", e => {
e.preventDefault();
container.close();
});
})();
</script>
</dialog>

View file

@ -0,0 +1,17 @@
<li class="credit" data-artist="{{.ID}}">
<div>
<img src="{{.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<div class="credit-info">
<p class="artist-name">{{.Name}}</p>
<div class="credit-attribute">
<label for="role">Role:</label>
<input type="text" name="role" value="">
</div>
<div class="credit-attribute">
<label for="primary">Primary:</label>
<input type="checkbox" name="primary">
</div>
</div>
<a class="delete">Delete</a>
</div>
</li>

View file

@ -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>

View file

@ -0,0 +1,23 @@
{{define "release"}}
<div class="release">
<div class="release-artwork">
<img src="{{.GetArtwork}}" alt="" width="128" loading="lazy">
</div>
<div class="release-info">
<h3 class="release-title">
<a href="/admin/release/{{.ID}}">{{.Title}}</a>
<small>
<span title="{{.PrintReleaseDate}}">{{.ReleaseDate.Year}}</span>
{{if not .Visible}}(hidden){{end}}
</small>
</h3>
<p class="release-artists">{{.PrintArtists true true}}</p>
<p class="release-type-single">{{.ReleaseType}}
(<a href="/admin/release/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p>
<div class="release-actions">
<a href="/admin/release/{{.ID}}">Edit</a>
<a href="/music/{{.ID}}" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a>
</div>
</div>
</div>
{{end}}

View file

@ -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>

View file

@ -0,0 +1,112 @@
<dialog id="edittracks">
<header>
<h2>Editing: Tracks</h2>
<a id="add-track"
class="button new"
href="/admin/release/{{.Release.ID}}/addtrack"
hx-get="/admin/release/{{.Release.ID}}/addtrack"
hx-target="body"
hx-swap="beforeend"
>Add</a>
</header>
<form action="/api/v1/music/{{.Release.ID}}/tracks">
<ul>
{{range $i, $track := .Release.Tracks}}
<li class="track" data-track="{{$track.ID}}" data-title="{{$track.Title}}" data-number="{{$track.Add $i 1}}" draggable="true">
<div>
<p class="track-name">
<span class="track-number">{{.Add $i 1}}</span>
{{$track.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>

View file

@ -0,0 +1,9 @@
<li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="0" draggable="true">
<div>
<p class="track-name">
<span class="track-number">0</span>
{{.Title}}
</p>
<a class="delete">Delete</a>
</div>
</li>