Moved Static Client

This commit is contained in:
Kristy Fournier 2026-03-04 15:58:16 -05:00
parent 8d78960f0b
commit 4d3c707301
17 changed files with 0 additions and 0 deletions

134
Server/Client/ext/popper.js Normal file
View file

@ -0,0 +1,134 @@
function Pop() {
// var cssRuleFile = "/src/css/style.css"; // will be the link once this css file became available online
var cssRuleFile = "https://cookieconsent.popupsmart.com/src/css/style.css"; // will be the link once this css file became available online
let lnk = document.createElement("link");
lnk.setAttribute("rel", "stylesheet");
lnk.setAttribute("type", "text/css");
lnk.setAttribute("href", cssRuleFile);
document.getElementsByTagName("head")[0].appendChild(lnk);
let styl = "undefined";
var conDivObj;
var fadeInTime = 10; // If needed could be served as an customizable option to the user
var fadeOutTime = 10;
let cookie = {
name: "cookieconsent_status",
path: "/",
expiryDays: 365 * 24 * 60 * 60 * 5000,
};
let content = {
/// Add a field for link color
message:
"This website uses cookies to ensure you get the best experience on our website.",
btnText: "Got it!",
mode: " banner bottom",
theme: " theme-classic",
palette: " palette1",
link: "Learn more",
href: "https://www.cookiesandyou.com",
target: "_blank",
};
let createPopUp = function () {
console.log(content);
if (typeof conDivObj === "undefined") {
conDivObj = document.createElement("DIV");
conDivObj.style.opacity = 0;
conDivObj.setAttribute("id", "spopupCont");
}
conDivObj.innerHTML =
'<div id="poper" class="window ' +
content.mode +
content.theme +
content.palette +
'"><span id="msg" class="message">' +
content.message +
'<a id="plcy-lnk" class="policylink" href="' +
content.href +
'"' +
" target=" +
content.target +
">" +
content.link +
'</a></span><div id="btn" class="compliance"><a id="cookie-btn" class="spopupbtnok" >' +
content.btnText +
'</a></div><span class="credit"><a href="https://popupsmart.com" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 60 60" fill="currentColor"><path id="popupsmart" d="M56.627,12.279a22.441,22.441,0,0,0-9.549-9.074c-4.122-2.088-8.951-3.2-15.722-3.2H28.644c-6.769,0-11.6,1.112-15.72,3.2a22.425,22.425,0,0,0-9.551,9.072C1.174,16.191,0,20.783,0,27.214v5.578c0,6.434,1.173,11.024,3.373,14.934A22.412,22.412,0,0,0,12.924,56.8c4.12,2.094,8.949,3.206,15.72,3.206h2.711c6.771,0,11.6-1.112,15.72-3.206a22.427,22.427,0,0,0,9.551-9.072c2.2-3.91,3.373-8.5,3.373-14.934V27.216C60,20.78,58.827,16.19,56.627,12.279ZM30,45.006c-.237,0-.473-.005-.708-.015l-.211-.012c-.14-.008-.28-.019-.419-.031-.123-.011-.245-.022-.367-.036l-.191-.024a14.979,14.979,0,0,1-2.672-.59V44.3a14.861,14.861,0,0,1-6.294-3.955,1.406,1.406,0,1,0-2.036,1.94,17.648,17.648,0,0,0,8.33,4.944v.354a5.214,5.214,0,1,1-10.428,0V30.046c0-.013,0-.026,0-.039a15,15,0,1,1,15,15Z" transform="translate(0 -0.005)"></path></svg><span>Powered by Popupsmart</span></a></span></div>';
document.body.appendChild(conDivObj);
fadeIn(conDivObj);
document
.getElementById("cookie-btn")
.addEventListener("click", function () {
saveCookie();
fadeOut(conDivObj);
});
};
let fadeOut = function (element) {
var op = 1;
var timer = setInterval(function () {
if (op <= 0.1) {
clearInterval(timer);
conDivObj.parentElement.removeChild(conDivObj);
}
element.style.opacity = op;
element.style.filter = "alpha(opacity=" + op * 100 + ")";
op -= op * 0.1;
}, fadeOutTime);
};
let fadeIn = function (element) {
var op = 0.1;
var timer = setInterval(function () {
if (op >= 1) {
clearInterval(timer);
}
element.style.opacity = op;
element.style.filter = "alpha(opacity=" + op * 100 + ")";
op += op * 0.1;
}, fadeInTime);
};
let checkCookie = function (key) {
var keyValue = document.cookie.match("(^|;) ?" + key + "=([^;]*)(;|$)");
return keyValue ? true : false;
};
let saveCookie = function () {
var expires = new Date();
expires.setTime(expires.getTime() + cookie.expiryDays);
document.cookie =
cookie.name +
"=" +
"ok" +
";expires=" +
expires.toUTCString() +
"path=" +
cookie.path;
};
this.init = function (param) {
if (checkCookie(cookie.name)) return;
if (typeof param === "object") {
if ("ButtonText" in param) content.btnText = param.ButtonText;
if ("Mode" in param) content.mode = " " + param.Mode;
if ("Theme" in param) content.theme = " " + param.Theme;
if ("Palette" in param) content.palette = " " + param.Palette;
if ("Message" in param) content.message = param.Message;
if ("LinkText" in param) content.link = param.LinkText;
if ("Location" in param) content.href = param.Location;
if ("Target" in param) content.target = param.Target;
if ("Time" in param)
setTimeout(function () {
createPopUp();
}, param.Time * 1000);
else createPopUp();
}
};
}
window.start = new Pop();

1
Server/Client/ext/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
Server/Client/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

133
Server/Client/index.html Normal file
View file

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Jukebox Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<link rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.json" />
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js"></script>
<!-- above allows use of sha256() on http -->
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body id="test-body">
<!--Cookie Popup(does it matter if im not tracking them? i have no idea)-->
<script type="text/javascript" src="/ext/popper.js"></script><script> window.start.init({Palette:"palette2",Mode:"floating left",Theme:"classic",LinkText:" Learn More!",Message:"This website uses cookies to save necessary data to your device, and no tracking is performed.",Time:"0",})</script>
<div class="intro">
<h1 id="title">Jukebox Remote</h1>
<p>Add songs to the shared playlist below!</p>
</div>
<h1 class="alert" id="alert"></h1>
<!-- this is the main body when PICKING songs to add to the playlist,
changes visibility with JS-->
<div class="songlist-mode" id="songlist-mode">
<div class="searchbox-holder">
<input type="text" autocomplete="off" placeholder="Search" id="songsearch" enterkeyhint="Search" class="searchbox"><button class="go-search" id="go-search">Go!</button>
</div>
<div class="songlist" id="songlist">
<h1 style="margin-top: 0px;">Search to find songs!</h1>
<!-- Placeholder for the song items
These are generated using javascript for search
<div class="item">
<img src="placeholder.png"></img>
<h3><span>Song title</span></h3>
<h4>Artist</h4>
</div>
-->
</div>
</div>
<!-- this is the opposite of the thing described above (looking at songs in queue) -->
<div class="playlist-mode" id="playlist-mode">
<div class="playlist" id="playlist">
<h1 id="playlist-alert"></h1>
<!-- The template for playlist items
<div class="item">
<img src="placeholder.png"></img>
<h3>Song title</h3>
<h4>Artist</h4>
<h5>Playing</h5>
</div>-->
</div>
</div>
<!-- Because settings involves no generation ( all the settings are known), it is filled out always, just dissapears-->
<div class="settings-mode" id="settings-mode">
<div class="settings" id="settings">
<h1>Client Settings (Saved to device)</h1>
<div class="item">
<h2 for="darkmode-button">Dark Mode:</h2>
<p class="italic">Opposite of light mode</p>
<button title="darkmode-button" id="darkmode-button">Off</button>
</div>
<div class="item">
<h2 for="iptextbox">Server IP:</h2>
<p class="italic">IP of the device running the song server</p>
<input title="iptextbox" style="width:200px" type="text" id="iptextbox" enterkeyhint="Done">
</div>
<div class="item">
<h2 for="alerttimetextbox">Alert Time:</h2>
<p class="italic">How long alerts stay on screen for (seconds)</p>
<input title="alerttimetextbox" style="width:50px" type="text" id="alerttimetextbox" value="2" enterkeyhint="Done">
</div>
<h1>Server Settings (Saved to server)</h1>
<div class="item">
<h2 for="partymode-button">Party Mode:</h2>
<p class="italic">Add random songs to the queue when it is about to be empty</p>
<button title="partymode-button" id="partymode-button">N/A</button>
</div>
<div class="item">
<h2 for="volumerange">Volume:</h2>
<p class="italic">Volume of the music</p>
<input type="range" min="0" max="100" step="1" title="volumerange" id="volumerange">
</div>
<div class="item">
<h2>Share the remote:</h2>
<p class="italic">This shares the IP and URL you are currently connected to</p>
<div id="qrcode" alt="QR code to the remote URL"></div>
</div>
<h1>Admin Settings</h1>
<p class="italic">Note: Admin password must have been set from the server</p>
<div class="item">
<h2>Admin Password:</h2>
<p class="italic">Enter to use admin restricted functions</p>
<input placeholder="Wordpass12" type="password" title="Admin password box" id="adminpasswordbox" enterkeyhint="done">
</div>
<div class=item>
<h2>Fine action control:</h2>
<p class="italic">A check means that action is avalible to everyone</p>
<div id="admincheckholder">
<input type="checkbox" title="addsongcheck" id="addsongsettingcheckbox"><label for="addsongsettingcheckbox">Add songs to queue</label><br>
<input type="checkbox" title="skipsongcheck" id="skipsongsettingcheckbox"><label for="skipsongsettingcheckbox">Skip songs</label><br>
<input type="checkbox" title="playpausecheck" id="playpausesettingcheckbox"><label for="playpausesettingcheckbox">Play/pause</label><br>
<input type="checkbox" title="partymodecheck" id="partymodesettingcheckbox"><label for="partymodesettingcheckbox">Toggle Party Mode</label><br>
<input type="checkbox" title="volumechangecheck" id="volumechangesettingcheckbox"><label for="volumechangesettingcheckbox">Change volume</label><br>
<input type="checkbox" title="duplicateallowcheck" id="duplicateallowesettingcheckbox"><label for="duplicateallowsettingcheck">Add duplicate songs</label><br>
</div>
</div>
<div class="item">
<h2>Clear the playlist</h2>
<p class="italic" title="With PartyMode enabled, the second song will be added back randomly">Wipe the playlist, except the currently playing song*</p>
<button id="clear-button">Clear Playlist</button>
</div>
<p class="versionNumber">PartyJukebox is under an <a href="https://github.com/kristy-fournier/PartyJukebox/blob/main/LICENSE.md" target="_blank">AGPLV3</a> liscense. You can access the source code <a href=https://github.com/kristy-fournier/PartyJukebox target="_blank">here</a>.</p>
</div>
</div>
<!--All the buttons are down here but settings is just doing its own thing-->
<div id="controls" class="controls">
<img tabindex=0 class="control-button" id="playlist-button" src="./images/playlist.png" alt="Playlist"></img>
<img tabindex=0 class="control-button" id="play-pause-button" src="./images/play-pause.png" alt="Play pause"></img>
<img tabindex=0 class="control-button" id="skip-button" src="./images/skip.png" alt="Skip"></img>
<img tabindex=0 class="control-button" id="search-button" src="./images/search.png" alt="Search"></img>
</div>
<div class="settings-button-holder">
<img tabindex=0 class="settings-button control-button" id="settings-button" src="./images/settings.png" alt="settings"></img>
</div>
<script src="/ext/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="scripts.js"></script>
</body>
</html>

View file

@ -0,0 +1,35 @@
{
"name": "Jukebox Remote",
"short_name": "Jukebox Remote",
"description": "Controller for the PartyJukebox server app.",
"start_url": "index.html",
"display": "standalone",
"background_color": "#eeeeee",
"theme_color": "#eeeeee",
"orientation": "portrait-primary",
"icons": [
{
"src": "/favicon.ico",
"type": "image/ico", "sizes": "100x100"
},
{
"src":"images/Icon-144.png",
"type": "image/png",
"sizes": "144x144"
}
],
"screenshots": [
{
"src": "images/Screenshot-Main-Desktop.png",
"sizes" : "1919x1199",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "images/Screenshot-Main-Mobile.png",
"type": "image/png",
"sizes": "485x859",
"form_factor": "narrow"
}
]
}

730
Server/Client/scripts.js Normal file
View file

@ -0,0 +1,730 @@
// set all the global stuff
let ip;
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 (is the ip set?)")
} else {
alertText("Error: " + 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 <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
//someone more organised than me would have set all these html elements to variables so they dont have to get them 50 times
// also someone who likes things not being dumb more than me would have separated the client and server buttons
async function controlButton(buttonType) {
if (buttonType == "pp") { // Play-Pause button
let result = await getFromServer({control: "play-pause"}, "controls");
// console.log(result);
currentlyPlaying = result["data"]["playingState"];
} else if (buttonType == "sk") { // Skip button
// clearInterval(playlistTimeTimer);
let returnCode = await getFromServer({control: "skip"}, "controls");
// console.log(returnCode["ok"])
if(returnCode["ok"]) {
if (document.getElementById("playlist-mode").style.display == "block") {
skipInPlaylist();
playlistElapsedSeconds = 0;
justSkipped = true;
}
}
} else if (buttonType == "pl") { // Playlist button
clearInterval(playlistTimeTimer);
document.getElementById("songlist").innerHTML = "";
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
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 = "<h1>Search to find songs!</h1>";
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 = "./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 = "<h1>We might not have that one...</h1>";
}
}
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 ipSetEnter(e){
if (e.key == "Enter") {
e.preventDefault();
// why on gosh's green earth am i sending a value here?
// im gonna get rid of all these individual "enter" dectectors and do something
// like i did for the keyboard selection of elements
// basically just if(e==click || e.key == enter)
ipSetter(document.getElementById("iptextbox").value)
}
}
function ipSetter(){
ipBox = document.getElementById("iptextbox").value
if (ipBox == "") {
alertText("Your IP is set to "+ip)
} else {
if (ipBox.includes(":")) {
port = ipBox.slice(ipBox.indexOf(":")+1)
ip = ipBox;
document.cookie = "ip="+ip+"; path=/;"
alertText("Your IP is now set to "+ip.slice(0, ipBox.indexOf(":"))+" at port "+port)
} else {
ip = ipBox + ":19054"
document.cookie = "ip="+ip+"; path=/;"
alertText("Your IP is now set to "+ipBox+" at port 19054 (Default)")
}
}
// anytime the server ip changes the qrcode should change to use it
qrCodeGenerate()
}
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<nodeList.length;i++) {
if (nodeList[i].type == 'checkbox') {
nodeList[i].checked = true;
}
}
//ping the server here
data = await getFromServer({setting: "getsettings"}, "settings");
x = data["data"];
if (!(skipServer) || partyButtonState=="N/A") {
if (x["partymode"] == false) {
document.getElementById("partymode-button").innerHTML = "Off";
} else {
document.getElementById("partymode-button").innerHTML = "On";
}
} else if (document.getElementById("partymode-button").innerHTML == "Off") {
document.getElementById("partymode-button").innerHTML = "On";
} else {
document.getElementById("partymode-button").innerHTML = "Off";
}
document.getElementById("volumerange").value = parseInt(x["volume"])
// do the admin checkboxes here
let currentAdminPerms = x["admin"];
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"];
}
async function addToPlaylist(songObject) {
i = document.getElementById("playlist").children.length-1
let newItem = document.createElement("div");
newItem.className = "item";
newItem.id = Object.keys(songObject)[0];
newItem.tabIndex = 0;
let image = document.createElement("img");
try {
if (songObject[newItem.id]["art"] == null) {
throw "no image lolz"
}
image.src = songObject[newItem.id]["art"];
} catch(err){
image.src = "./images/placeholder.png";
}
image.id = String(songObject[newItem.id])+" image";
let head3 = document.createElement("h3");
head3.innerText = songObject[newItem.id]["title"];
let head4 = document.createElement("h4");
head4.innerText= songObject[newItem.id]["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";
playlistElapsedSeconds = playlist[0]["time"];
playlistSongLength = playlist[0]["length"];
displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength);
clearInterval(playlistTimeTimer);
}
} catch(e) {
console.log("I dunno something bad happened:"+e);
}
}
async function skipInPlaylist() {
playlistElapsedSeconds = 0;
let playlistChildren = document.getElementById("playlist").children;
if(playlistChildren[1].nodeName === "DIV") {
playlistChildren[1].remove();
}
playlistChildren = document.getElementById("playlist").children;
if(playlistChildren.length === 1) {
playlistChildren[0].innerText = "Nothing's Queued..."
} else {
let firstElementTextChildren = playlistChildren[1].children[1].children
// console.log(firstElementTextChildren);
firstElementTextChildren[2].id = "elapsed-time-display";
firstElementTextChildren[3].id = "playing-indicator-text";
firstElementTextChildren[3].textContent = "Playing";
}
displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength);
}
async function generateVisualPlaylist(conditions="") {
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
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 = "./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";
//for my use case (my immediate family), they dont know how to set an ip
//using this allows the creator of the link for, a qr code for example, to set the ip before distributing the code, and it would all work smoothly
//example (http://192.168.1.100:8000/?ip=192.168.1.100:19054 sets the ip to the same host at the default port)
//the port must be set manually using this method, but only has to be done once for the url that ends up being shared
//tries the url first, then the cookie, then the default
ip = params.get("ip")
if (ip == null || ip=="") {
ip=getCookie("ip")
}
if (ip==null || ip==""){
ip = ""
}
// 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);
}
});

246
Server/Client/styles.css Normal file
View file

@ -0,0 +1,246 @@
/* dark mode stuff */
.dark-mode {
--bg-main: #333333;
--bg-item: #3f3f3f;
--bg-inputs: #2a2a2a;
--text-color: #ffffff;
/* -webkit-filter:invert(100%);
filter:progid:DXImageTransform.Microsoft.BasicImage(invert='1'); */
}
.dark-mode .control-button {
filter: invert(100%) brightness(0.9)
}
:root {
--bg-main: #eeeeee;
--bg-item: #dddddd;
--bg-inputs: #ffffff;
--text-color: #000000;
color: var(--text-color);
}
/* In hindsight i should have just used p's with classes to decide size, but whatever
Should probably fix that at some point, this is like the least accessible site ever */
h1,h2,h3,h4,h5,p,input,button,label {
color: var(--text-color);
}
input, button {
background-color: var(--bg-inputs);
}
/* Things that are always visible */
body {
background-color: var(--bg-main);
}
* {
font-family: 'Arial',sans-serif;
}
.italic {
font-style: italic;
}
h4 {
font-weight: 100;
}
.clear{
clear: both;
display: block;
content: "";
width: 100%;
}
.controls {
max-width: 550px;
min-width: 300px;
position: fixed;
width:100%;
left: 50%;
bottom: 0;
margin: 0 auto;
transform: translateX(-50%);
background-color:var(--bg-main);
}
.alert {
position: fixed;
bottom: 10%;
width: 100%;
text-align: center;
z-index: 1000;
background-color: color-mix(in srgb, var(--bg-main), transparent 16%);
}
.settings-button-holder {
width:15%;
max-width: 90px;
position:fixed;
top:0;
right:0;
margin: 3px;
background-color: var(--bg-main);
/* This is a circle background for the circle settings button
So it can display over other text and such */
border-radius: 50%;
}
.settings-button {
width: 100%;
}
.controls > .control-button{
width:20%;
max-width: 110px;
margin: auto 2%;
}
.intro {
width: 300px;
margin: auto;
text-align: center;
}
.item {
/* Only actually applies to playlist and search because settings item has "inherit" bg-colour */
background-color: var(--bg-item);
}
/* Songlist stuff */
.songlist {
width: 80%;
min-width: 400px;
margin:auto auto 150px;
display: flex;
flex-wrap: wrap;
}
.songlist > .item{
border: 1px solid var(--bg-item);
width:30%;
max-width: 150px;
margin: 5px auto;
min-width: 75px;
}
.songlist > .item > img{
max-width:200px;
width:100%
}
.songlist > .item > h3, .songlist > .item > h4{
margin-left: 2px;
margin-right: 2px;
margin: 5px;
word-wrap: break-word;
}
.searchbox-holder {
width: 20%;
margin: 20px auto 0;
min-width: 250px;
}
.searchbox {
width: 65%;
margin: 1px;
margin-bottom: 16px;
}
.go-search {
width: 20%;
min-width: 50px;
}
.lossless-tag {
width:16px;
padding: 1px;
margin-left: auto;
}
/* playlist mode stuff */
.playlist {
width: 60%;
min-width: 300px;
margin:auto auto 150px;
}
.playlist > .item{
border: 1px solid var(--bg-item);
display: flex;
max-width: 50em;
min-width: 200px;
margin: 5px auto;
height: auto;
}
.playlist > .item > .text {
padding: 3px;
display: inline-block;
margin: 0px 3px;
}
.playlist > .item > img {
display: inline-flex;
max-width: 100px;
max-height: 100px;
width:30%;
margin: 0;
aspect-ratio: 1/1;
}
.playlist > .item > .text > * {
margin:2px;
}
/* settings stuff */
.settings {
width: 95%;
margin: auto auto 150px;
max-width: 600px;
}
.settings > .item {
margin-left: 10%;
width:fit-content;
background-color: var(--bg-main);
}
.settings > .item:not(:last-child) {
padding-bottom: 10px;
border-bottom: 1px solid var(--bg-item);
}
.settings > .item.no-line {
border-bottom: 0px none #00000000;
padding-bottom: 0px;
}
.settings > .lastSet1 {
border-bottom: 0;
}
.settings > .item > h2 {
margin-bottom: 4px;
}
.settings > .item > p {
margin-top: 0px
}
.versionNumber {
font-size: 11px;
font-style: italic;
text-align: left;
width: 80%;
}
#volumerange {
background-color: #4477AA;
color: #4477ff;
}