I am creating a Music Player App for Wix Users, Any feedback will be appreciated.
Best Regards
I am creating a Music Player App for Wix Users, Any feedback will be appreciated.
Best Regards
I thought it was a fantastic achievement, It must have been quite a challenge to create. Personally, I really appreciated the attention to detail in the Retro Player’s turntable and spectrum display animation. The separate left and right volume meters were a nice touch as well.
However, if I were to use it for my site, I think I’d go with the Lo-Fi Player design (or Neuromorphism). The reason is that the spectrum display was the most striking and beautiful, and the overall design and color scheme felt elegant and rich without being too flashy. I feel the Lo-Fi Player would be the most fitting for a music service, as it’s a safe design yet still very beautiful.
Lastly, one small thing I noticed personally— the shadow at the very bottom of the Retro Player is cut off. Keep up the great work with your development!
Good work! → But surely can be improved more…
1) Buttons do not show up POP-UP-TITLES → TOOLTIPS <— missing.
2) The shown selected button → (either REPEAT-BUTTON or RANDOM-BUTTON) we do not know this → not really working and always jumps out of service. Once selected this should normaly either set the PLAYER into the RANDOM-PLAY-MODE or REPEAT-MODE, depending on the buttons intention → i assume → REPEAT-MODE.
The same for another button —>
Also check one more time your CSS-Transitions for your buttons. Taking the last shown button → try to hover over the very edge oof the button → you will get a wildly → FLACTUATING-BUTTON ← due to CSS-ZOOM-FUNCTION on hover (at least my assumtion)
Good choice for DEMO-MUSIC.
Very good Sound Visualizer.
Most players extract the ID3 tags from MP3, you maybe could do the same, popping up more informations about SONG, ARTIST, DATE and so on, when hover over the SONG-COVER.
Surrely more options available…
Good luck!
Thanks a lot!!! Totally missed these; you are the best.
Do you know any way to hide the song link in the source code? i am using CMS for catalog and the song link is out open in the code. Would be nice to have it hidden…
I definitely missed the shadow!! Will work on that… You are the best, thanks for your time.
var encodedURL = "aHR0cHM6Ly9leGFtcGxlLmNvbS9zb25nLm1wMw=="; // Base64 encoded link
var decodedURL = atob(encodedURL); // Decodes the link
var audio = new Audio(decodedURL);
audio.play();
URL-MASKING or something in this direction.
Without beeing sure, but maybe you could try to get this to work…
In your Wix app, you can create a custom API endpoint using Wix HTTP Functions to serve the song dynamically. This means the song will be streamed from your server (or Wix’s backend) and the URL will not be exposed to the client.
Backend Code (Velo HTTP Function):
import { ok, notFound } from 'wix-http-functions';
export function get_song(request) {
const songPath = '/path/to/song.mp3'; // Location on Wix Media
const songFile = wixMedia.getFileUrl(songPath);
if (songFile) {
return ok(songFile); // Send the song file URL to the client.
} else {
return notFound();
}
}
Frontend Code:
$w.onReady(function () {
fetch('/_functions/get_song')
.then(response => response.text())
.then(url => {
const audio = new Audio(url);
audio.play();
})
.catch(err => console.log('Error fetching song:', err));
});
Here, the song URL is abstracted through the HTTP function, and it’s not easily visible in the front-end code.
And maybe you want to use my generated VOLUME-KNOB (present)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Volume Knob with Corrected Scale</title>
<style>
body {
background: #111;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.tick {
position: absolute;
width: 2px;
height: 10px;
background: #ccc;
top: 60px;
left: 50%;
transform-origin: bottom center;
}
.tick-label {
position: absolute;
color: #aaa;
font-size: 9px;
transform: translate(-50%, -50%);
pointer-events: none;
}
</style>
</head>
<body>
<script>
function createVolumeKnob({ initialVolume = 0.5, onChange = () => {} } = {}) {
const container = document.createElement("div");
const scale = document.createElement("div");
const knob = document.createElement("div");
const indicator = document.createElement("div");
const shine = document.createElement("div");
container.style.position = "relative";
container.style.width = "140px";
container.style.height = "140px";
scale.style.position = "absolute";
scale.style.top = "0";
scale.style.left = "0";
scale.style.width = "100%";
scale.style.height = "100%";
scale.style.zIndex = "1";
knob.style.position = "absolute";
knob.style.top = "20px";
knob.style.left = "20px";
knob.style.width = "100px";
knob.style.height = "100px";
knob.style.borderRadius = "50%";
knob.style.background = `
radial-gradient(circle at 30% 30%, rgba(255,255,255,0.3), transparent 60%),
repeating-linear-gradient(90deg, #444 0px, #333 1px, #222 2px),
linear-gradient(145deg, #666, #111)
`;
knob.style.backgroundBlendMode = "overlay";
knob.style.border = "2px solid #888";
knob.style.cursor = "pointer";
knob.style.transition = "box-shadow 0.1s linear, background 0.3s ease";
knob.style.boxShadow = `
inset -3px -3px 8px rgba(255,255,255,0.15),
inset 3px 3px 10px rgba(0,0,0,0.5),
0 0 20px rgba(0,0,0,0.6)
`;
knob.style.zIndex = "2";
Object.assign(indicator.style, {
width: "4px",
height: "40px",
background: "grey",
position: "absolute",
top: "10px",
left: "50%",
transformOrigin: "bottom center",
transform: "translateX(-50%) rotate(0deg)",
borderRadius: "25%",
});
Object.assign(shine.style, {
position: "absolute",
top: "5px",
left: "5px",
width: "90px",
height: "90px",
borderRadius: "50%",
background: "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.2), transparent 60%)",
pointerEvents: "none"
});
knob.appendChild(indicator);
knob.appendChild(shine);
container.appendChild(scale);
container.appendChild(knob);
// --- Generate 10 scale ticks and labels from 0% to 100%
const ticks = 10;
for (let i = 0; i <= ticks; i++) {
let percent = i / ticks;
let angle = percent * 270 - 135; // Distribute over 270 degrees
const tick = document.createElement("div");
tick.className = "tick";
tick.style.transform = `rotate(${angle}deg) translateY(-50px)`;
scale.appendChild(tick);
const label = document.createElement("div");
label.className = "tick-label";
label.innerText = `${i * 10}%`;
const radians = (angle - 90) * (Math.PI / 180);
const radius = 72;
const cx = 70 + radius * Math.cos(radians);
const cy = 70 + radius * Math.sin(radians);
label.style.left = `${cx}px`;
label.style.top = `${cy}px`;
scale.appendChild(label);
}
let volume = initialVolume;
let angle = volumeToAngle(volume);
updateKnob(angle);
function volumeToAngle(v) {
return v * 270 - 135;
}
function angleToVolume(a) {
return Math.max(0, Math.min(1, (a + 135) / 270));
}
function getRGBFromVolume(vol) {
let r = Math.min(255, Math.floor(510 * vol));
let g = Math.min(255, Math.floor(510 * (1 - vol)));
return `rgb(${r},${g},0)`;
}
function updateKnob(deg) {
indicator.style.transform = `translateX(-50%) rotate(${deg}deg)`;
volume = angleToVolume(deg);
const glowColor = getRGBFromVolume(volume);
knob.style.boxShadow = `
0 0 20px ${glowColor},
inset -3px -3px 8px rgba(255,255,255,0.15),
inset 3px 3px 10px rgba(0,0,0,0.5),
0 0 10px ${glowColor}
`;
onChange(volume);
}
// --- Interaction
let isDragging = false;
knob.addEventListener("mousedown", () => { isDragging = true; });
document.addEventListener("mouseup", () => { isDragging = false; });
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const rect = knob.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
let deg = Math.atan2(y, x) * (180 / Math.PI);
deg = Math.max(-135, Math.min(135, deg));
angle = deg;
updateKnob(deg);
});
knob.addEventListener("wheel", (e) => {
e.preventDefault();
const delta = -e.deltaY || e.wheelDelta;
angle += (delta > 0 ? 10 : -10);
angle = Math.max(-135, Math.min(135, angle));
updateKnob(angle);
});
return {
element: container,
getVolume: () => volume,
setVolume: (v) => {
angle = volumeToAngle(v);
updateKnob(angle);
},
};
}
// --- Example usage
const audioElement = new Audio();
audioElement.volume = 0.5;
const knob = createVolumeKnob({
initialVolume: 0.5,
onChange: (v) => {
console.log("Volume:", v.toFixed(2));
audioElement.volume = v;
}
});
document.body.appendChild(knob.element);
</script>
</body>
</html>
I tried to code it as simple as possible and as self-enclosed as possible, so it is reusable.
Paste the code into JS-FIDDLE (HTML-Section), but i think you will know what and how to do…
And some other ideas…
P.S.: You can use the mouse-scroll on the knob to set your desired volume-power.
And maybe you take a look here…
Take a look onto the desired features people are searching for, to make your player unique in it’s functionality, features and functions.
Good luck!
And do not forget to show me your end-result!
Wow… That’s a lot of ideas!!! Thanks a lot for investing your time, I really appreciate it.
No problem, you are welcome!
Here my last present for you…
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Compact MP3 Player</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: sans-serif;
background-color: #121212;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.player {
border: 1px solid white;
background: #1e1e1e;
padding: 0 10px 25px 10px;
border-radius: 15px;
width: 350px;
min-height: 300px;
max-height: 35%;
box-shadow: 0 0 15px rgba(255,255,255,0.4);
display: flex;
position: relative;
flex-direction: column;
align-items: center;
}
.top-menu {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
text-align: center;
font-size: 15px;
background: rgba(169, 169, 169, 0.2);
padding: 4px;
border-radius: 5px;
width: 99%;
margin-top: 5px;
margin-bottom: 20px;
}
.cell-left {
background: rgba(7, 7, 7, 0.2);
padding: 4px;
font-size: 10px;
}
.cell-right {
background: rgba(0, 0, 0, 0.2);
padding: 4px;
font-size: 10px;
}
.time-display {
text-align: center;
font-size: 15px;
color: #ccc;
margin: 5px;
}
.controls {
display: flex;
justify-content: space-around;
align-items: center;
margin: 10px 0;
}
button {
background: #2e2e2e;
padding: 10px;
border-radius: 50%;
font-size: 18px;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
button:hover { background: rgb(255, 255, 255); }
button:disabled { opacity: 0.3; cursor: not-allowed; }
input[type="range"] {
width: 100%;
margin: 6px 0;
}
.playlist {
max-height: 250px;
overflow-y: auto;
margin-top: 5px;
background: #2a2a2a;
border-radius: 5px;
padding: 5px;
max-width: 99%;
list-style: none;
}
.playlist li {
padding: 2px;
margin-bottom: 4px;
background: #3a3a3a;
border-radius: 4px;
cursor: pointer;
white-space: wrap; /* Prevent text from wrapping */
overflow: hidden; /* Hide overflowing text */
text-overflow: ellipsis; /* Add "..." at the end if it overflows */
}
#volumeControl {
margin-top: 200px;
display: flex;
justify-content: center;
}
.playlist li:hover, .playlist li.selected {background: #007bff;}
.tick {
position: absolute;
width: 2px;
height: 10px;
background: lightgrey;
top: 60px;
left: 50%;
transform-origin: bottom center;
}
.tick-label {
position: absolute;
color: #aaa;
font-size: 9px;
transform: translate(-50%, -50%);
pointer-events: none;
}
.signature {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
font-size: 12px;
color: #ccc;
background: rgba(41, 169, 248, 0.5);
padding: 4px;
border-radius: 0 0 10px 10px;
}
</style>
<script>
function createVolumeKnob({ initialVolume = 0.5, onChange = () => {} } = {}) {
const container = document.createElement("div");
const scale = document.createElement("div");
const knob = document.createElement("div");
const indicator = document.createElement("div");
const shine = document.createElement("div");
//---------------
container.style.position = "relative";
container.style.width = "140px";
container.style.height = "140px";
scale.style.position = "absolute";
scale.style.top = "0";
scale.style.left = "0";
scale.style.width = "100%";
scale.style.height = "100%";
scale.style.zIndex = "1";
//---------------
knob.style.position = "absolute";
knob.style.top = "20px";
knob.style.left = "20px";
knob.style.width = "100px";
knob.style.height = "100px";
knob.style.borderRadius = "50%";
knob.style.background = `
radial-gradient(circle at 30% 30%, rgba(255,255,255,0.3), transparent 60%),
repeating-linear-gradient(90deg, #444 0px, #333 1px, #222 2px),
linear-gradient(145deg, #666, #111)
`;
knob.style.backgroundBlendMode = "overlay";
knob.style.border = "2px solid #888";
knob.style.cursor = "pointer";
knob.style.transition = "box-shadow 0.1s linear, background 0.3s ease";
knob.style.boxShadow = `
inset -3px -3px 8px rgba(255,255,255,0.15),
inset 3px 3px 10px rgba(0,0,0,0.5),
0 0 20px rgba(0,0,0,0.6)
`;
knob.style.zIndex = "2";
Object.assign(indicator.style, {
width: "4px",
height: "40px",
background: "grey",
position: "absolute",
top: "10px",
left: "50%",
transformOrigin: "bottom center",
transform: "translateX(-50%) rotate(0deg)",
borderRadius: "25%",
});
Object.assign(shine.style, {
position: "absolute",
top: "5px",
left: "5px",
width: "90px",
height: "90px",
borderRadius: "50%",
background: "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.2), transparent 60%)",
pointerEvents: "none"
});
knob.appendChild(indicator);
knob.appendChild(shine);
container.appendChild(scale);
container.appendChild(knob);
const ticks = 10;
for (let i = 0; i <= ticks; i++) {
let percent = i / ticks;
let angle = percent * 270 - 135;
const tick = document.createElement("div");
tick.className = "tick";
tick.style.transform = `rotate(${angle}deg) translateY(-50px)`;
scale.appendChild(tick);
const label = document.createElement("div");
label.className = "tick-label";
label.innerText = `${i * 10}%`;
const radians = (angle - 90) * (Math.PI / 180);
const radius = 72;
const cx = 70 + radius * Math.cos(radians);
const cy = 70 + radius * Math.sin(radians);
label.style.left = `${cx}px`;
label.style.top = `${cy}px`;
scale.appendChild(label);
}
let volume = initialVolume;
let angle = volumeToAngle(volume);
updateKnob(angle);
function volumeToAngle(v) {return v * 270 - 135;}
function angleToVolume(a) {return Math.max(0, Math.min(1, (a + 135) / 270));}
function getRGBFromVolume(vol) {
let r = Math.min(255, Math.floor(510 * vol));
let g = Math.min(255, Math.floor(510 * (1 - vol)));
return `rgb(${r},${g},0)`;
}
function updateKnob(deg) {
indicator.style.transform = `translateX(-50%) rotate(${deg}deg)`;
volume = angleToVolume(deg);
const glowColor = getRGBFromVolume(volume);
knob.style.boxShadow = `
0 0 20px ${glowColor},
inset -3px -3px 8px rgba(255,255,255,0.15),
inset 3px 3px 10px rgba(0,0,0,0.5),
0 0 10px ${glowColor}
`;
onChange(volume);
}
// --- Interaction
let isDragging = false;
knob.addEventListener("mousedown", () => { isDragging = true; });
document.addEventListener("mouseup", () => { isDragging = false; });
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const rect = knob.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
let deg = Math.atan2(y, x) * (180 / Math.PI);
deg = Math.max(-135, Math.min(135, deg));
angle = deg;
updateKnob(deg);
});
knob.addEventListener("wheel", (e) => {
e.preventDefault();
const delta = -e.deltaY || e.wheelDelta;
angle += (delta > 0 ? 10 : -10);
angle = Math.max(-135, Math.min(135, angle));
updateKnob(angle);
});
// Append the volume knob to the player div
const volumeControl = document.createElement("div");
volumeControl.id = 'volumeControl';
volumeControl.appendChild(container);
// Insert the knob between time display and controls
PLAYER.insertBefore(container, document.querySelector('.controls'));
return {
element: container,
getVolume: () => volume,
setVolume: (v) => {
angle = volumeToAngle(v);
updateKnob(angle);
},
};
}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
</head>
<body>
<div id="PLAYER" class="player">
<div class="top-menu" id="topMenu">
<div class="cell-left" id="cellLeft">MP3-Mini</div>
<div class="time-display" id="timeDisplay">00:00 / 00:00</div>
<div class="cell-right" id="cellRight">Version: 1.00</div>
</div>
<div class="controls">
<button id="prevBtn" disabled><i class="fas fa-backward"></i></button>
<button id="playPauseBtn" disabled><i class="fas fa-play"></i></button>
<button id="nextBtn" disabled><i class="fas fa-forward"></i></button>
<button id="stopBtn" disabled><i class="fas fa-stop"></i></button>
<button id="repeatBtn" disabled><i class="fas fa-redo"></i></button>
<button id="shuffleBtn" disabled><i class="fas fa-random"></i></button>
<button id="loadBtn"><i class="fas fa-folder-open"></i></button>
</div>
<input type="range" id="seek" value="0" min="0" max="100">
<ul class="playlist" id="playlist"></ul>
<input type="file" id="fileInput" accept="audio/*" multiple hidden>
<div class="signature" id="signature">Velo-Ninja's MP3</div>
</div>
<script>
const audioElement = new Audio();
let songs = [], currentIndex = 0, isPlaying = false, repeat = false, shuffle = false;
const PLAYER = document.getElementById("PLAYER");
const prevBtn = document.getElementById("prevBtn");
const playPauseBtn = document.getElementById("playPauseBtn");
const nextBtn = document.getElementById("nextBtn");
const stopBtn = document.getElementById("stopBtn");
const repeatBtn = document.getElementById("repeatBtn");
const shuffleBtn = document.getElementById("shuffleBtn");
const seekBar = document.getElementById("seek");
const timeDisplay = document.getElementById("timeDisplay");
const playlist = document.getElementById("playlist");
const loadBtn = document.getElementById("loadBtn");
const fileInput = document.getElementById("fileInput");
// Update this part where you append the knob to the DOM.
const knob = createVolumeKnob({initialVolume: 0.5, onChange: (v)=> {audioElement.volume = v;}});
loadBtn.onclick = () => fileInput.click();
fileInput.addEventListener("change", () => {
if (fileInput.files.length) {
songs = Array.from(fileInput.files).map(file => ({
name: file.name,
url: URL.createObjectURL(file)
})); loadSong(0);
}
});
function loadSong(index) {
if (!songs[index]) return;
currentIndex = index;
audioElement.src = songs[index].url;
updatePlaylistUI();
playPauseBtn.disabled = false;
stopBtn.disabled = false;
prevBtn.disabled = false;
nextBtn.disabled = false;
repeatBtn.disabled = false;
shuffleBtn.disabled = false;
playAudio();
}
function updatePlaylistUI() {
playlist.innerHTML = "";
songs.forEach((song, i) => {
const li = document.createElement("li");
li.textContent = song.name;
li.className = i === currentIndex ? "selected" : "";
li.onclick = () => loadSong(i);
playlist.appendChild(li);
});
}
function playAudio() {audioElement.play(); isPlaying = true; playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';}
function pauseAudio() {audioElement.pause(); isPlaying = false; playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';}
function stopAudio() {audioElement.pause(); audioElement.currentTime = 0; isPlaying = false; playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';}
function nextSong() {currentIndex = shuffle ? Math.floor(Math.random() * songs.length) : (currentIndex + 1) % songs.length; loadSong(currentIndex);}
function prevSong() {currentIndex = (currentIndex - 1 + songs.length) % songs.length; loadSong(currentIndex);}
function updateTime() {
if (!audioElement.duration) return;
seekBar.value = (audioElement.currentTime / audioElement.duration) * 100;
const format = t => String(Math.floor(t / 60)).padStart(2, "0") + ":" + String(Math.floor(t % 60)).padStart(2, "0");
timeDisplay.textContent = `${format(audioElement.currentTime)} / ${format(audioElement.duration)}`;
}
seekBar.addEventListener("input", ()=> {if (audioElement.duration) {audioElement.currentTime = (seekBar.value / 100) * audioElement.duration;}});
playPauseBtn.onclick = () => isPlaying ? pauseAudio() : playAudio();
stopBtn.onclick = stopAudio;
nextBtn.onclick = nextSong;
prevBtn.onclick = prevSong;
repeatBtn.onclick = () => {
repeat = !repeat;
repeatBtn.style.background = repeat ? "#0a5" : "#2e2e2e";
};
shuffleBtn.onclick =()=> {shuffle = !shuffle; shuffleBtn.style.background = shuffle ? "#0a5" : "#2e2e2e";}
audioElement.addEventListener("timeupdate", updateTime);
audioElement.addEventListener("ended", ()=> repeat ? loadSong(currentIndex) : nextSong());
audioElement.volume = 0.5;
</script>
</body>
</html>
Here you can see the → VOLUME-KNOB ← in action! (version-1.03)
Save this as an HTML-FILE on your local machine (DESKTOP for example). Launch the file and have fun with my mini-mp3-player (and yes, it was you, who gave me the motivation to create it) …
Do what ever you want with it.