Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions public/assets/js/player-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Player state manager for persistent music playback
class PlayerState {
constructor() {
this.currentSong = null;
this.isPlaying = false;
this.currentTime = 0;
this.duration = 0;
this.loadFromStorage();
this.initializePlayer();
}

// load saved state from localStorage
loadFromStorage() {
try {
const saved = localStorage.getItem('playerState');
if (saved) {
const state = JSON.parse(saved);
this.currentSong = state.currentSong;
this.currentTime = state.currentTime || 0;
}
} catch (e) {
console.error('failed to load player state:', e);
}
}

// save current state to localStorage
saveToStorage() {
try {
localStorage.setItem('playerState', JSON.stringify({
currentSong: this.currentSong,
currentTime: this.currentTime,
isPlaying: this.isPlaying
}));
} catch (e) {
console.error('failed to save player state:', e);
}
}

// initialize player on page load
initializePlayer() {
document.addEventListener('DOMContentLoaded', () => {
const audio = document.getElementById('audio');
if (!audio) return;

// restore previous song if exists
if (this.currentSong) {
this.updateAudioElement();
this.showPlayer();
}

// save current time periodically
audio.addEventListener('timeupdate', () => {
this.currentTime = audio.currentTime;
// debounce storage writes
if (!this._saveTimeout) {
this._saveTimeout = setTimeout(() => {
this.saveToStorage();
this._saveTimeout = null;
}, 2000);
}
});

// update duration
audio.addEventListener('loadedmetadata', () => {
this.duration = audio.duration;
});

// track play/pause state
audio.addEventListener('play', () => {
this.isPlaying = true;
this.saveToStorage();
});

audio.addEventListener('pause', () => {
this.isPlaying = false;
this.saveToStorage();
});
});
}

// load a new song into player
loadSong(musicId, title, artist) {
this.currentSong = {
id: musicId,
title: title,
artist: artist,
streamUrl: `/music/stream?id=${musicId}`
};
this.currentTime = 0;
this.saveToStorage();
this.updateAudioElement();
this.showPlayer();
this.autoPlay();
}

// update the audio element with current song
updateAudioElement() {
const audio = document.getElementById('audio');
if (!audio || !this.currentSong) return;

// only change source if its different
if (audio.src !== window.location.origin + this.currentSong.streamUrl) {
audio.src = this.currentSong.streamUrl;
}

audio.currentTime = this.currentTime;

// update UI elements
const titleEl = document.getElementById('currentSongTitle');
const artistEl = document.getElementById('currentArtist');

if (titleEl) titleEl.textContent = this.currentSong.title;
if (artistEl) artistEl.textContent = this.currentSong.artist;
}

// show the persistent player
showPlayer() {
const player = document.getElementById('persistent-player');
if (player) {
player.style.display = 'block';
}
}

// hide the persistent player
hidePlayer() {
const player = document.getElementById('persistent-player');
if (player) {
player.style.display = 'none';
}
}

// auto-play after loading
autoPlay() {
const audio = document.getElementById('audio');
if (!audio) return;

// small delay to ensure audio is ready
setTimeout(() => {
audio.play().catch(err => {
console.log('autoplay prevented:', err);
// browser blocked autoplay, user needs to click play
});
}, 100);
}

// toggle play/pause
togglePlay() {
const audio = document.getElementById('audio');
if (!audio || !this.currentSong) return;

if (this.isPlaying) {
audio.pause();
} else {
audio.play();
}
}

// get current song info
getCurrentSong() {
return this.currentSong;
}
}

// create global instance
window.playerState = new PlayerState();

// helper function for loading songs from onclick attributes
window.loadSong = (id, title, artist) => {
window.playerState.loadSong(id, title, artist);
};
12 changes: 9 additions & 3 deletions public/assets/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ window.jumpTo = function (seconds) {
document.addEventListener("DOMContentLoaded", () => {
const audio = document.getElementById("audio");
const playBtn = document.getElementById("playBtn");
const icon = playBtn.querySelector("i");
const bar = document.getElementById("progressBar");
const barCont = document.getElementById("progressContainer");
const timestampInput = document.getElementById("timestampInput");
const popup = document.getElementById("popup");
const popupUser = document.getElementById("popupUser");
const popupContent = document.getElementById("popupContent");

if (!audio || !playBtn || !bar || !barCont) return; // skip if essential elements missing

const icon = playBtn.querySelector("i");
let isPlaying = false;
let markersCreated = false;

Expand Down Expand Up @@ -93,7 +95,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (timestampInput) timestampInput.value = Math.floor(audio.currentTime);

const currentSec = audio.currentTime;
if (typeof commentsData !== "undefined") {
if (typeof commentsData !== "undefined" && popup && popupUser && popupContent) {
const activeComment = commentsData.find(
(c) => Math.abs(c.timestamp - currentSec) < 0.5,
);
Expand All @@ -113,7 +115,11 @@ document.addEventListener("DOMContentLoaded", () => {
audio.currentTime = pct * audio.duration;
});

init3D();
// only init 3D if canvas container exists (fullscreen page only)
const canvasContainer = document.getElementById("canvas-container");
if (canvasContainer) {
init3D();
}
});

let scene, camera, renderer, geometry, mesh, context, analyser, dataArray;
Expand Down
8 changes: 6 additions & 2 deletions public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
(new MusicController())->show();
break;

case '/music/stream':
(new MusicController())->stream();
case '/music/stream':
(new MusicController())->stream();
break;

case '/music/details':
(new MusicController())->details();
break;

// Auth
Expand Down
39 changes: 32 additions & 7 deletions src/Controllers/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,41 @@
namespace App\Controllers;

class Controller {
protected function render($view, $data = []) {
extract($data); // Converts array keys to variables ($music, $comments, etc.)

// Check if the view file exists
protected function render($view, $data = [], $layout = 'layouts/base') {
// extract data for use in views
extract($data);

// check if htmx request
$isHtmxRequest = isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true';

// get view content
$viewFile = __DIR__ . "/../Views/$view.php";
if (file_exists($viewFile)) {
require $viewFile;
} else {
if (!file_exists($viewFile)) {
die("View '$view' not found!");
}

// capture view output
ob_start();
require $viewFile;
$content = ob_get_clean();

// if htmx request, return content only
if ($isHtmxRequest) {
echo $content;
return;
}

// otherwise wrap in layout
if ($layout !== false) {
$layoutFile = __DIR__ . "/../Views/$layout.php";
if (file_exists($layoutFile)) {
require $layoutFile;
} else {
echo $content; // fallback if layout missing
}
} else {
echo $content;
}
}

protected function redirect($url) {
Expand Down
52 changes: 51 additions & 1 deletion src/Controllers/MusicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,63 @@ public function show() {
$comments = $commentModel->getAllForMusic($musicId);
$openDrawer = (isset($_GET['drawer']) && $_GET['drawer'] === 'open') ? 'open' : '';

// Render View
// Render fullscreen visualization (no layout)
$this->render('music/show', [
'music' => $music,
'comments' => $comments,
'avgRating' => $avgRating,
'openDrawer' => $openDrawer,
'isUserLoggedIn' => $userId ? true : false
], false);
}

public function details() {
// music details page with comments and info
if (!isset($_GET['id'])) die("ID manquant");
$musicId = (int)$_GET['id'];

// session check for user
if (session_status() === PHP_SESSION_NONE) session_start();
$userId = $_SESSION['user_id'] ?? null;

$pdo = Database::getConnection();
$musicModel = new Music($pdo);
$commentModel = new Comment($pdo);

// handle POST actions (comments & ratings)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $userId) {
// comments
if (isset($_POST['comment'])) {
$content = trim($_POST['comment']);
$timestamp = (int)($_POST['timestamp'] ?? 0);
if (!empty($content)) {
$commentModel->create($userId, $musicId, $content, $timestamp);
$this->redirect("/music/details?id=$musicId");
}
}
// ratings
if (isset($_POST['rating'])) {
$val = (int)$_POST['rating'];
if ($val >= 1 && $val <= 5) {
$musicModel->addRating($userId, $musicId, $val);
$this->redirect("/music/details?id=$musicId");
}
}
}

// fetch data
$music = $musicModel->findById($musicId);
if (!$music) die("Musique introuvable");

$avgRating = $musicModel->getAvgRating($musicId);
$comments = $commentModel->getAllForMusic($musicId);

// render details page with layout
$this->render('music/details', [
'music' => $music,
'comments' => $comments,
'avgRating' => $avgRating,
'isUserLoggedIn' => $userId ? true : false
]);
}
}
Loading