moving design to final files
This commit is contained in:
		
							parent
							
								
									f65918db8b
								
							
						
					
					
						commit
						3e635d9e44
					
				
					 6 changed files with 325 additions and 59 deletions
				
			
		
							
								
								
									
										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> | ||||
|       <img id="sanic-logo" alt="sanic logo" src="/img/sanic-logo.webp" /> | ||||
|       <div>Sanic © 2023</div> | ||||
|     <div id="sanic-logo"> | ||||
|       <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,42 +81,62 @@ | |||
|   </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 tr = document.createElement("tr"); | ||||
| 
 | ||||
|     const pos = document.createElement("td"); | ||||
|     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 = "🗑"; | ||||
|     actions.appendChild(del); | ||||
|     tr.appendChild(actions); | ||||
| 
 | ||||
|     queue.appendChild(tr); | ||||
| 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.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 = "🔺"; // 🔺 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); | ||||
|   table.appendChild(tr); | ||||
| } | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue