summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2024-12-15 00:17:07 +0100
committerCarl Hetherington <cth@carlh.net>2026-02-16 01:20:38 +0100
commitae96ef6432d87a2c186dec0d2d902f63ed919e84 (patch)
treea7e3083fadce6a1e6bace7688b6dfbacc91da4e5
parentf0f3bf7395c9b0b3c1bb6edbd3e4fe12290da8e3 (diff)
Add playlists web interface.
-rw-r--r--web/common.css33
-rw-r--r--web/index.html36
-rw-r--r--web/playlists.html329
-rw-r--r--web/sidebar.html25
4 files changed, 391 insertions, 32 deletions
diff --git a/web/common.css b/web/common.css
new file mode 100644
index 000000000..d4c5f7d0d
--- /dev/null
+++ b/web/common.css
@@ -0,0 +1,33 @@
+
+button {
+ border: 1px solid rgba(27, 31, 35, 0.15);
+ border-radius: 6px;
+ color: #24292E;
+ display: inline-block;
+ line-height: 20px;
+ padding: 6px 16px;
+ vertical-align: middle;
+ white-space: nowrap;
+ word-wrap: break-word;
+}
+
+button:hover {
+ background-color: #F3F4F6;
+ text-decoration: none;
+ transition-duration: 0.1s;
+}
+
+button:active {
+ background-color: #EDEFF2;
+ box-shadow: rgba(225, 228, 232, 0.2) 0 1px 0 inset;
+ transition: none 0s;
+}
+
+button:focus {
+ outline: 1px transparent;
+}
+
+button:before {
+ display: none;
+}
+
diff --git a/web/index.html b/web/index.html
index bf5359515..a9f4d5629 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
+<link rel="stylesheet" href="common.css">
<script>
setInterval(function() {
status = fetch("/api/v1/status").then(response => {
@@ -24,38 +25,6 @@
</script>
<style>
-button {
- border: 1px solid rgba(27, 31, 35, 0.15);
- border-radius: 6px;
- color: #24292E;
- display: inline-block;
- line-height: 20px;
- padding: 6px 16px;
- vertical-align: middle;
- white-space: nowrap;
- word-wrap: break-word;
-}
-
-button:hover {
- background-color: #F3F4F6;
- text-decoration: none;
- transition-duration: 0.1s;
-}
-
-button:active {
- background-color: #EDEFF2;
- box-shadow: rgba(225, 228, 232, 0.2) 0 1px 0 inset;
- transition: none 0s;
-}
-
-button:focus {
- outline: 1px transparent;
-}
-
-button:before {
- display: none;
-}
-
table {
border-collapse: collapse;
margin: 25px 0;
@@ -80,6 +49,9 @@ td {
</head>
<body>
+
+ SIDEBAR
+
<button name="play" value="play" onclick="play()">Play</button>
<button name="stop" value="stop" onclick="stop()">Stop</button>
<table>
diff --git a/web/playlists.html b/web/playlists.html
new file mode 100644
index 000000000..3d2e37f41
--- /dev/null
+++ b/web/playlists.html
@@ -0,0 +1,329 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+var selectedPlaylistIndices = [];
+
+function selectedPlaylistId()
+{
+ var children = document.getElementById("playlists").children;
+ for (var i = 0; i < children.length; i++) {
+ if (children[i].classList.contains("selected")) {
+ return children[i].uuid;
+ }
+ }
+
+ return null;
+}
+
+function allowDrop(event)
+{
+ event.preventDefault();
+}
+
+function postPlaylist(playlistId, endpoint, payload)
+{
+ fetch("/api/v1/playlist/" + playlistId + "/" + endpoint, {
+ method: "POST",
+ body: JSON.stringify(payload),
+ headers: {
+ "Content-type": "application/json; charset=UTF-8"
+ }
+ });
+}
+
+function itemIndex(item) {
+ return Array.prototype.indexOf.call(item.parentNode.childNodes, item);
+}
+
+function newPlaylist()
+{
+ fetch("/api/v1/playlists", { method: "POST" }).then(response => { updatePlaylistList(); });
+}
+
+function removeSelectedPlaylist()
+{
+ var id = selectedPlaylistId();
+ if (id == null) {
+ return;
+ }
+
+ fetch("/api/v1/playlist/" + id, { method: "DELETE" }).then(response => { updatePlaylistList(); });
+}
+
+function renamePlaylist()
+{
+ var id = selectedPlaylistId();
+ if (id == null) {
+ return;
+ }
+
+ postPlaylist(id, "rename", { "name": document.getElementById('playlist-name').value });
+ updatePlaylistList();
+}
+
+// Return a <li> to represent a playlist entry
+function makePlaylistEntry(content)
+{
+ var li = document.createElement("li");
+ li.classList.add("playlist");
+
+ var table = document.createElement('table');
+ table.style.width = '100%';
+ var row = table.insertRow()
+ var cell = row.insertCell();
+ cell.style.width = '90%'
+ cell.appendChild(document.createTextNode(content.name));
+ cell = row.insertCell();
+ cell.style.width = '10%'
+ cell.appendChild(document.createTextNode(content.approximate_length));
+ li.appendChild(table);
+
+ li.name = name;
+ li.onclick = function() {
+ li.classList.toggle("selected");
+ selectedPlaylistIndices = [ itemIndex(li) ];
+ updatePlaylist();
+ };
+ li.ondrop = function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ var playlistElement = document.getElementById('playlist');
+ var type = event.dataTransfer.getData("type");
+
+ target = event.target.closest("li");
+
+ var afterElement = null;
+ if (event.offsetY > target.clientHeight / 2) {
+ afterElement = target;
+ } else {
+ afterElement = target.previousSibling;
+ }
+
+ var droppedEntry = null;
+
+ if (type == "insert") {
+ dropped = JSON.parse(event.dataTransfer.getData("content"));
+ droppedEntry = makePlaylistEntry(dropped);
+ var index = 0;
+ if (afterElement) {
+ index = itemIndex(afterElement) + 1;
+ }
+ postPlaylist(
+ playlistElement.getAttribute('playlist-id'),
+ "insert",
+ { index: index, uuid: dropped.uuid },
+ );
+ } else if (type == "move") {
+ var old_index = parseInt(event.dataTransfer.getData("index"));
+ droppedEntry = playlistElement.getElementsByTagName('li')[old_index];
+ var new_index = 0;
+ if (afterElement) {
+ new_index = itemIndex(afterElement) + 1;
+ }
+ if (old_index < new_index) {
+ new_index--;
+ }
+ postPlaylist(
+ playlistElement.getAttribute('playlist-id'),
+ "move",
+ { new_index: new_index, old_index: old_index },
+ );
+ }
+
+ if (afterElement) {
+ afterElement.insertAdjacentElement('afterend', droppedEntry);
+ } else {
+ playlistElement.getElementsByTagName('ul')[0].prepend(droppedEntry);
+ }
+ };
+ li.draggable = "true";
+ li.ondragstart = function(event) {
+ event.dataTransfer.setData("type", "move");
+ event.dataTransfer.setData("index", itemIndex(li));
+ event.dataTransfer.setData("name", li.name);
+ };
+ li.ondragover = "allowDrop(event)";
+ return li;
+}
+
+// Update the displayed playlist (in div id="playlist")
+function updatePlaylist()
+{
+ var playlistId = selectedPlaylistId();
+ if (playlistId == null) {
+ document.getElementById('playlist-name').value = "";
+ return;
+ }
+
+ fetch("/api/v1/playlist/" + playlistId).then(response => {
+ response.json().then(data => {
+ var playlistContents = document.getElementById("playlist");
+ playlistContents.ondrop = function(event) {
+ event.preventDefault();
+ content = JSON.parse(event.dataTransfer.getData("content"));
+ newLi = makePlaylistEntry(content);
+ playlistContents.getElementsByTagName('ul')[0].insertBefore(newLi, null);
+ postPlaylist(
+ playlistContents.getAttribute('playlist-id'),
+ "insert",
+ { index: 0, uuid: content.uuid }
+ );
+ };
+ playlistContents.setAttribute("playlist-id", playlistId);
+ playlistContents.innerHTML = "";
+ var ul = document.createElement("ul");
+ ul.classList.add("playlist");
+ var list = playlistContents.appendChild(ul);
+ var index = 0;
+ data.content.forEach(content => {
+ var li = makePlaylistEntry(content);
+ list.appendChild(li);
+ if (selectedPlaylistIndices.includes(index)) {
+ li.classList.toggle("selected");
+ }
+ ++index;
+ });
+
+ document.getElementById('playlist-name').value = data.name;
+ });
+ });
+}
+
+function setSelectedPlaylist(uuid)
+{
+ var children = document.getElementById("playlists").children;
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ if (child.uuid == uuid) {
+ child.classList.add("selected");
+ } else {
+ child.classList.remove("selected");
+ }
+ }
+};
+
+
+// Fetch all playlists and make the playlist list
+function updatePlaylistList()
+{
+ var selected = null;
+ var playlists = document.getElementById('playlists');
+ if (playlists) {
+ selected = selectedPlaylistId();
+ playlists.innerHTML = "";
+ }
+
+ fetch("/api/v1/playlists").then(response => {
+ response.json().then(data => {
+ data.forEach(playlist => {
+ var li = document.createElement("li");
+ li.classList.add("playlist");
+ li.appendChild(document.createTextNode(playlist.name));
+ li.uuid = playlist['uuid'];
+ li.onclick = function() {
+ selectedPlaylistIndices = [];
+ setSelectedPlaylist(playlist['uuid']);
+ updatePlaylist();
+ };
+ document.getElementById('playlists').appendChild(li);
+ });
+
+ if (selected) {
+ console.log("reselect " + selected);
+ setSelectedPlaylist(selected);
+ }
+ });
+ });
+}
+
+updatePlaylistList();
+
+// Fetch all content and make the content list
+fetch("/api/v1/content").then(response => {
+ response.json().then(data => {
+ data.forEach(content => {
+ var li = document.createElement("li");
+ li.classList.add("playlist");
+ li.appendChild(document.createTextNode(content.name));
+ li.draggable = "true";
+ li.ondragstart = function(event) {
+ event.dataTransfer.setData("type", "insert");
+ event.dataTransfer.setData("content", JSON.stringify(content));
+ };
+ document.getElementById('content').appendChild(li);
+ });
+ });
+})
+</script>
+
+<style>
+
+ul#playlists {
+ width: 400px;
+}
+
+ul.playlist {
+ padding: 2em;
+ margin: 0pt;
+}
+
+li.playlist {
+ border: 1px solid black;
+ background-color: #a7bad9;
+ padding: 3px;
+ list-style-type: none;
+ cursor: pointer;
+}
+
+li.playlist.selected {
+ background-color: #d343e0;
+ cursor: pointer;
+}
+
+div.playlist, div.content {
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
+ border: 2px solid black;
+}
+
+div.content {
+ width: 95%;
+ padding: 1em;
+}
+
+ul#content {
+ padding: 0pt;
+ margin: 0pt;
+}
+
+</style>
+<head>
+<link rel="stylesheet" href="common.css">
+<title>Playlists</title>
+</head>
+
+<body>
+
+SIDEBAR
+
+<div style="width: 45%; display: inline-block; margin-right: 3em;">
+<p>Playlists: <button onclick="newPlaylist();">Add</button> <button id="remove-playlist" onclick="removeSelectedPlaylist();">Remove</button>
+<ul id="playlists">
+</ul>
+
+<p>Playlist: <input id="playlist-name" type="text" maxlength="60"></input> <button onclick="renamePlaylist();">Rename</button>
+<div id="playlist" class="playlist" ondragover="allowDrop(event)">
+</div>
+</div>
+
+<div style="float: right;">
+<p>Content:
+<div class="content">
+<ul id="content">
+</div>
+</ul>
+</div>
+
+</body>
+
+</html>
diff --git a/web/sidebar.html b/web/sidebar.html
new file mode 100644
index 000000000..7ea629cc0
--- /dev/null
+++ b/web/sidebar.html
@@ -0,0 +1,25 @@
+<style>
+div.nav {
+ float: left;
+ margin-right: 4em;
+}
+li.nav {
+ border: 1px solid rgba(57, 61, 65, 0.50);
+ border-radius: 6px;
+ list-style-type: none;
+ padding: 8px;
+ margin: 4px;
+}
+a.nav {
+ color: black;
+ text-decoration: none;
+}
+</style>
+
+<div class="nav">
+<ul class="nav">
+ <li class="nav"><a class="nav" href="/">Transport</a></li>
+ <li class="nav"><a class="nav" href="/playlists">Playlists</a></li>
+</ul>
+</div>
+