diff options
| author | Carl Hetherington <cth@carlh.net> | 2024-12-15 00:17:07 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2026-02-16 01:20:38 +0100 |
| commit | ae96ef6432d87a2c186dec0d2d902f63ed919e84 (patch) | |
| tree | a7e3083fadce6a1e6bace7688b6dfbacc91da4e5 | |
| parent | f0f3bf7395c9b0b3c1bb6edbd3e4fe12290da8e3 (diff) | |
Add playlists web interface.
| -rw-r--r-- | web/common.css | 33 | ||||
| -rw-r--r-- | web/index.html | 36 | ||||
| -rw-r--r-- | web/playlists.html | 329 | ||||
| -rw-r--r-- | web/sidebar.html | 25 |
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> + |
