gui functions nearly done
This commit is contained in:
		
							parent
							
								
									114d6ba4da
								
							
						
					
					
						commit
						f65918db8b
					
				
					 6 changed files with 297 additions and 81 deletions
				
			
		
							
								
								
									
										1
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
										
									
									
									
								
							|  | @ -1,3 +1,4 @@ | |||
| mpd: | ||||
| 	mkdir -p /tmp/sanic/{music,playlists} | ||||
| 	touch /tmp/sanic/mpd_db | ||||
| 	mpd --no-daemon ./mpd.conf | ||||
|  |  | |||
							
								
								
									
										64
									
								
								mpd.go
									
										
									
									
									
								
							
							
						
						
									
										64
									
								
								mpd.go
									
										
									
									
									
								
							|  | @ -3,7 +3,6 @@ package main | |||
| import ( | ||||
| 	"github.com/fhs/gompd/v2/mpd" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | @ -15,13 +14,13 @@ func updateDb(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	jobId, err := conn.Update("") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, strconv.Itoa(jobId)) | ||||
|  | @ -31,13 +30,13 @@ func previousTrack(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	err = conn.Previous() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -47,13 +46,13 @@ func nextTrack(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	err = conn.Next() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -63,13 +62,13 @@ func stopPlayback(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	err = conn.Stop() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -79,13 +78,24 @@ func resumePlayback(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	err = conn.Pause(false) | ||||
| 	status, err := conn.Status() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	if status["state"] == "stop" { | ||||
| 		err := conn.Play(-1) | ||||
| 		if err != nil { | ||||
| 			c.Logger().Error(err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		err = conn.Pause(false) | ||||
| 		if err != nil { | ||||
| 			c.Logger().Error(err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -95,13 +105,13 @@ func pausePlayback(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	err = conn.Pause(true) | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -111,13 +121,13 @@ func seek(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	seconds, err := strconv.Atoi(c.Param("seconds")) | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if seconds < 0 { | ||||
|  | @ -126,7 +136,7 @@ func seek(c echo.Context) error { | |||
| 
 | ||||
| 	err = conn.SeekCur(time.Duration(seconds)*time.Second, false) | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -136,13 +146,13 @@ func toggleRepeat(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	status, err := conn.Status() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	if status["repeat"] == "1" { | ||||
| 		err = conn.Repeat(false) | ||||
|  | @ -150,7 +160,7 @@ func toggleRepeat(c echo.Context) error { | |||
| 		err = conn.Repeat(true) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -160,21 +170,21 @@ func toggleRandom(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	status, err := conn.Status() | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	if status["toggleRandom"] == "1" { | ||||
| 	if status["random"] == "1" { | ||||
| 		err = conn.Random(false) | ||||
| 	} else { | ||||
| 		err = conn.Random(true) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  | @ -184,13 +194,13 @@ func setVolume(c echo.Context) error { | |||
| 	// Connect to MPD server | ||||
| 	conn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	level, err := strconv.Atoi(c.Param("level")) | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if level > 100 || level < 0 { | ||||
|  | @ -199,7 +209,7 @@ func setVolume(c echo.Context) error { | |||
| 
 | ||||
| 	err = conn.SetVolume(level) | ||||
| 	if err != nil { | ||||
| 		log.Fatalln(err) | ||||
| 		c.Logger().Error(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return c.String(http.StatusOK, "") | ||||
|  |  | |||
							
								
								
									
										24
									
								
								server.go
									
										
									
									
									
								
							
							
						
						
									
										24
									
								
								server.go
									
										
									
									
									
								
							|  | @ -107,7 +107,12 @@ func wsServe(c echo.Context) error { | |||
| 		// Connect to MPD server | ||||
| 		mpdConn, err := mpd.Dial("tcp", "localhost:6600") | ||||
| 		if err != nil { | ||||
| 			log.Fatalln(err) | ||||
| 			//log.Fatalln(err) | ||||
| 			c.Logger().Error(err) | ||||
| 			err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_error\":\"%s\"}", err.Error())) | ||||
| 			if err != nil { | ||||
| 				c.Logger().Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 		defer mpdConn.Close() | ||||
| 
 | ||||
|  | @ -121,16 +126,23 @@ func wsServe(c echo.Context) error { | |||
| 			} else { | ||||
| 				log.Println(msg) | ||||
| 				if strings.ToLower(msg) == "#status" { | ||||
| 					// TODO: Get current MPD status and return it | ||||
| 					status, err := mpdConn.Status() | ||||
| 					if err != nil { | ||||
| 						log.Fatalln(err) | ||||
| 						c.Logger().Error(err) | ||||
| 					} | ||||
| 					jsonData, err := json.Marshal(status) | ||||
| 					currentsong, err := mpdConn.CurrentSong() | ||||
| 					if err != nil { | ||||
| 						log.Fatalln(err) | ||||
| 						c.Logger().Error(err) | ||||
| 					} | ||||
| 					err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s}", string(jsonData))) | ||||
| 					jsonStatus, err := json.Marshal(status) | ||||
| 					if err != nil { | ||||
| 						c.Logger().Error(err) | ||||
| 					} | ||||
| 					jsonCurrentSong, err := json.Marshal(currentsong) | ||||
| 					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))) | ||||
| 					if err != nil { | ||||
| 						c.Logger().Error(err) | ||||
| 					} | ||||
|  |  | |||
							
								
								
									
										172
									
								
								static/controls.js
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										172
									
								
								static/controls.js
									
										
									
									
										vendored
									
									
								
							|  | @ -1,7 +1,9 @@ | |||
| const API_URL = `${document.location.protocol}://${document.location.host}/api`; | ||||
| const API_URL = `${document.location.protocol}//${document.location.host}/api`; | ||||
| const VOLUME_STEP = 5; | ||||
| 
 | ||||
| // Get control elements
 | ||||
| 
 | ||||
| const connection_state = document.getElementById("connection-state"); | ||||
| const control_update_db = document.getElementById("control-update-db"); | ||||
| const control_previous = document.getElementById("control-previous"); | ||||
| const control_play_pause = document.getElementById("control-play-pause"); | ||||
|  | @ -13,18 +15,31 @@ const control_shuffle = document.getElementById("control-shuffle"); | |||
| const control_xfade = document.getElementById("control-xfade"); | ||||
| const control_xfade_minus = document.getElementById("control-xfade-minus"); | ||||
| const control_xfade_plus = document.getElementById("control-xfade-plus"); | ||||
| const control_volume = document.getElementById("control-volume"); | ||||
| const control_volume_up = document.getElementById("control-volume-up"); | ||||
| const control_volume_down = document.getElementById("control-volume-down"); | ||||
| const queue_table = document.querySelector("#queue tbody"); | ||||
| const control_track = document.getElementById("control-track"); | ||||
| const control_time = document.getElementById("control-time"); | ||||
| 
 | ||||
| // Add API calls to controls
 | ||||
| 
 | ||||
| control_update_db.addEventListener("click", e => { | ||||
|   fetch(`${API_URL}/update_db`); | ||||
|   console.log("Issuing database update") | ||||
|   fetch(`${API_URL}/update_db`).then(async r => { | ||||
|     if (r.status === 200) { | ||||
|       const job_id = await r.text(); | ||||
|       console.log(`Update started (Job ID: ${job_id})`) | ||||
|     } else { | ||||
|       console.error(`API returned ${r.status}: ${r.statusText}`) | ||||
|     } | ||||
|   }); | ||||
| }); | ||||
| control_previous.addEventListener("click", e => { | ||||
|   fetch(`${API_URL}/previous_track`); | ||||
| }); | ||||
| control_play_pause.addEventListener("click", e => { | ||||
|   if (e.target.innerText === "⏵︎") {  // Play
 | ||||
|   if (e.target.innerHTML === "⏵︎") {  // TODO: check is never true
 | ||||
|     fetch(`${API_URL}/pause`); | ||||
|   } else {  // Pause
 | ||||
|     fetch(`${API_URL}/play`); | ||||
|  | @ -40,10 +55,20 @@ control_progress.addEventListener("change", e => { | |||
|   fetch(`${API_URL}/seek/${e.target.value}`) | ||||
| }); | ||||
| control_repeat.addEventListener("click", e => { | ||||
|   if (e.target.innerHTML === "🔴 repeat") {  // TODO: check is never true
 | ||||
|     e.target.innerHTML = "🔘 repeat"; | ||||
|   } else { | ||||
|     e.target.innerHTML = "🔴 repeat"; | ||||
|   } | ||||
|   fetch(`${API_URL}/repeat`); | ||||
| }); | ||||
| control_shuffle.addEventListener("click", e => { | ||||
|   fetch(`${API_URL}/shuffle`); | ||||
|   if (e.target.innerHTML === "🔴 shuffle") {  // TODO: check is never true
 | ||||
|     e.target.innerHTML = "🔘 shuffle"; | ||||
|   } else { | ||||
|     e.target.innerHTML = "🔴 shuffle"; | ||||
|   } | ||||
|   fetch(`${API_URL}/random`); | ||||
| }); | ||||
| control_xfade_minus.addEventListener("click", e => { | ||||
|   // TODO: not yet implemented
 | ||||
|  | @ -53,46 +78,129 @@ control_xfade_plus.addEventListener("click", e => { | |||
|   // TODO: not yet implemented
 | ||||
|   fetch(`${API_URL}/xfade`); | ||||
| }); | ||||
| control_volume_up.addEventListener("click", e => { | ||||
|   const v = Math.min(parseInt(control_volume.value) + VOLUME_STEP, 100); | ||||
|   fetch(`${API_URL}/volume/${v}`); | ||||
|   control_volume.value = v; | ||||
| 
 | ||||
| }); | ||||
| control_volume_down.addEventListener("click", e => { | ||||
|   const v = Math.max(parseInt(control_volume.value) - VOLUME_STEP, 0); | ||||
|   fetch(`${API_URL}/volume/${v}`); | ||||
|   control_volume.value = v; | ||||
| }); | ||||
| control_volume.addEventListener("change", e => { | ||||
|   fetch(`${API_URL}/volume/${e.target.value}`); | ||||
| }); | ||||
| 
 | ||||
| // Create WebSocket connection.
 | ||||
| const socket = new WebSocket(`${document.location.protocol === "https" ? "wss" : "ws"}://${document.location.host}/ws`); | ||||
| const socket = new WebSocket(`${document.location.protocol === "https:" ? "wss" : "ws"}://${document.location.host}/ws`); | ||||
| 
 | ||||
| // Connection opened
 | ||||
| socket.addEventListener("open", (e) => { | ||||
|   socket.send("Hello Server!"); | ||||
| }); | ||||
| 
 | ||||
| // Listen for messages
 | ||||
| // Listen for messages and update UI state
 | ||||
| socket.addEventListener("message", (e) => { | ||||
|   console.log("Message from server"); | ||||
|   // Print out mpd response
 | ||||
|   console.log(`DEBUG: ${e.data}`);  // DEBUG
 | ||||
| 
 | ||||
|   const msg = JSON.parse(e.data); | ||||
|   if ("error" in msg.mpd_status) { | ||||
|     console.error(msg.mpd_status.error); | ||||
| 
 | ||||
|   if ("mpd_status" in msg) { | ||||
|     if (msg.mpd_status == null) { | ||||
|       connection_state.innerHTML = "❌ Disconnected";  // ✅ Check Mark Button
 | ||||
|     } else { | ||||
|       // print error if present
 | ||||
|       if ("error" in msg.mpd_status) { | ||||
|         console.error(msg.mpd_status.error); | ||||
|       } | ||||
| 
 | ||||
|       // update "Update DB" button
 | ||||
|       if ("updating_db" in msg.mpd_status) { | ||||
|         control_update_db.disabled = true; | ||||
|       } else { | ||||
|         if (control_update_db.disabled) { | ||||
|           console.log("Database update done.") | ||||
|         } | ||||
|         control_update_db.disabled = false; | ||||
|       } | ||||
| 
 | ||||
|       // update play/pause button
 | ||||
|       if ("state" in msg.mpd_status && msg.mpd_status.state === "play") { | ||||
|         control_play_pause.innerHTML = "⏸︎";  // Pause
 | ||||
|       } else { | ||||
|         control_play_pause.innerHTML = "⏵︎";  // Play
 | ||||
|       } | ||||
| 
 | ||||
|       // update playback time
 | ||||
|       if ("elapsed" in msg.mpd_status && "duration" in msg.mpd_status) { | ||||
|         const elapsed_hours = Math.floor(msg.mpd_status.elapsed / 3600); | ||||
|         const elapsed_minutes = Math.floor((msg.mpd_status.elapsed - elapsed_hours * 3600) / 60); | ||||
|         const elapsed_seconds = Math.floor(msg.mpd_status.elapsed - elapsed_hours * 3600 - elapsed_minutes * 60); | ||||
|         const duration_hours = Math.floor(msg.mpd_status.duration / 3600); | ||||
|         const duration_minutes = Math.floor((msg.mpd_status.duration - duration_hours * 3600) / 60); | ||||
|         const duration_seconds = Math.floor(msg.mpd_status.duration - duration_hours * 3600 - duration_minutes * 60); | ||||
|         control_time.value = `${elapsed_hours}:${elapsed_minutes.toString().padStart(2, '0')}:${elapsed_seconds.toString().padStart(2, '0')}/${duration_hours}:${duration_minutes.toString().padStart(2, '0')}:${duration_seconds.toString().padStart(2, '0')}`; | ||||
|       } | ||||
|       if ("elapsed" in msg.mpd_status) { | ||||
|         control_progress.value = msg.mpd_status.elapsed; | ||||
|       } | ||||
|       if ("duration" in msg.mpd_status) { | ||||
|         control_progress.max = msg.mpd_status.duration; | ||||
|       } | ||||
| 
 | ||||
|       // update repeat state
 | ||||
|       if ("repeat" in msg.mpd_status) { | ||||
|         if (msg.mpd_status.repeat === "1") { | ||||
|           control_repeat.innerHTML = "🔴 repeat"; // 🔴 Red Circle
 | ||||
|         } else { | ||||
|           control_repeat.innerHTML = "🔘 repeat"; // 🔘 Radio Button
 | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // update shuffle state
 | ||||
|       if ("random" in msg.mpd_status) { | ||||
|         if (msg.mpd_status.random === "1") { | ||||
|           control_shuffle.innerHTML = "🔴 shuffle"; // 🔴 Red Circle
 | ||||
|         } else { | ||||
|           control_shuffle.innerHTML = "🔘 shuffle"; // 🔘 Radio Button
 | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // update crossfade state
 | ||||
|       if ("xfade" in msg.mpd_status) { | ||||
|         control_xfade.value = msg.mpd_status.xfade; | ||||
|       } | ||||
| 
 | ||||
|       // update volume
 | ||||
|       if ("volume" in msg.mpd_status) { | ||||
|         control_volume.value = msg.mpd_status.volume; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if ("updating_db" in msg.mpd_status) { | ||||
|     control_update_db.disable(); | ||||
|   } else { | ||||
|     control_update_db.enable(); | ||||
|   // update song info
 | ||||
|   if ("mpd_current_song" in msg && msg.mpd_current_song != null) { | ||||
|     if ("Artist" in msg.mpd_current_song && "Title" in msg.mpd_current_song) { | ||||
|       control_track.value = `${msg.mpd_current_song.Artist} - ${msg.mpd_current_song.Title}` | ||||
|     } else { | ||||
|       control_track.value = msg.mpd_current_song.file; | ||||
|     } | ||||
|   } | ||||
|   if ("state" in msg.mpd_status && msg.mpd_status.state === "play") { | ||||
|     control_play_pause.innerText = "⏸︎";  // Pause
 | ||||
|   } else { | ||||
|     control_play_pause.innerText = "⏵︎";  // Play
 | ||||
|   } | ||||
|   if ("elapsed" in msg.mpd_status) { | ||||
|     control_progress.value = msg.mpd_status.elapsed; | ||||
|   } | ||||
|   if ("duration" in msg.mpd_status) { | ||||
|     control_progress.max = msg.mpd_status.duration; | ||||
|   } | ||||
|   if ("repeat" in msg.mpd_status) { | ||||
|     control_repeat.checked = msg.mpd_status.repeat; | ||||
|   } | ||||
|   if ("random" in msg.mpd_status) { | ||||
|     control_shuffle.checked = msg.mpd_status.random; | ||||
|   } | ||||
|   if ("xfade" in msg.mpd_status) { | ||||
|     control_xfade.value = msg.mpd_status.xfade; | ||||
| 
 | ||||
|   if ("mpd_error" in msg) { | ||||
|     console.error(`MPD Error: ${msg.mpd_error}`) | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| // Request MPD status every second
 | ||||
| window.setInterval(() => { | ||||
|   if (socket.readyState === socket.OPEN) { | ||||
|     socket.send("#status"); | ||||
|     connection_state.innerHTML = "✅ Connected";  // ❌ Cross Mark
 | ||||
|   } else { | ||||
|     connection_state.innerHTML = "❌ Disconnected";  // ✅ Check Mark Button
 | ||||
|   } | ||||
| }, 1000); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <html lang="en" xmlns="http://www.w3.org/1999/html"> | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <title>Sanic</title> | ||||
|  | @ -9,13 +9,14 @@ | |||
| <body> | ||||
| <main> | ||||
|   <div id="nav"> | ||||
|     <div> | ||||
|     <div id="control-admin"> | ||||
|       <button id="connection-state">❌ Disconnected</button> <!-- ❌ Cross Mark --> | ||||
|       <button>Login</button> | ||||
|       <button>Config</button> | ||||
|       <button id="control-update-db" disabled="disabled">Update DB</button> | ||||
|     </div> | ||||
|     <div> | ||||
|       <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 --> | ||||
|  | @ -27,24 +28,38 @@ | |||
|       </div> | ||||
|     </div> | ||||
|     <div> | ||||
|       <div> | ||||
|       <div class="spaced"> | ||||
|         <button id="control-repeat">🔘 repeat</button> | ||||
|         <button id="control-shuffle">🔘 shuffle</button> | ||||
|       </div> | ||||
|       <div class="spaced"> | ||||
|         <label for="control-xfade">xfade</label> | ||||
|         <div> | ||||
|           <div> | ||||
|             <label for="control-repeat">repeat</label> | ||||
|             <input type="checkbox" id="control-repeat" name="repeat" /> | ||||
|           </div> | ||||
|           <div> | ||||
|             <label for="control-shuffle">shuffle</label> | ||||
|             <input type="checkbox" id="control-shuffle" name="shuffle" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div> | ||||
|           <label for="control-xfade">xfade</label> | ||||
|           <button id="control-xfade-minus">➖</button> | ||||
|           <input type="number" id="control-xfade" name="xfade" value="00" /> | ||||
|           <button id="control-xfade-plus">➕</button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <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> | ||||
|     <div> | ||||
|       <p>Now playing:</p> | ||||
|       <div> | ||||
|         <label for="control-track">Track:</label> | ||||
|         <input type="text" id="control-track" name="track" disabled="disabled" /> | ||||
|       </div> | ||||
|       <div> | ||||
|         <label for="control-time">Time:</label> | ||||
|         <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> | ||||
|   </div> | ||||
|   <div id="queue"> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| /* #################### */ | ||||
| /* ####### main ####### */ | ||||
| /* #### structure ##### */ | ||||
| /* #################### */ | ||||
| 
 | ||||
| html, body { margin: 0; height: 100%; } | ||||
|  | @ -37,6 +37,21 @@ table { | |||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| #control-admin { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
| 
 | ||||
| #sanic-logo { | ||||
|   max-width: 80%; | ||||
|   max-height: 80%; | ||||
| } | ||||
| 
 | ||||
| .spaced { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
| } | ||||
| 
 | ||||
| /* #################### */ | ||||
| /* ###### debug ####### */ | ||||
| /* #################### */ | ||||
|  | @ -62,3 +77,58 @@ div { | |||
| #control-xfade[type=number] { | ||||
|   -moz-appearance: textfield; | ||||
| } | ||||
| 
 | ||||
| #control-xfade { | ||||
|   width: 2em; | ||||
| } | ||||
| 
 | ||||
| #control-previous, | ||||
| #control-play-pause, | ||||
| #control-stop, | ||||
| #control-next { | ||||
|   width: 2.5em; | ||||
|   height: 2.5em; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| #control-track { | ||||
|   transform: translateX(100%); | ||||
|   -moz-transform: translateX(100%); | ||||
|   -webkit-transform: translateX(100%); | ||||
|   animation: scroll-left 20s linear infinite; | ||||
|   -moz-animation: scroll-left 2s linear infinite; | ||||
|   -webkit-animation: scroll-left 2s linear infinite; | ||||
| } | ||||
| 
 | ||||
| @keyframes scroll-left { | ||||
|   0% { | ||||
|     transform: translateX(100%); | ||||
|     -moz-transform: translateX(100%); | ||||
|     -webkit-transform: translateX(100%); | ||||
|   } | ||||
|   100% { | ||||
|     transform: translateX(-100%); | ||||
|     -moz-transform: translateX(-100%); | ||||
|     -webkit-transform: translateX(-100%); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @-moz-keyframes scroll-left { | ||||
|   0% { | ||||
|     -moz-transform: translateX(100%); | ||||
|   } | ||||
|   100% { | ||||
|     -moz-transform: translateX(-100%); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @-webkit-keyframes scroll-left { | ||||
|   0% { | ||||
|     -webkit-transform: translateX(100%); | ||||
|   } | ||||
|   100% { | ||||
|     -webkit-transform: translateX(-100%); | ||||
|   } | ||||
| } | ||||
| */ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue