moving design to final files

This commit is contained in:
XenGi 2024-01-20 11:40:13 +01:00
parent f65918db8b
commit 3e635d9e44
Signed by: xengi
SSH key fingerprint: SHA256:FGp51kRvGOcWnTHiOI39ImwVO4A3fpvR30nPX3LpV7g
6 changed files with 325 additions and 59 deletions

View file

@ -29,3 +29,11 @@
- [x] track progress (seek) - [x] track progress (seek)
- [ ] track name - [ ] track name
# foo
- client: connect websocket
- server: on_connect: send full state
- server: subscribe to changes
- server: on_change: send to client

View file

@ -134,6 +134,10 @@ func wsServe(c echo.Context) error {
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
} }
queue, err := mpdConn.PlaylistInfo(-1, -1)
if err != nil {
c.Logger().Error(err)
}
jsonStatus, err := json.Marshal(status) jsonStatus, err := json.Marshal(status)
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
@ -142,7 +146,11 @@ func wsServe(c echo.Context) error {
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
} }
err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s,\"mpd_current_song\":%s}", string(jsonStatus), string(jsonCurrentSong))) jsonQueue, err := json.Marshal(queue)
if err != nil {
c.Logger().Error(err)
}
err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s,\"mpd_current_song\":%s,\"mpd_queue\":%s}", string(jsonStatus), string(jsonCurrentSong), string(jsonQueue)))
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
} }

48
static/controls.js vendored
View file

@ -190,6 +190,54 @@ socket.addEventListener("message", (e) => {
} }
} }
// update queue
if ("mpd_queue" in msg && msg.mpd_queue != null) {
const tbody = document.createElement("tbody");
msg.mpd_queue.forEach(elem => {
const tr = document.createElement("tr");
const pos = document.createElement("td");
pos.innerText = elem.Pos;
const artist = document.createElement("td");
if ("Artist" in elem) {
artist.innerText = elem.Artist;
}
const track = document.createElement("td");
if ("Title" in elem) {
track.innerText = elem.Title;
} else {
track.innerText = elem.file;
}
const album = document.createElement("td");
// album.innerText = "";
const length = document.createElement("td");
const duration_hours = Math.floor(elem.duration / 3600);
const duration_minutes = Math.floor((elem.duration - duration_hours * 3600) / 60);
const duration_seconds = Math.floor(elem.duration - duration_hours * 3600 - duration_minutes * 60);
length.innerText = `${duration_hours}:${duration_minutes.toString().padStart(2, '0')}:${duration_seconds.toString().padStart(2, '0')}`;
const actions = document.createElement("td");
const del = document.createElement("button");
del.innerHTML = "🗑️";
del.addEventListener("click", e => {
fetch(`${API_URL}/queue_del/${elem.Pos}`).then(r => {
console.log(r.text());
})
});
actions.appendChild(del);
tr.appendChild(pos);
tr.appendChild(artist);
tr.appendChild(track);
tr.appendChild(album);
tr.appendChild(length);
tr.appendChild(actions);
tbody.appendChild(tr);
});
const currentQueue = document.querySelector("#queue tbody")
if (currentQueue.innerHTML !== tbody.innerHTML) {
console.log("Updating queue")
currentQueue.outerHTML = tbody.outerHTML;
}
}
if ("mpd_error" in msg) { if ("mpd_error" in msg) {
console.error(`MPD Error: ${msg.mpd_error}`) console.error(`MPD Error: ${msg.mpd_error}`)
} }

View file

@ -14,14 +14,14 @@
<button>Login</button> <button>Login</button>
<button>Config</button> <button>Config</button>
<button id="control-update-db" disabled="disabled">Update DB</button> <button id="control-update-db" disabled="disabled">Update DB</button>
</div> </div><!--/#control-admin-->
<div> <div>
<div class="spaced"> <div class="spaced">
<button id="control-previous">&#x23EE;&#xFE0E;</button> <!-- ⏮️ Last Track Button --> <button id="control-previous">&#x23EE;&#xFE0E;</button> <!-- ⏮️ Last Track Button -->
<button id="control-stop">&#x23F9;&#xFE0E;</button> <!-- ⏹️ Stop Button --> <button id="control-stop">&#x23F9;&#xFE0E;</button> <!-- ⏹️ Stop Button -->
<button id="control-play-pause">&#x23F5;&#xFE0E;</button> <!-- ▶️ Play or ⏸️ Pause Button --> <button id="control-play-pause">&#x23F5;&#xFE0E;</button> <!-- ▶️ Play or ⏸️ Pause Button -->
<button id="control-next">&#x23ED;&#xFE0E;</button> <!-- ⏭️ Next Track Button --> <button id="control-next">&#x23ED;&#xFE0E;</button> <!-- ⏭️ Next Track Button -->
</div> </div><!--/.spaced-->
<div> <div>
<label for="control-progress"></label> <label for="control-progress"></label>
<input type="range" id="control-progress" name="progress" min="0" step="1" /> <input type="range" id="control-progress" name="progress" min="0" step="1" />
@ -31,7 +31,7 @@
<div class="spaced"> <div class="spaced">
<button id="control-repeat">&#x1F518; repeat</button> <button id="control-repeat">&#x1F518; repeat</button>
<button id="control-shuffle">&#x1F518; shuffle</button> <button id="control-shuffle">&#x1F518; shuffle</button>
</div> </div><!--/.spaced-->
<div class="spaced"> <div class="spaced">
<label for="control-xfade">xfade</label> <label for="control-xfade">xfade</label>
<div> <div>
@ -39,12 +39,12 @@
<input type="number" id="control-xfade" name="xfade" value="00" /> <input type="number" id="control-xfade" name="xfade" value="00" />
<button id="control-xfade-plus">&#x2795;</button> <button id="control-xfade-plus">&#x2795;</button>
</div> </div>
</div> </div><!--/.spaced-->
<div class="spaced"> <div class="spaced">
<button id="control-volume-down">&#x1F509;</button> <!-- 🔉 Speaker with sound wave --> <button id="control-volume-down">&#x1F509;</button> <!-- 🔉 Speaker with sound wave -->
<input id="control-volume" name="volume" type="range" min="0" max="100" value="50" /> <input id="control-volume" name="volume" type="range" min="0" max="100" value="50" />
<button id="control-volume-up">&#x1F50A;</button> <!-- 🔊 Speaker with sound waves --> <button id="control-volume-up">&#x1F50A;</button> <!-- 🔊 Speaker with sound waves -->
</div> </div><!--/.spaced-->
</div> </div>
<div> <div>
<p>Now playing:</p> <p>Now playing:</p>
@ -57,21 +57,23 @@
<input type="text" id="control-time" name="time" value="00:00:00/00:00:00" disabled="disabled" /> <input type="text" id="control-time" name="time" value="00:00:00/00:00:00" disabled="disabled" />
</div> </div>
</div> </div>
<div id="sanic-logo">
<div> <div>
<img id="sanic-logo" alt="sanic logo" src="/img/sanic-logo.webp" /> <img alt="sanic logo" src="/img/sanic-logo.webp" />
<div>Sanic &copy; 2023</div> Sanic &copy; 2023
</div>
</div> </div>
</div> </div>
<div id="queue"> <div id="queue">
<table> <table>
<thead> <thead>
<tr> <tr>
<td>Pos</td> <th>Pos</th>
<td>Artists</td> <th>Artists</th>
<td>Track</td> <th>Track</th>
<td>Album</td> <th>Album</th>
<td>Length</td> <th>Length</th>
<td>Actions</td> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -79,42 +81,62 @@
</div> </div>
<div id="browser">browser</div> <div id="browser">browser</div>
<div id="result">result</div> <div id="result">result</div>
<footer>
<a href="https://git.berlin.ccc.de/cccb/sanic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 92 92"><defs><clipPath id="a"><path d="M0 .113h91.887V92H0Zm0 0"/></clipPath></defs><g clip-path="url(#a)"><path style="stroke:none;fill-rule:nonzero;fill:#ffffff;fill-opacity:1" d="M90.156 41.965 50.036 1.848a5.913 5.913 0 0 0-8.368 0l-8.332 8.332 10.566 10.566a7.03 7.03 0 0 1 7.23 1.684 7.043 7.043 0 0 1 1.673 7.277l10.183 10.184a7.026 7.026 0 0 1 7.278 1.672 7.04 7.04 0 0 1 0 9.957 7.045 7.045 0 0 1-9.961 0 7.038 7.038 0 0 1-1.532-7.66l-9.5-9.497V59.36a7.04 7.04 0 0 1 1.86 11.29 7.04 7.04 0 0 1-9.957 0 7.04 7.04 0 0 1 0-9.958 7.034 7.034 0 0 1 2.308-1.539V33.926a7.001 7.001 0 0 1-2.308-1.535 7.049 7.049 0 0 1-1.516-7.7L29.242 14.273 1.734 41.777a5.918 5.918 0 0 0 0 8.371L41.855 90.27a5.92 5.92 0 0 0 8.368 0l39.933-39.934a5.925 5.925 0 0 0 0-8.371"/></g></svg></a> Sanic MPD Web UI 0.1.0 - by XenGi and coon &copy; 2023
</footer>
</main> </main>
<script src="controls.js"></script> <script src="controls.js"></script>
<script> <script>
// create example elements in queue const table = document.querySelector("#queue > table > tbody");
const queue = document.querySelector("#queue tbody"); for (let i = 1; i <= 100; i++) {
for (let i = 0; i < 100; i++) {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
if (i === 1) {
const pos = document.createElement("td"); tr.classList.add("playing");
pos.appendChild(document.createTextNode(i.toString()));
tr.appendChild(pos);
const artist = document.createElement("td");
artist.appendChild(document.createTextNode(`Artist ${i.toString()}`));
tr.appendChild(artist);
const track = document.createElement("td");
track.appendChild(document.createTextNode(`Track ${i.toString()}`));
tr.appendChild(track);
const album = document.createElement("td");
album.appendChild(document.createTextNode(`Album ${i.toString()}`));
tr.appendChild(album);
const length = document.createElement("td");
length.appendChild(document.createTextNode("1:23"));
tr.appendChild(length);
const actions = document.createElement("td");
const del = document.createElement("button");
del.innerHTML = "&#x1f5d1;";
actions.appendChild(del);
tr.appendChild(actions);
queue.appendChild(tr);
} }
const pos = document.createElement("td");
pos.innerText = i.toString();
const artist = document.createElement("td");
artist.innerText = `Artist ${i.toString()} with a super long name that should finally bring the length to a maximum`;
const track = document.createElement("td");
track.innerText = `Track ${i.toString()} with super long name that could potentially expand the table by a lot!`;
const album = document.createElement("td");
album.innerText = `Album ${i.toString()}`;
const length = document.createElement("td");
length.innerText = "01:00:00";
const actions = document.createElement("td");
actions.classList.add("actions");
// TODO: maybe use a instead of button?
const moveUp = document.createElement("button");
moveUp.classList.add("borderless");
moveUp.innerHTML = "&#x1F53A;"; // 🔺 Red Triangle Pointed Down
moveUp.addEventListener("click", event => {
console.log(`DEBUG: move song ${i} up`);
});
// TODO: maybe use a instead of button?
const moveDown = document.createElement("button");
moveDown.classList.add("borderless");
moveDown.innerHTML = "&#x1F53B;"; // 🔻 Red Triangle Pointed Up
moveDown.addEventListener("click", event => {
console.log(`DEBUG: move song ${i} down`);
});
// TODO: maybe use a instead of button?
const remove = document.createElement("button");
remove.classList.add("borderless");
remove.innerHTML = "&#x274C;"; // ❌ Cross mark; 🗑️ Wastebasket
remove.addEventListener("click", event => {
console.log(`DEBUG: remove song ${i} from queue`);
});
actions.appendChild(moveUp);
actions.appendChild(moveDown);
actions.appendChild(remove);
tr.appendChild(pos);
tr.appendChild(artist);
tr.appendChild(track);
tr.appendChild(album);
tr.appendChild(length);
tr.appendChild(actions);
table.appendChild(tr);
}
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,8 +1,15 @@
:root {
--ribbon-width: 160px;
}
/* #################### */ /* #################### */
/* #### structure ##### */ /* #### structure ##### */
/* #################### */ /* #################### */
html, body { margin: 0; height: 100%; } html, body {
margin: 0;
height: 100%;
}
main { main {
height: 100%; height: 100%;
@ -11,7 +18,7 @@ main {
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
grid-template-rows: 150px 1fr 1fr; grid-template-rows: 150px 1fr 1fr;
gap: 0 0; gap: 0 0;
grid-template-areas: "nav nav" "queue queue" "browser result"; grid-template-areas: "nav nav" "queue queue" "browser result" "footer footer";
} }
#queue { #queue {
@ -33,6 +40,13 @@ main {
grid-area: browser; grid-area: browser;
} }
main footer {
grid-area: footer;
overflow: auto;
background-color: #041936;
text-align: right;
}
table { table {
width: 100%; width: 100%;
} }
@ -43,8 +57,22 @@ table {
} }
#sanic-logo { #sanic-logo {
max-width: 80%; display: flex;
max-height: 80%; flex-grow: 1;
justify-content: flex-end;
}
#sanic-logo > div {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
#sanic-logo img {
max-width: 75%;
max-height: 75%;
margin: auto;
} }
.spaced { .spaced {
@ -52,14 +80,6 @@ table {
justify-content: space-between; justify-content: space-between;
} }
/* #################### */
/* ###### debug ####### */
/* #################### */
div {
border: 1px solid blue;
}
/* #################### */ /* #################### */
/* ### pretty stuff ### */ /* ### pretty stuff ### */
/* #################### */ /* #################### */
@ -132,3 +152,116 @@ div {
} }
} }
*/ */
html, body {
background-color: #09101d;
color: #bbb;
scrollbar-color: #490b00 #09101d; /* only in firefox: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color */
font-weight: normal;
font-family: Arial, Helvetica, sans-serif;
font-size: 12pt;
}
a {
color: #bbb;
text-decoration: none;
}
button {
background-color: #28374a;
color: #bbb;
border-top-color: #545454;
border-right-color: #3a3a3a;
border-bottom-color: #3a3a3a;
border-left-color: #545454;
}
/* borderless button used in queue */
.borderless {
border: none;
background-color: inherit;
cursor: pointer;
}
input[type=text] {
background-color: #28374a;
color: white;
border: 1px solid black;
border-right-color: #545454;
border-bottom-color: #545454;
}
thead {
background: rgb(15,29,47);
background: linear-gradient(0deg, rgba(15,29,47,1) 0%, rgba(15,29,47,1) 50%, rgba(7,14,23,1) 100%);
}
th {
font-weight: bold;
padding: 2px 2px 2px 14px;
border: solid #1c2c1a;
border-width: 0 1px 0 0;
cursor: pointer;
}
/* show and hide action buttons on hover */
tbody tr td button {
display: none;
}
tbody tr:hover td button {
display: inline-block;
}
/* fixed width for action buttons in queue so it doesn't change size when hovering */
tbody tr td:last-of-type {
min-width: 6em;
}
tbody td.actions {
white-space: nowrap;
}
#queue {
border-bottom: 4px ridge #3a506b;
}
/* make arrow for currently playing song look nice */
#queue table tr.playing td:first-of-type::before {
content: '\2BC8'; //
}
#queue table tr td:first-of-type {
text-align: right;
padding-right: 0.5em;
}
/* align times */
#queue table tr td:nth-last-of-type(2) {
text-align: right;
}
table tr:nth-child(odd) td {
background: #1e1f1a;
}
table tr:nth-child(even) td {
background: #171812;
}
#queue table tr:nth-child(odd).playing td,
#queue table tr:nth-child(even).playing td {
background-color: #490b00;
}
table tr:hover td {
background-color: #354158 !important; /* TODO: remove !important */
}
footer svg {
color: white;
width: 12pt;
height: 12pt;
}

47
static/util.js Normal file
View file

@ -0,0 +1,47 @@
function addSongToQueue(song) {
const table = document.querySelector("#queue tbody");
const tr = document.createElement("tr");
const pos = document.createElement("td");
pos.innerText = "?"; // TODO: figure out queue length +1
const artist = document.createElement("td");
artist.innerText = song.artist;
const track = document.createElement("td");
track.innerText = song.track;
const album = document.createElement("td");
album.innerText = song.album;
const length = document.createElement("td");
length.innerText = song.length;
const actions = document.createElement("td");
actions.classList.add("actions");
// TODO: maybe use a instead of button?
const moveUp = document.createElement("button");
moveUp.classList.add("borderless");
moveUp.innerHTML = "&#x1F53A;"; // 🔺 Red Triangle Pointed Down
moveUp.addEventListener("click", event => {
console.log(`DEBUG: move song ${song.id} up`);
});
// TODO: maybe use a instead of button?
const moveDown = document.createElement("button");
moveDown.classList.add("borderless");
moveDown.innerHTML = "$#x1F53B;"; // 🔻 Red Triangle Pointed Up
moveDown.addEventListener("click", event => {
console.log(`DEBUG: move song ${song.id} down`);
});
// TODO: maybe use a instead of button?
const remove = document.createElement("button");
remove.classList.add("borderless");
remove.innerHTML = "$#x274C;"; // ❌ Cross mark; 🗑️ Wastebasket
remove.addEventListener("click", event => {
console.log(`DEBUG: remove song ${song.id} from queue`);
});
actions.appendChild(moveUp);
actions.appendChild(moveDown);
actions.appendChild(remove);
tr.appendChild(pos);
tr.appendChild(artist);
tr.appendChild(track);
tr.appendChild(album);
tr.appendChild(length);
tr.appendChild(actions);
table.appendChild(tr);
}