moving design to final files
This commit is contained in:
parent
f65918db8b
commit
3e635d9e44
8
NOTES.md
8
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
|
||||
|
||||
|
|
10
server.go
10
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)
|
||||
}
|
||||
|
|
48
static/controls.js
vendored
48
static/controls.js
vendored
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
<button>Login</button>
|
||||
<button>Config</button>
|
||||
<button id="control-update-db" disabled="disabled">Update DB</button>
|
||||
</div>
|
||||
</div><!--/#control-admin-->
|
||||
<div>
|
||||
<div class="spaced">
|
||||
<button id="control-previous">⏮︎</button> <!-- ⏮️ Last Track Button -->
|
||||
<button id="control-stop">⏹︎</button> <!-- ⏹️ Stop Button -->
|
||||
<button id="control-play-pause">⏵︎</button> <!-- ▶️ Play or ⏸️ Pause Button -->
|
||||
<button id="control-next">⏭︎</button> <!-- ⏭️ Next Track Button -->
|
||||
</div>
|
||||
</div><!--/.spaced-->
|
||||
<div>
|
||||
<label for="control-progress"></label>
|
||||
<input type="range" id="control-progress" name="progress" min="0" step="1" />
|
||||
|
@ -31,7 +31,7 @@
|
|||
<div class="spaced">
|
||||
<button id="control-repeat">🔘 repeat</button>
|
||||
<button id="control-shuffle">🔘 shuffle</button>
|
||||
</div>
|
||||
</div><!--/.spaced-->
|
||||
<div class="spaced">
|
||||
<label for="control-xfade">xfade</label>
|
||||
<div>
|
||||
|
@ -39,12 +39,12 @@
|
|||
<input type="number" id="control-xfade" name="xfade" value="00" />
|
||||
<button id="control-xfade-plus">➕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div><!--/.spaced-->
|
||||
<div class="spaced">
|
||||
<button id="control-volume-down">🔉</button> <!-- 🔉 Speaker with sound wave -->
|
||||
<input id="control-volume" name="volume" type="range" min="0" max="100" value="50" />
|
||||
<button id="control-volume-up">🔊</button> <!-- 🔊 Speaker with sound waves -->
|
||||
</div>
|
||||
</div><!--/.spaced-->
|
||||
</div>
|
||||
<div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="sanic-logo">
|
||||
<div>
|
||||
<img id="sanic-logo" alt="sanic logo" src="/img/sanic-logo.webp" />
|
||||
<div>Sanic © 2023</div>
|
||||
<img alt="sanic logo" src="/img/sanic-logo.webp" />
|
||||
Sanic © 2023
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="queue">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Pos</td>
|
||||
<td>Artists</td>
|
||||
<td>Track</td>
|
||||
<td>Album</td>
|
||||
<td>Length</td>
|
||||
<td>Actions</td>
|
||||
<th>Pos</th>
|
||||
<th>Artists</th>
|
||||
<th>Track</th>
|
||||
<th>Album</th>
|
||||
<th>Length</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
@ -79,41 +81,61 @@
|
|||
</div>
|
||||
<div id="browser">browser</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 © 2023
|
||||
</footer>
|
||||
</main>
|
||||
<script src="controls.js"></script>
|
||||
<script>
|
||||
// create example elements in queue
|
||||
const queue = document.querySelector("#queue tbody");
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const table = document.querySelector("#queue > table > tbody");
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
if (i === 1) {
|
||||
tr.classList.add("playing");
|
||||
}
|
||||
const pos = document.createElement("td");
|
||||
pos.appendChild(document.createTextNode(i.toString()));
|
||||
tr.appendChild(pos);
|
||||
|
||||
pos.innerText = i.toString();
|
||||
const artist = document.createElement("td");
|
||||
artist.appendChild(document.createTextNode(`Artist ${i.toString()}`));
|
||||
tr.appendChild(artist);
|
||||
|
||||
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.appendChild(document.createTextNode(`Track ${i.toString()}`));
|
||||
tr.appendChild(track);
|
||||
|
||||
track.innerText = `Track ${i.toString()} with super long name that could potentially expand the table by a lot!`;
|
||||
const album = document.createElement("td");
|
||||
album.appendChild(document.createTextNode(`Album ${i.toString()}`));
|
||||
tr.appendChild(album);
|
||||
|
||||
album.innerText = `Album ${i.toString()}`;
|
||||
const length = document.createElement("td");
|
||||
length.appendChild(document.createTextNode("1:23"));
|
||||
tr.appendChild(length);
|
||||
|
||||
length.innerText = "01:00:00";
|
||||
const actions = document.createElement("td");
|
||||
const del = document.createElement("button");
|
||||
del.innerHTML = "🗑";
|
||||
actions.appendChild(del);
|
||||
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 ${i} up`);
|
||||
});
|
||||
// TODO: maybe use a instead of button?
|
||||
const moveDown = document.createElement("button");
|
||||
moveDown.classList.add("borderless");
|
||||
moveDown.innerHTML = "🔻"; // 🔻 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 = "❌"; // ❌ 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);
|
||||
|
||||
queue.appendChild(tr);
|
||||
table.appendChild(tr);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
157
static/style.css
157
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;
|
||||
}
|
||||
|
|
47
static/util.js
Normal file
47
static/util.js
Normal 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 = "🔺"; // 🔺 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);
|
||||
}
|
Loading…
Reference in a new issue