diff --git a/NOTES.md b/NOTES.md index 793ee3e..559fd24 100644 --- a/NOTES.md +++ b/NOTES.md @@ -29,3 +29,11 @@ - [x] track progress (seek) - [ ] track name + +# foo + +- client: connect websocket +- server: on_connect: send full state +- server: subscribe to changes +- server: on_change: send to client + diff --git a/server.go b/server.go index ae7e321..c9dff82 100644 --- a/server.go +++ b/server.go @@ -134,6 +134,10 @@ func wsServe(c echo.Context) error { if err != nil { c.Logger().Error(err) } + queue, err := mpdConn.PlaylistInfo(-1, -1) + if err != nil { + c.Logger().Error(err) + } jsonStatus, err := json.Marshal(status) if err != nil { c.Logger().Error(err) @@ -142,7 +146,11 @@ func wsServe(c echo.Context) error { if err != nil { 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 { c.Logger().Error(err) } diff --git a/static/controls.js b/static/controls.js index 67b2610..9748f64 100644 --- a/static/controls.js +++ b/static/controls.js @@ -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) { console.error(`MPD Error: ${msg.mpd_error}`) } diff --git a/static/index.html b/static/index.html index de40c89..2387fb9 100644 --- a/static/index.html +++ b/static/index.html @@ -14,14 +14,14 @@ - +
-
+
@@ -31,7 +31,7 @@
-
+
@@ -39,12 +39,12 @@
-
+
-
+

Now playing:

@@ -57,21 +57,23 @@
-
- -
Sanic © 2023
+
- - - - - - + + + + + + @@ -79,42 +81,62 @@
browser
result
+ diff --git a/static/style.css b/static/style.css index 3b9adae..9e16773 100644 --- a/static/style.css +++ b/static/style.css @@ -1,8 +1,15 @@ +:root { + --ribbon-width: 160px; +} + /* #################### */ /* #### structure ##### */ /* #################### */ -html, body { margin: 0; height: 100%; } +html, body { + margin: 0; + height: 100%; +} main { height: 100%; @@ -11,7 +18,7 @@ main { grid-template-columns: 1fr 2fr; grid-template-rows: 150px 1fr 1fr; gap: 0 0; - grid-template-areas: "nav nav" "queue queue" "browser result"; + grid-template-areas: "nav nav" "queue queue" "browser result" "footer footer"; } #queue { @@ -33,6 +40,13 @@ main { grid-area: browser; } +main footer { + grid-area: footer; + overflow: auto; + background-color: #041936; + text-align: right; +} + table { width: 100%; } @@ -43,8 +57,22 @@ table { } #sanic-logo { - max-width: 80%; - max-height: 80%; + display: flex; + 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 { @@ -52,14 +80,6 @@ table { justify-content: space-between; } -/* #################### */ -/* ###### debug ####### */ -/* #################### */ - -div { - border: 1px solid blue; -} - /* #################### */ /* ### 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; +} diff --git a/static/util.js b/static/util.js new file mode 100644 index 0000000..b732169 --- /dev/null +++ b/static/util.js @@ -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 = "🔺"; // 🔺 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); +}
PosArtistsTrackAlbumLengthActionsPosArtistsTrackAlbumLengthActions