Adds Seekbar
This commit is contained in:
69
hg.css
69
hg.css
@@ -427,6 +427,75 @@ canvas {
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 55px;
|
||||||
|
background: linear-gradient(145deg, rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.85));
|
||||||
|
border-top: 2px solid #ff2975;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-shadow:
|
||||||
|
0 -2px 10px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 15px rgba(255, 41, 117, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-family: "VCR", monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #00ffff;
|
||||||
|
text-shadow: 0 0 5px rgba(0, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: relative;
|
||||||
|
height: 6px;
|
||||||
|
background: linear-gradient(145deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.9));
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #ff2975, #00ffff);
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.1s ease-out;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 41, 117, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: radial-gradient(circle, #00ffff, #ff2975);
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 255, 255, 0.6);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar:hover .progress-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-handle:hover {
|
||||||
|
transform: translateX(-50%) scale(1.1);
|
||||||
|
box-shadow: 0 0 12px rgba(0, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.player-controls {
|
.player-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
10
index.html
10
index.html
@@ -129,6 +129,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="progress-bar-container" class="progress-bar-container">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span id="current-time">00:00</span>
|
||||||
|
<span id="total-time">00:00</span>
|
||||||
|
</div>
|
||||||
|
<div id="progress-bar" class="progress-bar">
|
||||||
|
<div id="progress-fill" class="progress-fill"></div>
|
||||||
|
<div id="progress-handle" class="progress-handle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="codef-canvas"></div>
|
<div id="codef-canvas"></div>
|
||||||
<div class="OVR hidden"></div>
|
<div class="OVR hidden"></div>
|
||||||
<div class="ERRORS hidden"></div>
|
<div class="ERRORS hidden"></div>
|
||||||
|
|||||||
190
player.js
190
player.js
@@ -18,6 +18,16 @@
|
|||||||
let currentEffectIndex = 0;
|
let currentEffectIndex = 0;
|
||||||
const modalEl = document.getElementById("intro-modal");
|
const modalEl = document.getElementById("intro-modal");
|
||||||
|
|
||||||
|
// Progress bar elements (initialized later to avoid null references)
|
||||||
|
let progressBarContainer;
|
||||||
|
let progressBar;
|
||||||
|
let progressFill;
|
||||||
|
let progressHandle;
|
||||||
|
let currentTimeEl;
|
||||||
|
let totalTimeEl;
|
||||||
|
let progressUpdateInterval;
|
||||||
|
let isDragging = false;
|
||||||
|
|
||||||
// const uploadInfoEl = document.getElementById("upload-info");
|
// const uploadInfoEl = document.getElementById("upload-info");
|
||||||
const fileUploadEl = document.getElementById("file-upload");
|
const fileUploadEl = document.getElementById("file-upload");
|
||||||
let volume = 1;
|
let volume = 1;
|
||||||
@@ -111,6 +121,7 @@
|
|||||||
});
|
});
|
||||||
initHydra();
|
initHydra();
|
||||||
initControls();
|
initControls();
|
||||||
|
initProgressBar();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("history").addEventListener("click", () => {
|
document.getElementById("history").addEventListener("click", () => {
|
||||||
@@ -223,6 +234,18 @@
|
|||||||
onend: function () {
|
onend: function () {
|
||||||
playNextTrack();
|
playNextTrack();
|
||||||
},
|
},
|
||||||
|
onload: function () {
|
||||||
|
updateProgressBar();
|
||||||
|
},
|
||||||
|
onplay: function () {
|
||||||
|
startProgressTracking();
|
||||||
|
},
|
||||||
|
onpause: function () {
|
||||||
|
stopProgressTracking();
|
||||||
|
},
|
||||||
|
onstop: function () {
|
||||||
|
stopProgressTracking();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setVolume();
|
setVolume();
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
@@ -318,6 +341,7 @@ https://retrowave.ru/${musicData.streamUrl}
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetHowler(destroy = false) {
|
function resetHowler(destroy = false) {
|
||||||
|
stopProgressTracking();
|
||||||
howlerInstance.stop();
|
howlerInstance.stop();
|
||||||
if (destroy) {
|
if (destroy) {
|
||||||
howlerInstance.unload();
|
howlerInstance.unload();
|
||||||
@@ -325,6 +349,134 @@ https://retrowave.ru/${musicData.streamUrl}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Progress Bar Utility Functions
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressBar() {
|
||||||
|
if (!howlerInstance || isDragging || !progressFill || !progressHandle || !currentTimeEl || !totalTimeEl) return;
|
||||||
|
|
||||||
|
const seek = howlerInstance.seek() || 0;
|
||||||
|
const duration = howlerInstance.duration() || 0;
|
||||||
|
|
||||||
|
if (duration > 0) {
|
||||||
|
const percentage = (seek / duration) * 100;
|
||||||
|
progressFill.style.width = `${percentage}%`;
|
||||||
|
progressHandle.style.left = `${percentage}%`;
|
||||||
|
|
||||||
|
currentTimeEl.textContent = formatTime(seek);
|
||||||
|
totalTimeEl.textContent = formatTime(duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startProgressTracking() {
|
||||||
|
if (progressUpdateInterval) {
|
||||||
|
clearInterval(progressUpdateInterval);
|
||||||
|
}
|
||||||
|
progressUpdateInterval = setInterval(updateProgressBar, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopProgressTracking() {
|
||||||
|
if (progressUpdateInterval) {
|
||||||
|
clearInterval(progressUpdateInterval);
|
||||||
|
progressUpdateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function seekToPosition(percentage) {
|
||||||
|
if (!howlerInstance) return;
|
||||||
|
|
||||||
|
const duration = howlerInstance.duration() || 0;
|
||||||
|
if (duration > 0) {
|
||||||
|
const seekTime = (percentage / 100) * duration;
|
||||||
|
howlerInstance.seek(seekTime);
|
||||||
|
updateProgressBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initProgressBar() {
|
||||||
|
// Initialize DOM elements
|
||||||
|
progressBarContainer = document.getElementById("progress-bar-container");
|
||||||
|
progressBar = document.getElementById("progress-bar");
|
||||||
|
progressFill = document.getElementById("progress-fill");
|
||||||
|
progressHandle = document.getElementById("progress-handle");
|
||||||
|
currentTimeEl = document.getElementById("current-time");
|
||||||
|
totalTimeEl = document.getElementById("total-time");
|
||||||
|
|
||||||
|
if (!progressBar || !progressFill || !progressHandle || !currentTimeEl || !totalTimeEl) {
|
||||||
|
console.warn("Progress bar elements not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click to seek
|
||||||
|
progressBar.addEventListener("click", (e) => {
|
||||||
|
if (isDragging) return;
|
||||||
|
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
const percentage = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
seekToPosition(Math.max(0, Math.min(100, percentage)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag to seek functionality
|
||||||
|
let startX = 0;
|
||||||
|
let startPercentage = 0;
|
||||||
|
|
||||||
|
progressHandle.addEventListener("mousedown", (e) => {
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
startPercentage = ((startX - rect.left) / rect.width) * 100;
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMouseMove(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
const percentage = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const clampedPercentage = Math.max(0, Math.min(100, percentage));
|
||||||
|
|
||||||
|
progressFill.style.width = `${clampedPercentage}%`;
|
||||||
|
progressHandle.style.left = `${clampedPercentage}%`;
|
||||||
|
|
||||||
|
if (howlerInstance) {
|
||||||
|
const duration = howlerInstance.duration() || 0;
|
||||||
|
if (duration > 0) {
|
||||||
|
const seekTime = (clampedPercentage / 100) * duration;
|
||||||
|
currentTimeEl.textContent = formatTime(seekTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
const percentage = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const clampedPercentage = Math.max(0, Math.min(100, percentage));
|
||||||
|
|
||||||
|
seekToPosition(clampedPercentage);
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch support for mobile
|
||||||
|
progressBar.addEventListener("touchstart", (e) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
const percentage = ((touch.clientX - rect.left) / rect.width) * 100;
|
||||||
|
seekToPosition(Math.max(0, Math.min(100, percentage)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initControls() {
|
function initControls() {
|
||||||
refreshBtn.addEventListener("click", () => {
|
refreshBtn.addEventListener("click", () => {
|
||||||
playNextTrack();
|
playNextTrack();
|
||||||
@@ -1267,6 +1419,7 @@ https://retrowave.ru/${musicData.streamUrl}
|
|||||||
addTerminalLine(' pause - Pause playback');
|
addTerminalLine(' pause - Pause playback');
|
||||||
addTerminalLine(' next - Skip to next track');
|
addTerminalLine(' next - Skip to next track');
|
||||||
addTerminalLine(' volume [0-10] - Set volume (0-10)');
|
addTerminalLine(' volume [0-10] - Set volume (0-10)');
|
||||||
|
addTerminalLine(' seek [time] - Seek to time (seconds or mm:ss)');
|
||||||
addTerminalLine(' status - Show current track info');
|
addTerminalLine(' status - Show current track info');
|
||||||
addTerminalLine(' history - Download playlist history');
|
addTerminalLine(' history - Download playlist history');
|
||||||
addTerminalLine(' effect [list|name]- Change/list visual effects');
|
addTerminalLine(' effect [list|name]- Change/list visual effects');
|
||||||
@@ -1319,12 +1472,49 @@ https://retrowave.ru/${musicData.streamUrl}
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'seek':
|
||||||
|
if (args[1] && howlerInstance) {
|
||||||
|
let seekTime = 0;
|
||||||
|
const input = args[1];
|
||||||
|
|
||||||
|
if (input.includes(':')) {
|
||||||
|
const [mins, secs] = input.split(':').map(Number);
|
||||||
|
seekTime = (mins * 60) + secs;
|
||||||
|
} else {
|
||||||
|
seekTime = parseFloat(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = howlerInstance.duration() || 0;
|
||||||
|
if (seekTime >= 0 && seekTime <= duration) {
|
||||||
|
howlerInstance.seek(seekTime);
|
||||||
|
updateProgressBar();
|
||||||
|
addTerminalLine(`Seeked to ${formatTime(seekTime)}`);
|
||||||
|
} else {
|
||||||
|
addTerminalLine(`Invalid seek time. Duration: ${formatTime(duration)}`);
|
||||||
|
}
|
||||||
|
} else if (!howlerInstance) {
|
||||||
|
addTerminalLine('No track loaded.');
|
||||||
|
} else {
|
||||||
|
if (howlerInstance) {
|
||||||
|
const seek = howlerInstance.seek() || 0;
|
||||||
|
const duration = howlerInstance.duration() || 0;
|
||||||
|
addTerminalLine(`Current position: ${formatTime(seek)} / ${formatTime(duration)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'status':
|
case 'status':
|
||||||
if (currentTracks.length > 0) {
|
if (currentTracks.length > 0) {
|
||||||
const track = currentTracks[0];
|
const track = currentTracks[0];
|
||||||
addTerminalLine(`Now playing: ${track.title}`);
|
addTerminalLine(`Now playing: ${track.title}`);
|
||||||
addTerminalLine(`Status: ${isPlaying ? 'Playing' : 'Paused'}`);
|
addTerminalLine(`Status: ${isPlaying ? 'Playing' : 'Paused'}`);
|
||||||
addTerminalLine(`Volume: ${Math.round(volume * 10)}/10`);
|
addTerminalLine(`Volume: ${Math.round(volume * 10)}/10`);
|
||||||
|
if (howlerInstance) {
|
||||||
|
const seek = howlerInstance.seek() || 0;
|
||||||
|
const duration = howlerInstance.duration() || 0;
|
||||||
|
const progress = duration > 0 ? ((seek / duration) * 100).toFixed(1) : 0;
|
||||||
|
addTerminalLine(`Progress: ${formatTime(seek)} / ${formatTime(duration)} (${progress}%)`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
addTerminalLine('No track loaded.');
|
addTerminalLine('No track loaded.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user