// set all the global stuff let ip = URL.parse(document.URL).host; let alertTime = 2; let adminPass = ""; let justSkipped = false; let justChangedSetting = false; const ERR_NO_ADMIN = 401; const VALID_FILE_EXT = ["mp3","flac","wav"]; let playlistTimeTimer=null; let playlistElapsedSeconds=0; let playlistSongLength=-1; let currentlyPlaying = false; const params = new URLSearchParams(location.search); let darkmodetemp = getCookie("darkmode"); if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { darkmodetemp = "true"; } if(darkmodetemp === "") { darkmodetemp = params.get("darkmode") } if (darkmodetemp === "true") { // i know this is gonna cause weird blinking // maybe the dark mode function should be loaded before any content, would that work? // NEW JS FILE ????? exciting stuff // im thinking a few new js files // you know like good design separating stuff // yeah but i need the getCookie function in both the darkmode.js and this one, so im gonna make a // getcookie.js toggleDark("None"); } async function alertText(text="Song Added!") { alertbox = document.getElementById("alert"); alertbox.innerHTML = text; await new Promise(r => setTimeout(r, alertTime*1000)); if (alertbox.innerHTML == text) { alertbox.innerHTML = "" } } // a lot of this is kinda waffly because i was trying to get // it to return the right stuff and javascript is asyrcronouse (boo) async function getFromServer(bodyInfo, source="", secure=false, password=adminPass) { try{ if (bodyInfo != null) { // the currently set password is always included in every request bodyInfo["password"] = password; } let href = ""; if(secure) { href = "https://"+ip+"/" + source; } else { href = "http://"+ip+"/" + source; } const response = await fetch(href, { method: "POST", body: JSON.stringify(bodyInfo), headers: { "Content-type": "application/json; charset=UTF-8" } }); let data = await response.json(); // original json if (response.status == ERR_NO_ADMIN) { // im suprised i didn't comment on this already but this is kinda lame desing // its not wrong but you know // it is easy which i like alertText("Error: Admin restricted action") } else if(!response.ok){ throw new Error(data.error); alertText("Error: "+data.error); } // we add some information from the response just in case it is needed data["ok"] = response.ok; data["status"] = response.status; // console.log(data); return await data; } catch(e) { // console.log("error print here:"); // console.log(e); if (e.toString().includes("TypeError: Failed to fetch")){ alertText("Error: Can't Connect to Server") } else { alertText(e); } const response=null; return response; } } //cookie reader is taken from internet because cookies ae too complicated for me //i still understand how it works though promise just i see no reason to write this from scratch function getCookie(cname) { let name = cname + "="; let decodedCookie = decodeURIComponent(document.cookie); let ca = decodedCookie.split(';'); for(let i = 0; i "; document.getElementById("playlist-mode").style.display = "block"; document.getElementById("songlist-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "none"; generateVisualPlaylist(); } else if (buttonType == "se") { //SearchMode button clearInterval(playlistTimeTimer); document.getElementById("songlist").innerHTML = "

Search to find songs!

"; document.getElementById("playlist").innerHTML = ""; document.getElementById("playlist-mode").style.display = "none"; document.getElementById("songlist-mode").style.display = "block"; document.getElementById("settings-mode").style.display = "none"; } else if (buttonType == "st") { //Settings button clearInterval(playlistTimeTimer); document.getElementById("songlist").innerHTML = ""; document.getElementById("playlist").innerHTML = ""; document.getElementById("playlist-mode").style.display = "none"; document.getElementById("songlist-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "block"; checkSettings() } else if (buttonType == "pm") { //Partymode toggle (in settings) let response = await getFromServer({setting: "partymode-toggle"}, "settings") if(response.ok) { justChangedSetting = true; checkSettings(); } else { // dont think anything is needed here } } else { alertText("Error: You pushed a button that does not exist"); } } function searchSongsEnter(e) { if (e.keyCode == 13) { searchSongs(document.getElementById("songsearch").value) } } async function searchSongs(searchTerm){ document.getElementById("songlist").innerHTML = "" let fetchResults = await getFromServer({search:searchTerm},"search").then(); let searchResults = fetchResults.data; //generate the visual song list for(var fileName in searchResults) { let currentSongInJSON = searchResults[fileName] let newItem = document.createElement("div"); newItem.className = "item"; newItem.id = fileName; newItem.tabIndex = 0; let image = document.createElement("img"); try { if (currentSongInJSON["art"] == null) { throw "no image lolz" } image.src = currentSongInJSON["art"]; } catch(err){ image.src = "/static/images/placeholder.png"; } image.id = String(fileName)+" image"; let head3 = document.createElement("h3"); head3.innerText = currentSongInJSON["title"]; let head4 = document.createElement("h4"); head4.innerText = currentSongInJSON["artist"]; newItem.appendChild(image); newItem.appendChild(head3); newItem.appendChild(head4); // I like this concept but i'm leaving it out for now if(currentSongInJSON.lossless === 1) { let losslesstag = document.createElement("p"); losslesstag.textContent = "Ⓛ"; losslesstag.classList.add("lossless-tag"); newItem.appendChild(losslesstag); } document.getElementById("songlist").appendChild(newItem); } if (JSON.stringify(searchResults)==JSON.stringify({})) { //display error if no results document.getElementById("songlist").innerHTML = "

We might not have that one...

"; } } function alertTimeEnter(e){ if (e.key == "Enter") { e.preventDefault(); alertTimeSet(document.getElementById("alerttimetextbox").value); } } function alertTimeSet(time) { alertTime = time; document.cookie = "alertTime="+alertTime+"; path=/;" alertText("Alerts stay on screen for " + alertTime.toString() + " seconds") } function qrCodeGenerate() { let tempURL = "http://" + document.location.href.split("/")[2] + "/?ip=" + ip; document.getElementById("qrcode").innerHTML = ""; // get the current foreground and background let dark = window.getComputedStyle(document.body).getPropertyValue("--text-color"); let light = window.getComputedStyle(document.body).getPropertyValue("--bg-main"); new QRCode(document.getElementById("qrcode"), { text: tempURL, width: 256, height: 256, colorDark : dark, colorLight : light, correctLevel : QRCode.CorrectLevel.H }); } async function displayElapsedPlaylistTime(elapsed=0,length=-1) { if(currentlyPlaying) { if(Math.floor(elapsed) > Math.floor(length) && typeof length === "number" && typeof elapsed === "number"){ // console.log("somethingShouldBeHappening") playlistElapsedSeconds = 0; generateVisualPlaylist(); } let mins = Math.floor(elapsed/60); let secs = Math.floor(elapsed%60); let durMins = Math.floor(length/60); let durSecs = Math.floor(length%60); let timeLeft = document.getElementById("elapsed-time-display"); if(mins > durMins) { mins = durMins; if(secs > durSecs) { secs = durSecs; } } timeLeft.innerHTML = mins.toString() +":"+ secs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}) + "/"+ durMins.toString()+":"+durSecs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}); playlistElapsedSeconds++; } } async function updateSingleSetting(data) { let toBeChanged = data["settingToChange"]; if (toBeChanged === "partymode") { document.getElementById("partymode-button").textContent = data["newData"]; } else if (toBeChanged === "perms") { let currentAdminPerms = data["newData"]; document.getElementById("addsongsettingcheckbox").checked = currentAdminPerms["AS"]; document.getElementById("skipsongsettingcheckbox").checked = currentAdminPerms["SK"]; document.getElementById("playpausesettingcheckbox").checked = currentAdminPerms["PP"]; document.getElementById("partymodesettingcheckbox").checked = currentAdminPerms["PM"]; document.getElementById("volumechangesettingcheckbox").checked = currentAdminPerms["VOL"]; document.getElementById("duplicateallowesettingcheckbox").checked = currentAdminPerms["DUP"]; } else if (toBeChanged === "volume") { document.getElementById("volumerange").value = data["newData"]; } } async function checkSettings(skipServer=false) { //check client stuff first so if the server doesn't exist it can still be changed and seen // if (ip.slice(-5)=="19054") { // // don't show the port if it is the default // document.getElementById("iptextbox").value = ip.slice(0,-6) // } else { // document.getElementById("iptextbox").value = ip; // } qrCodeGenerate() document.getElementById("alerttimetextbox").value = alertTime partyButtonState = document.getElementById("partymode-button").innerHTML; let nodeList = document.getElementById("admincheckholder").children // temporary for (let i=0; i"; data = await getFromServer(null, "playlist"); playlist = data["data"]["playlist"]; currentlyPlaying = data["data"]["playingState"] playlist = Object.values(playlist).map(obj => { const filename = Object.keys(obj)[0]; // Get the filename const songData = obj[filename]; // Get the song metadata return { filename, ...songData }; // Merge filename with song data }); if (playlist.length==0){ clearInterval(playlistTimeTimer); document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..." } else { if (conditions==="skip-button") { playlist.shift() if (playlist.length==0){ document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..." } } for (let i in playlist) { let fileName = playlist[i]["filename"] let newItem = document.createElement("div"); newItem.className = "item"; newItem.id = fileName; newItem.tabIndex = 0; let image = document.createElement("img"); try { if (playlist[i]["art"] == null) { throw "no image lolz" } image.src = playlist[i]["art"]; } catch(err){ image.src = "/static/images/placeholder.png"; } image.id = String(fileName)+" image"; let head3 = document.createElement("h3"); head3.innerText = playlist[i]["title"]; let head4 = document.createElement("h4"); head4.innerText=playlist[i]["artist"]; let head5 = document.createElement("h5"); let timeLeft =document.createElement("h5"); timeLeft.style.fontWeight = 100; if(i== 0) { // they can all have the text, doesn't really matter, but only the first one // should get the ids since its the one we want to mess with head5.id = "playing-indicator-text"; timeLeft.id = "elapsed-time-display"; } let textdiv = document.createElement("div") textdiv.className="text" newItem.appendChild(image); textdiv.appendChild(head3); textdiv.appendChild(head4); textdiv.appendChild(timeLeft); textdiv.appendChild(head5); newItem.appendChild(textdiv); document.getElementById("playlist").appendChild(newItem); try { if (i == 0) { // Only the first song in the loop gets a time head5.innerHTML="Playing"; if ((conditions != "skip-button")) { playlistElapsedSeconds = playlist[0]["time"]; playlistSongLength = playlist[0]["length"]; displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength); clearInterval(playlistTimeTimer); } } }catch(err){ // i dont know why there's a try catch here but i'm leaving it i dont want to break something console.error(err) } } playlistTimeTimer = setInterval(() => { displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength); },1000) } } async function submitSong(songid) { let returncode = await getFromServer({song: songid}, "songadd"); if(returncode["status"] === ERR_NO_ADMIN) { // right now the error is alerted in getFromServer, maybe will change that } else if(returncode["status"]!==200) { alertText("That song's already in the queue! Hang on!") } else { alertText("Added to Queue"); } } function checkWhatSongWasClicked(e) { if(e.type == "click" || e.key == "Enter") { itemId = e.srcElement.id; if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) { itemId = itemId.slice(0,-6) } let filenameSep = itemId.split('.') //i feel like later kristy won't apreciate this //one of my files was "file.MP3" so it didn't work //windows be like if (VALID_FILE_EXT.includes(filenameSep[filenameSep.length-1].toLowerCase())) { submitSong(itemId); } } } function toggleDark(e) { let x = document.getElementById("test-body").classList if (!(x.contains("dark-mode"))) { document.cookie = "darkmode=true; path=/;"; document.getElementById("darkmode-button").innerHTML = "On"; x.add("dark-mode"); } else { document.cookie = "darkmode=false; path=/;"; document.getElementById("darkmode-button").innerHTML = "Off"; x.remove("dark-mode"); } qrCodeGenerate(); } // async function sha256(message) { // // Encode the message as UTF-8 // const msgBuffer = new TextEncoder().encode(message); // // Hash the message // const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); // // Convert ArrayBuffer to hex string // const hashArray = Array.from(new Uint8Array(hashBuffer)); // const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // return hashHex; // } async function adminPassEnter(e) { if (e.key == "Enter") { e.preventDefault(); let enteredpass = document.getElementById("adminpasswordbox").value; if(enteredpass === "") { adminPass = ""; // an empty pass is technically meant to represent not having one // this isn't stritly necesarry but i dont wanna break anything that might depend on this being true } else { adminPass= await sha256(document.getElementById("adminpasswordbox").value); } alertText("Admin Password Updated"); } } async function submitPerms(e) { let tempData = {} tempData["PP"] = document.getElementById("playpausesettingcheckbox").checked; tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked; tempData["AS"] = document.getElementById("addsongsettingcheckbox").checked; tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked; tempData["VOL"] = document.getElementById("volumechangesettingcheckbox").checked; tempData["DUP"] = document.getElementById("duplicateallowesettingcheckbox").checked; let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings"); if (!(returncode["ok"])) { // if you aren't allowed to check the box then toggle it again // its not perfect if you spam click, but it gets the point across to the user let clickedBox = e.srcElement; clickedBox.checked = !clickedBox.checked; } else { justChangedSetting = true; } } async function clearPlaylist() { let returncode = await getFromServer({control:"clear"},"controls"); if(returncode == ERR_NO_ADMIN || returncode == null) { // alertText("Admin Restricted ") // there's an admin restrict alert built into getFromServer } else { alertText("Playlist Cleared!"); } } let optionslist = [] //sets all de stuff for buttons document.addEventListener('keydown', function(e){ if (e.key == "/"){ document.getElementById("title").scrollIntoView(); document.getElementById("songsearch").select(); e.preventDefault() }}) document.getElementById("playlist-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "none"; document.getElementById("volumerange").onchange = async function(e) { // there is no reason for this not to be a defined function // FIX THIS let returnValue = await getFromServer({setting:"volume",level:e.target.value}, "settings") if (returnValue["status"] == ERR_NO_ADMIN) { // alertText("Error: Admin restricted action"); // there's an admin restrict alert built into getFromServer // i wanna put the volume slider back to where it was but idk a good way to keep the previous volume checkSettings(false); } else if (returnValue["data"]["volumePassed"] !=0) { // i forgot about this, i had to do this because it confused the crap out of me one time // vlc doesn't let you change the volume of nothing, which makes sense if you think about it alertText("Nothing is playing") document.getElementById("volumerange").value = -1 } else if (this.value == 0) { alertText("The volume is now set to 0 (Pause?)") } else { alertText("The volume is now set to " + this.value.toString()) } } //bit of a cheat code for clearing the alerts when they don't clear normally document.getElementById("title").addEventListener('click',function(){document.getElementById("alert").innerHTML = ""}) document.getElementById("settings-button").addEventListener('click',function(){controlButton("st")}); document.getElementById("play-pause-button").addEventListener('click', function(){controlButton("pp")}); document.getElementById("playlist-button").addEventListener('click', function(){controlButton("pl")}); document.getElementById("search-button").addEventListener('click', function(){controlButton("se")}); document.getElementById("skip-button").addEventListener('click',function(){controlButton("sk")}); document.getElementById("go-search").addEventListener('click', function(){searchSongs(document.getElementById("songsearch").value)}) document.getElementById("songsearch").addEventListener('keydown', function(e){searchSongsEnter(e)}); // document.getElementById("iptextbox").addEventListener('keydown', function(e){ipSetEnter(e)}); document.getElementById("alerttimetextbox").addEventListener('keydown', function(e){alertTimeEnter(e)}); document.getElementById("adminpasswordbox").addEventListener('keydown',function(e){adminPassEnter(e)}); document.getElementById("admincheckholder").addEventListener('click',function(e){submitPerms(e)}); document.getElementById("partymode-button").addEventListener('click',function(){controlButton("pm")}) document.getElementById("darkmode-button").addEventListener('click',function(){toggleDark()}) document.getElementById("clear-button").addEventListener('click',function(){clearPlaylist()}) //sets the fact that clicking a song needs to return its id to the function to find it document.getElementById("songlist").addEventListener('keydown', function(e){checkWhatSongWasClicked(e)}); document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)}); //makes the controls look mostly normal on all screens, best solution i could find, idk man // replaced this with "transform" css stuff // let tempWidth = document.getElementById('controls').clientWidth; // document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px"; // saving the cookies (don't tell the EU) document.cookie = "ip="+ip+"; path=/;" alertTime = getCookie("alertTime") document.getElementById("alerttimetextbox").value = alertTime if (alertTime == "") { alertTime = 2; document.cookie = "alertTime="+alertTime+"; path=/;" } // this is the code that makes the qr code at the very start qrCodeGenerate() // socket testing stuff socket = io("http://"+ip,{ reconnectionAttemps: 5, timeout: 10000, }); socket.on("songAdd", function(data) { // console.log("recieved data from songAdd"); // console.log(data); addToPlaylist(data); }) socket.on("timeUpdate", function(data) { // console.log("recieved data from timeUpdate"); // console.log(data); playlistElapsedSeconds = data["elapsedTime"]; currentlyPlaying = data["playingState"] }); socket.on("skipSong",() => { if(justSkipped === false) { skipInPlaylist(); } else { justSkipped = false; } }) socket.on("settingsChange",(data) => { // console.log(data); if(justChangedSetting) { // console.log("working"); justChangedSetting = false; } else { // checkSettings(); updateSingleSetting(data); } });