Merge branch 'dev' into feature/blog
THAT WAS PAINFUL!
This commit is contained in:
commit
3e5ecb9372
99 changed files with 2029 additions and 1010 deletions
26
admin/templates/html/artists.html
Normal file
26
admin/templates/html/artists.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{{define "head"}}
|
||||
<title>Artists - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/artists.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<header>
|
||||
<h1>Artists <small>({{len .Artists}} total)</small></h2>
|
||||
<a class="button new" id="create-artist">Create New</a>
|
||||
</header>
|
||||
|
||||
{{if .Artists}}
|
||||
<div class="artists-group">
|
||||
{{range .Artists}}
|
||||
{{block "artist" .}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>There are no artists.</p>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/artists.js"></script>
|
||||
{{end}}
|
||||
6
admin/templates/html/components/artist/artist.html
Normal file
6
admin/templates/html/components/artist/artist.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{{define "artist"}}
|
||||
<div class="artist">
|
||||
<img src="{{.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||
<a href="/admin/music/artists/{{.ID}}" class="artist-name">{{.Name}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
47
admin/templates/html/components/credit/addcredit.html
Normal file
47
admin/templates/html/components/credit/addcredit.html
Normal 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/music/releases/{{$.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>
|
||||
114
admin/templates/html/components/credit/editcredits.html
Normal file
114
admin/templates/html/components/credit/editcredits.html
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<dialog id="editcredits">
|
||||
<header>
|
||||
<h2>Editing: Credits</h2>
|
||||
<a id="add-credit"
|
||||
class="button new"
|
||||
href="/admin/music/releases/{{.ID}}/addcredit"
|
||||
hx-get="/admin/music/releases/{{.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>
|
||||
17
admin/templates/html/components/credit/newcredit.html
Normal file
17
admin/templates/html/components/credit/newcredit.html
Normal 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>
|
||||
159
admin/templates/html/components/link/editlinks.html
Normal file
159
admin/templates/html/components/link/editlinks.html
Normal 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>
|
||||
23
admin/templates/html/components/release/release.html
Normal file
23
admin/templates/html/components/release/release.html
Normal 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/music/releases/{{.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/music/releases/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p>
|
||||
<div class="release-actions">
|
||||
<a href="/admin/music/releases/{{.ID}}">Edit</a>
|
||||
<a href="/music/{{.ID}}" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
47
admin/templates/html/components/track/addtrack.html
Normal file
47
admin/templates/html/components/track/addtrack.html
Normal 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/music/releases/{{$.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>
|
||||
112
admin/templates/html/components/track/edittracks.html
Normal file
112
admin/templates/html/components/track/edittracks.html
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<dialog id="edittracks">
|
||||
<header>
|
||||
<h2>Editing: Tracks</h2>
|
||||
<a id="add-track"
|
||||
class="button new"
|
||||
href="/admin/music/releases/{{.Release.ID}}/addtrack"
|
||||
hx-get="/admin/music/releases/{{.Release.ID}}/addtrack"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Add</a>
|
||||
</header>
|
||||
|
||||
<form action="/api/v1/music/{{.Release.ID}}/tracks">
|
||||
<ul>
|
||||
{{range .Release.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>
|
||||
9
admin/templates/html/components/track/newtrack.html
Normal file
9
admin/templates/html/components/track/newtrack.html
Normal 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>
|
||||
24
admin/templates/html/components/track/track.html
Normal file
24
admin/templates/html/components/track/track.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{{define "track"}}
|
||||
<div class="track" data-id="{{.ID}}">
|
||||
<h2 class="track-title">
|
||||
{{if .Number}}
|
||||
<span class="track-number">{{.Number}}</span>
|
||||
{{end}}
|
||||
<a href="/admin/music/tracks/{{.ID}}">{{.Title}}</a>
|
||||
</h2>
|
||||
|
||||
<h3>Description</h3>
|
||||
{{if .Description}}
|
||||
<p class="track-description">{{.GetDescriptionHTML}}</p>
|
||||
{{else}}
|
||||
<p class="track-description empty">No description provided.</p>
|
||||
{{end}}
|
||||
|
||||
<h3>Lyrics</h3>
|
||||
{{if .Lyrics}}
|
||||
<p class="track-lyrics">{{.GetLyricsHTML}}</p>
|
||||
{{else}}
|
||||
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
91
admin/templates/html/edit-account.html
Normal file
91
admin/templates/html/edit-account.html
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{{define "head"}}
|
||||
<title>Account Settings - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/edit-account.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
{{if .Session.Message.Valid}}
|
||||
<p id="message">{{html .Session.Message.String}}</p>
|
||||
{{end}}
|
||||
{{if .Session.Error.Valid}}
|
||||
<p id="error">{{html .Session.Error.String}}</p>
|
||||
{{end}}
|
||||
<h1>Account Settings ({{.Session.Account.Username}})</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Change Password</h2>
|
||||
</div>
|
||||
<form action="/admin/account/password" method="POST" id="change-password">
|
||||
<label for="current-password">Current Password</label>
|
||||
<input type="password" id="current-password" name="current-password" value="" autocomplete="current-password" required>
|
||||
|
||||
<label for="new-password">New Password</label>
|
||||
<input type="password" id="new-password" name="new-password" value="" autocomplete="new-password" required>
|
||||
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input type="password" id="confirm-password" value="" autocomplete="new-password" required>
|
||||
|
||||
<br>
|
||||
|
||||
<button type="submit" class="save">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card mfa-devices">
|
||||
<div class="card-header">
|
||||
<h2>MFA Devices</h2>
|
||||
</div>
|
||||
{{if .TOTPs}}
|
||||
{{range .TOTPs}}
|
||||
<div class="mfa-device">
|
||||
<div>
|
||||
<p class="mfa-device-name">{{.TOTP.Name}}</p>
|
||||
<p class="mfa-device-date">Added: {{.CreatedAtString}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<form method="POST" action="/admin/account/totp-delete">
|
||||
<input type="text" name="totp-name" value="{{.TOTP.Name}}" hidden>
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>You have no MFA devices.</p>
|
||||
{{end}}
|
||||
|
||||
<div class="mfa-actions">
|
||||
<button type="submit" class="save" id="enable-email" disabled>Enable Email TOTP</button>
|
||||
<a class="button new" id="add-totp-device" href="/admin/account/totp-setup">Add TOTP Device</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card danger">
|
||||
<div class="card-header">
|
||||
<h2>Danger Zone</h2>
|
||||
</div>
|
||||
<p>
|
||||
Clicking the button below will delete your account.
|
||||
This action is <strong>irreversible</strong>.
|
||||
You will need to enter your password and TOTP below.
|
||||
</p>
|
||||
<form action="/admin/account/delete" method="POST" id="delete-account">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" value="" autocomplete="current-password" required>
|
||||
|
||||
<label for="totp">TOTP</label>
|
||||
<input type="text" name="totp" value="" autocomplete="one-time-code" required>
|
||||
|
||||
<br>
|
||||
|
||||
<button type="submit" class="delete">Delete Account</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/edit-account.js" defer></script>
|
||||
{{end}}
|
||||
73
admin/templates/html/edit-artist.html
Normal file
73
admin/templates/html/edit-artist.html
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{{define "head"}}
|
||||
<title>Editing {{.Artist.Name}} - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="{{.Artist.GetAvatar}}" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/edit-artist.css">
|
||||
<link rel="stylesheet" href="/admin/static/artists.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Editing Artist</h1>
|
||||
|
||||
<div id="artist" data-id="{{.Artist.ID}}">
|
||||
<div class="artist-avatar">
|
||||
<img src="{{.Artist.Avatar}}" alt="" width="256" loading="lazy" id="avatar">
|
||||
<input type="file" id="avatar-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||
<button id="remove-avatar">Remove</button>
|
||||
</div>
|
||||
<div class="artist-info">
|
||||
<p class="attribute-header">Name</p>
|
||||
<h2 class="artist-name">
|
||||
<input type="text" id="name" name="artist-name" value="{{.Artist.Name}}">
|
||||
</h2>
|
||||
|
||||
<p class="attribute-header">Website</p>
|
||||
<input type="text" id="website" name="website" value="{{.Artist.Website}}">
|
||||
|
||||
<div class="artist-actions">
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="releases">
|
||||
<div class="card-header">
|
||||
<h2>Featured in</h2>
|
||||
</div>
|
||||
{{if .Credits}}
|
||||
{{range .Credits}}
|
||||
<div class="credit">
|
||||
<img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork">
|
||||
<div class="credit-info">
|
||||
<h3 class="credit-name"><a href="/admin/music/releases/{{.Release.ID}}">{{.Release.Title}}</a></h3>
|
||||
<p class="credit-artists">{{.Release.PrintArtists true true}}</p>
|
||||
<p class="artist-role">
|
||||
Role: {{.Role}}
|
||||
{{if .Primary}}
|
||||
<small>(Primary)</small>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>This artist has no credits.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card" id="danger">
|
||||
<div class="card-header">
|
||||
<h2>Danger Zone</h2>
|
||||
</div>
|
||||
<p>
|
||||
Clicking the button below will delete this artist.
|
||||
This action is <strong>irreversible</strong>.
|
||||
You will be prompted to confirm this decision.
|
||||
</p>
|
||||
<button class="delete" id="delete">Delete Artist</button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/edit-artist.js" defer></script>
|
||||
{{end}}
|
||||
182
admin/templates/html/edit-release.html
Normal file
182
admin/templates/html/edit-release.html
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
{{define "head"}}
|
||||
<title>Editing {{.Release.Title}} - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="{{.Release.GetArtwork}}" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/edit-release.css">
|
||||
<link rel="stylesheet" href="/admin/static/releases.css">
|
||||
<link rel="stylesheet" href="/admin/static/tracks.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Editing {{.Release.Title}}</h1>
|
||||
|
||||
<div id="release" data-id="{{.Release.ID}}">
|
||||
<div class="release-artwork">
|
||||
<img src="{{.Release.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
||||
<input type="file" id="artwork-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||
<button id="remove-artwork">Remove</button>
|
||||
</div>
|
||||
<div class="release-info">
|
||||
<h1 class="release-title">
|
||||
<input type="text" id="title" name="Title" value="{{.Release.Title}}" autocomplete="on">
|
||||
</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
{{$t := .Release.ReleaseType}}
|
||||
<select name="Type" id="type">
|
||||
<option value="single" {{if eq $t "single"}}selected{{end}}>
|
||||
Single
|
||||
</option>
|
||||
<option value="album" {{if eq $t "album"}}selected{{end}}>
|
||||
Album
|
||||
</option>
|
||||
<option value="ep" {{if eq $t "ep"}}selected{{end}}>
|
||||
EP
|
||||
</option>
|
||||
<option value="compilation" {{if eq $t "compilation"}}selected{{end}}>
|
||||
Compilation
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>
|
||||
<textarea
|
||||
name="Description"
|
||||
value="{{.Release.Description}}"
|
||||
placeholder="No description provided."
|
||||
rows="3"
|
||||
id="description"
|
||||
>{{.Release.Description}}</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Release Date</td>
|
||||
<td>
|
||||
<input type="datetime-local" name="release-date" id="release-date" value="{{.Release.TextReleaseDate}}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Buy Name</td>
|
||||
<td>
|
||||
<input type="text" name="buyname" id="buyname" value="{{.Release.Buyname}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Buy Link</td>
|
||||
<td>
|
||||
<input type="text" name="buylink" id="buylink" value="{{.Release.Buylink}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Copyright</td>
|
||||
<td>
|
||||
<input type="text" name="copyright" id="copyright" value="{{.Release.Copyright}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Copyright URL</td>
|
||||
<td>
|
||||
<input type="text" name="copyright-url" id="copyright-url" value="{{.Release.CopyrightURL}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Visible</td>
|
||||
<td>
|
||||
<select name="Visibility" id="visibility">
|
||||
<option value="true" {{if .Release.Visible}}selected{{end}}>True</option>
|
||||
<option value="false" {{if not .Release.Visible}}selected{{end}}>False</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="release-actions">
|
||||
<a href="/music/{{.Release.ID}}" class="button" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a>
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="credits" class="card">
|
||||
<div class="card-header">
|
||||
<h2>Credits <small>({{len .Release.Credits}} total)</small></h2>
|
||||
<a class="button edit"
|
||||
href="/admin/music/releases/{{.Release.ID}}/editcredits"
|
||||
hx-get="/admin/music/releases/{{.Release.ID}}/editcredits"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Edit</a>
|
||||
</div>
|
||||
|
||||
{{range .Release.Credits}}
|
||||
<div class="credit">
|
||||
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||
<div class="credit-info">
|
||||
<p class="artist-name"><a href="/admin/music/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
|
||||
<p class="artist-role">
|
||||
{{.Role}}
|
||||
{{if .Primary}}
|
||||
<small>(Primary)</small>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .Release.Credits}}
|
||||
<p>There are no credits.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div id="links" class="card">
|
||||
<div class="card-header">
|
||||
<h2>Links <small>({{len .Release.Links}} total)</small></h2>
|
||||
<a class="button edit"
|
||||
href="/admin/music/releases/{{.Release.ID}}/editlinks"
|
||||
hx-get="/admin/music/releases/{{.Release.ID}}/editlinks"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Edit</a>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{{range .Release.Links}}
|
||||
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="tracks" class="card">
|
||||
<div class="card-header">
|
||||
<h2>Tracks <small>({{len .Release.Tracks}} total)</small></h2>
|
||||
<a class="button edit"
|
||||
href="/admin/music/releases/{{.Release.ID}}/edittracks"
|
||||
hx-get="/admin/music/releases/{{.Release.ID}}/edittracks"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Edit</a>
|
||||
</div>
|
||||
|
||||
{{range .Release.Tracks}}
|
||||
{{block "track" .}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card" id="danger">
|
||||
<div class="card-header">
|
||||
<h2>Danger Zone</h2>
|
||||
</div>
|
||||
<p>
|
||||
Clicking the button below will delete this release.
|
||||
This action is <strong>irreversible</strong>.
|
||||
You will be prompted to confirm this decision.
|
||||
</p>
|
||||
<button class="delete" id="delete">Delete Release</button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/edit-release.js" defer></script>
|
||||
{{end}}
|
||||
72
admin/templates/html/edit-track.html
Normal file
72
admin/templates/html/edit-track.html
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{{define "head"}}
|
||||
<title>Editing Track - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/edit-track.css">
|
||||
<link rel="stylesheet" href="/admin/static/tracks.css">
|
||||
<link rel="stylesheet" href="/admin/static/releases.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Editing Track</h1>
|
||||
|
||||
<div id="track" data-id="{{.Track.ID}}">
|
||||
<div class="track-info">
|
||||
<p class="attribute-header">Title</p>
|
||||
<h2 class="track-title">
|
||||
<input type="text" id="title" name="Title" value="{{.Track.Title}}">
|
||||
</h2>
|
||||
|
||||
<p class="attribute-header">Description</p>
|
||||
<textarea
|
||||
name="Description"
|
||||
value="{{.Track.Description}}"
|
||||
placeholder="No description provided."
|
||||
rows="5"
|
||||
id="description"
|
||||
>{{.Track.Description}}</textarea>
|
||||
|
||||
<p class="attribute-header">Lyrics</p>
|
||||
<textarea
|
||||
name="Lyrics"
|
||||
value="{{.Track.Lyrics}}"
|
||||
placeholder="There are no lyrics."
|
||||
rows="5"
|
||||
id="lyrics"
|
||||
>{{.Track.Lyrics}}</textarea>
|
||||
|
||||
<div class="track-actions">
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card releases">
|
||||
<div class="card-header">
|
||||
<h2>Featured in</h2>
|
||||
</div>
|
||||
{{if .Releases}}
|
||||
{{range .Releases}}
|
||||
{{block "release" .}}{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>This track isn't bound to a release.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card danger">
|
||||
<div class="card-header">
|
||||
<h2>Danger Zone</h2>
|
||||
</div>
|
||||
<p>
|
||||
Clicking the button below will delete this track.
|
||||
This action is <strong>irreversible</strong>.
|
||||
You will be prompted to confirm this decision.
|
||||
</p>
|
||||
<button class="delete" id="delete">Delete Track</button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/edit-track.js" defer></script>
|
||||
{{end}}
|
||||
61
admin/templates/html/index.html
Normal file
61
admin/templates/html/index.html
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{{define "head"}}
|
||||
<title>Admin - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/releases.css">
|
||||
<link rel="stylesheet" href="/admin/static/artists.css">
|
||||
<link rel="stylesheet" href="/admin/static/tracks.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main class="dashboard">
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" id="releases">
|
||||
<div class="card-header">
|
||||
<h2><a href="/admin/releases/">Releases</a> <small>({{.ReleaseCount}} total)</small></h2>
|
||||
<a class="button new" id="create-release">Create New</a>
|
||||
</div>
|
||||
{{if .Artists}}
|
||||
{{range .Releases}}
|
||||
{{block "release" .}}{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>There are no releases.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card" id="artists">
|
||||
<div class="card-header">
|
||||
<h2><a href="/admin/artists/">Artists</a> <small>({{.ArtistCount}} total)</small></h2>
|
||||
<a class="button new" id="create-artist">Create New</a>
|
||||
</div>
|
||||
{{if .Artists}}
|
||||
<div class="artists-group">
|
||||
{{range .Artists}}
|
||||
{{block "artist" .}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>There are no artists.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card" id="tracks">
|
||||
<div class="card-header">
|
||||
<h2><a href="/admin/tracks/">Tracks</a> <small>({{.TrackCount}} total)</small></h2>
|
||||
<a class="button new" id="create-track">Create New</a>
|
||||
</div>
|
||||
<p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p>
|
||||
<br>
|
||||
{{range .Tracks}}
|
||||
{{block "track" .}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/admin/static/artists.js"></script>
|
||||
<script type="module" src="/admin/static/index.js"></script>
|
||||
{{end}}
|
||||
71
admin/templates/html/layout.html
Normal file
71
admin/templates/html/layout.html
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{{block "head" .}}{{end}}
|
||||
|
||||
<link rel="stylesheet" href="/admin/static/admin.css">
|
||||
<script type="module" src="/admin/static/admin.js"></script>
|
||||
<script type="module" src="/script/vendor/htmx.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav id="navbar">
|
||||
<a href="/" class="nav icon" aria-label="ari melody" title="Return to Home">
|
||||
<img src="/img/favicon.png" alt="" width="64" height="64">
|
||||
</a>
|
||||
<div class="nav-item{{if eq .Path "/"}} active{{end}}">
|
||||
<a href="/admin">home</a>
|
||||
</div>
|
||||
{{if .Session.Account}}
|
||||
<div class="nav-item{{if eq .Path "/logs"}} active{{end}}">
|
||||
<a href="/admin/logs">logs</a>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="section-label">music</p>
|
||||
<div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}">
|
||||
<a href="/admin/music/releases/">releases</a>
|
||||
</div>
|
||||
<div class="nav-item{{if hasPrefix .Path "/artists"}} active{{end}}">
|
||||
<a href="/admin/music/artists/">artists</a>
|
||||
</div>
|
||||
<div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}">
|
||||
<a href="/admin/music/tracks/">tracks</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="flex-fill"></div>
|
||||
|
||||
{{if .Session.Account}}
|
||||
<div class="nav-item{{if eq .Path "/account"}} active{{end}}">
|
||||
<a href="/admin/account">account ({{.Session.Account.Username}})</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a href="/admin/logout" id="logout">log out</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="nav-item{{if eq .Path "/login"}} active{{end}}">
|
||||
<a href="/admin/login" id="login">log in</a>
|
||||
</div>
|
||||
<div class="nav-item{{if eq .Path "/register"}} active{{end}}">
|
||||
<a href="/admin/register" id="register">create account</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</nav>
|
||||
<button type="button" id="toggle-nav" aria-label="Navigation toggle">
|
||||
<img src="/img/hamburger.svg" alt="">
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{{block "content" .}}{{end}}
|
||||
|
||||
{{template "prideflag"}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
53
admin/templates/html/login-totp.html
Normal file
53
admin/templates/html/login-totp.html
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{{define "head"}}
|
||||
<title>Login - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<style>
|
||||
form#login-totp {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form div {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
form input {
|
||||
width: calc(100% - 1rem - 2px) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
h1 {
|
||||
margin-top: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
{{if .Session.Message.Valid}}
|
||||
<p id="message">{{html .Session.Message.String}}</p>
|
||||
{{end}}
|
||||
{{if .Session.Error.Valid}}
|
||||
<p id="error">{{html .Session.Error.String}}</p>
|
||||
{{end}}
|
||||
|
||||
<form action="/admin/totp" method="POST" id="login-totp">
|
||||
<h1>Two-Factor Authentication</h1>
|
||||
|
||||
<div>
|
||||
<label for="totp">TOTP</label>
|
||||
<input type="text" name="totp" value="" autocomplete="one-time-code" required autofocus>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="save">Login</button>
|
||||
</form>
|
||||
</main>
|
||||
{{end}}
|
||||
56
admin/templates/html/login.html
Normal file
56
admin/templates/html/login.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{{define "head"}}
|
||||
<title>Login - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<style>
|
||||
@media screen and (max-width: 720px) {
|
||||
h1 {
|
||||
margin-top: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
form#login {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form div {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
form input {
|
||||
width: calc(100% - 1rem - 2px) !important;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
{{if .Session.Message.Valid}}
|
||||
<p id="message">{{html .Session.Message.String}}</p>
|
||||
{{end}}
|
||||
{{if .Session.Error.Valid}}
|
||||
<p id="error">{{html .Session.Error.String}}</p>
|
||||
{{end}}
|
||||
|
||||
<form action="/admin/login" method="POST" id="login">
|
||||
<h1>Log In</h1>
|
||||
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" value="" autocomplete="username" required autofocus>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" value="" autocomplete="current-password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="save">Login</button>
|
||||
</form>
|
||||
</main>
|
||||
{{end}}
|
||||
22
admin/templates/html/logout.html
Normal file
22
admin/templates/html/logout.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{{define "head"}}
|
||||
<title>Admin - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
|
||||
<style>
|
||||
p a {
|
||||
color: #2a67c8;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
|
||||
<meta http-equiv="refresh" content="0;url=/admin/login" />
|
||||
<p>
|
||||
Logged out successfully.
|
||||
You should be redirected to <a href="/admin/login">/admin/login</a> shortly.
|
||||
</p>
|
||||
|
||||
</main>
|
||||
{{end}}
|
||||
69
admin/templates/html/logs.html
Normal file
69
admin/templates/html/logs.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{{define "head"}}
|
||||
<title>Audit Logs - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/logs.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Audit Logs</h1>
|
||||
|
||||
<form action="/admin/logs" method="GET" id="search-form">
|
||||
<div id="search">
|
||||
<input type="text" name="q" value="" placeholder="Filter by message...">
|
||||
<button type="submit" class="save">Search</button>
|
||||
</div>
|
||||
<div id="filters">
|
||||
<div>
|
||||
<p>Level:</p>
|
||||
<label for="level-info">Info</label>
|
||||
<input type="checkbox" name="level-info" id="level-info">
|
||||
<label for="level-warn">Warning</label>
|
||||
<input type="checkbox" name="level-warn" id="level-warn">
|
||||
</div>
|
||||
<div>
|
||||
<p>Type:</p>
|
||||
<label for="type-account">Account</label>
|
||||
<input type="checkbox" name="type-account" id="type-account">
|
||||
<label for="type-music">Music</label>
|
||||
<input type="checkbox" name="type-music" id="type-music">
|
||||
<label for="type-artist">Artist</label>
|
||||
<input type="checkbox" name="type-artist" id="type-artist">
|
||||
<label for="type-blog">Blog</label>
|
||||
<input type="checkbox" name="type-blog" id="type-blog">
|
||||
<label for="type-artwork">Artwork</label>
|
||||
<input type="checkbox" name="type-artwork" id="type-artwork">
|
||||
<label for="type-files">Files</label>
|
||||
<input type="checkbox" name="type-files" id="type-files">
|
||||
<label for="type-misc">Misc</label>
|
||||
<input type="checkbox" name="type-misc" id="type-misc">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="logs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="log-time">Time</th>
|
||||
<th class="log-level">Level</th>
|
||||
<th class="log-type">Type</th>
|
||||
<th class="log-content">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Logs}}
|
||||
<tr class="log {{toLower (parseLevel .Level)}}">
|
||||
<td class="log-time">{{prettyTime .CreatedAt}}</td>
|
||||
<td class="log-level">{{parseLevel .Level}}</td>
|
||||
<td class="log-type">{{titleCase .Type}}</td>
|
||||
<td class="log-content">{{.Content}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
{{end}}
|
||||
21
admin/templates/html/prideflag.html
Normal file
21
admin/templates/html/prideflag.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{{define "prideflag"}}
|
||||
<a href="https://github.com/arimelody/prideflag" target="_blank" id="prideflag">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120" hx-preserve="true">
|
||||
<path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/>
|
||||
<path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/>
|
||||
<path id="yellow" d="M120,40 V0 L60,60 L80,80 Z" style="fill:#e5fe02"/>
|
||||
<path id="green" d="M120,0 H80 L40,40 L60,60 Z" style="fill:#09be01"/>
|
||||
<path id="blue" d="M80,0 H40 L20,20 L40,40 Z" style="fill:#081a9a"/>
|
||||
<path id="purple" d="M40,0 H0 L20,20 Z" style="fill:#76008a"/>
|
||||
|
||||
<rect id="black" x="60" width="60" height="60" style="fill:#010101"/>
|
||||
<rect id="brown" x="70" width="50" height="50" style="fill:#603814"/>
|
||||
<rect id="lightblue" x="80" width="40" height="40" style="fill:#73d6ed"/>
|
||||
<rect id="pink" x="90" width="30" height="30" style="fill:#ffafc8"/>
|
||||
<rect id="white" x="100" width="20" height="20" style="fill:#fff"/>
|
||||
|
||||
<rect id="intyellow" x="110" width="10" height="10" style="fill:#fed800"/>
|
||||
<circle id="intpurple" cx="120" cy="0" r="5" stroke="#7601ad" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{end}}
|
||||
60
admin/templates/html/register.html
Normal file
60
admin/templates/html/register.html
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{{define "head"}}
|
||||
<title>Register - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<style>
|
||||
p a {
|
||||
color: #2a67c8;
|
||||
}
|
||||
|
||||
a.discord {
|
||||
color: #5865F2;
|
||||
}
|
||||
|
||||
form#register {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form div {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: calc(100% - 1rem - 2px);
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
{{if .Session.Error.Valid}}
|
||||
<p id="error">{{html .Session.Error.String}}</p>
|
||||
{{end}}
|
||||
|
||||
<form action="/admin/register" method="POST" id="register">
|
||||
<h1>Create Account</h1>
|
||||
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" value="" autocomplete="username" required autofocus>
|
||||
|
||||
<label for="email">Email</label>
|
||||
<input type="text" name="email" value="" autocomplete="email" required>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" value="" autocomplete="new-password" required>
|
||||
|
||||
<label for="invite">Invite Code</label>
|
||||
<input type="text" name="invite" value="" autocomplete="off" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="new">Create Account</button>
|
||||
</form>
|
||||
</main>
|
||||
{{end}}
|
||||
24
admin/templates/html/releases.html
Normal file
24
admin/templates/html/releases.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{{define "head"}}
|
||||
<title>Releases - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/releases.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<header>
|
||||
<h1>Releases <small>({{len .Releases}} total)</small></h1>
|
||||
<a class="button new" id="create-release">Create New</a>
|
||||
</header>
|
||||
|
||||
{{if .Releases}}
|
||||
<div id="releases">
|
||||
{{range .Releases}}
|
||||
{{block "release" .}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>There are no releases.</p>
|
||||
{{end}}
|
||||
</main>
|
||||
{{end}}
|
||||
63
admin/templates/html/totp-confirm.html
Normal file
63
admin/templates/html/totp-confirm.html
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{{define "head"}}
|
||||
<title>TOTP Confirmation - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<style>
|
||||
.qr-code {
|
||||
border: 1px solid #8888;
|
||||
}
|
||||
code {
|
||||
user-select: all;
|
||||
}
|
||||
#totp-setup input {
|
||||
width: 3.8em;
|
||||
min-width: auto;
|
||||
font-size: 32px;
|
||||
font-family: 'Monaspace Argon', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Two-Factor Authentication</h1>
|
||||
|
||||
{{if .Session.Error.Valid}}
|
||||
<p id="error">{{html .Session.Error.String}}</p>
|
||||
{{end}}
|
||||
|
||||
<form action="/admin/account/totp-confirm?totp-name={{.NameEscaped}}" method="POST" id="totp-setup">
|
||||
{{if .QRBase64Image}}
|
||||
<img src="data:image/png;base64,{{.QRBase64Image}}" alt="" class="qr-code">
|
||||
|
||||
<p>
|
||||
Scan the QR code above into your authentication app or password manager,
|
||||
then enter your 2FA code below.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If the QR code does not work, you may also enter this secret code:
|
||||
</p>
|
||||
{{else}}
|
||||
<p>
|
||||
Paste the below secret code into your authentication app or password manager,
|
||||
then enter your 2FA code below:
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<p><code>{{.TOTP.Secret}}</code></p>
|
||||
|
||||
<label for="totp">TOTP:</label>
|
||||
<input type="text"
|
||||
name="totp"
|
||||
value=""
|
||||
minlength="6"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
required
|
||||
autofocus>
|
||||
|
||||
<button type="submit" class="new">Create</button>
|
||||
</form>
|
||||
</main>
|
||||
{{end}}
|
||||
21
admin/templates/html/totp-setup.html
Normal file
21
admin/templates/html/totp-setup.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{{define "head"}}
|
||||
<title>TOTP Setup - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<h1>Two-Factor Authentication</h1>
|
||||
|
||||
{{if .Session.Error.Valid}}
|
||||
<p id="error">{{html .Session.Error.String}}</p>
|
||||
{{end}}
|
||||
|
||||
<form action="/admin/account/totp-setup" method="POST" id="totp-setup">
|
||||
<label for="totp-name">TOTP Device Name:</label>
|
||||
<input type="text" name="totp-name" value="" autocomplete="off" required autofocus>
|
||||
|
||||
<button type="submit" class="new">Create</button>
|
||||
</form>
|
||||
</main>
|
||||
{{end}}
|
||||
20
admin/templates/html/tracks.html
Normal file
20
admin/templates/html/tracks.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{{define "head"}}
|
||||
<title>Releases - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/admin/static/tracks.css">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<header>
|
||||
<h1>Tracks <small>({{len .Tracks}} total)</small></h1>
|
||||
<a class="button new" id="create-track">Create New</a>
|
||||
</header>
|
||||
|
||||
<div id="tracks">
|
||||
{{range $Track := .Tracks}}
|
||||
{{block "track" $Track}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</main>
|
||||
{{end}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue