diff --git a/public/assets/js/player-state.js b/public/assets/js/player-state.js new file mode 100644 index 0000000..ad2e62f --- /dev/null +++ b/public/assets/js/player-state.js @@ -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); +}; diff --git a/public/assets/player.js b/public/assets/player.js index ce89990..3ab3d45 100644 --- a/public/assets/player.js +++ b/public/assets/player.js @@ -36,7 +36,6 @@ 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"); @@ -44,6 +43,9 @@ document.addEventListener("DOMContentLoaded", () => { 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; @@ -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, ); @@ -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; diff --git a/public/index.php b/public/index.php index c6bbc9d..e079a4e 100644 --- a/public/index.php +++ b/public/index.php @@ -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 diff --git a/src/Controllers/Controller.php b/src/Controllers/Controller.php index 43adaf7..79556ce 100644 --- a/src/Controllers/Controller.php +++ b/src/Controllers/Controller.php @@ -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) { diff --git a/src/Controllers/MusicController.php b/src/Controllers/MusicController.php index 0286ee5..cecfc95 100644 --- a/src/Controllers/MusicController.php +++ b/src/Controllers/MusicController.php @@ -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 ]); } } diff --git a/src/Views/admin/index.php b/src/Views/admin/index.php index 5f18b06..2389084 100644 --- a/src/Views/admin/index.php +++ b/src/Views/admin/index.php @@ -1,196 +1,172 @@ - - - - - Administration - Tempo - - - - - + -
- -
- - -
-
+
+

Panneau d'Administration

+

Gérez les utilisateurs et le contenu de la plateforme.

-
-

Panneau d'Administration

-

Gérez les utilisateurs et le contenu de la plateforme.

- -
-
- Utilisateurs -
-
- Musiques -
+
+
+ Utilisateurs +
+
+ Musiques
+
+ +
+ +
+

+ Utilisateurs +

-
- -
-

- Utilisateurs -

- -
- - - - - - - - - - - - - + + + +
UtilisateurEmailRôleActions
-
-
- -
- +
+ + + + + + + + + + + + + - - + + + - - - - -
UtilisateurEmailRôleActions
+
+
+
-
+ + + + + ADMIN + + Membre + + + - ADMIN + + Révoquer + - Membre + + Promouvoir + + + Bannir + - - - - - Révoquer - - - - Promouvoir - - - Bannir - - - - Vous - -
-
+ + Vous + +
+
-
-

- Musiques -

+
+

+ Musiques +

-
- - - - - - - - - - - - - - - - - - - -
TitreAuteurDateActions
- - - - - - - - - - -
-
+
+ + + + + + + + + + + + + + + + + + + +
TitreAuteurDateActions
+ + + + + + + + + + +
-
- - - \ No newline at end of file +
diff --git a/src/Views/auth/login.php b/src/Views/auth/login.php index dffd5e7..609b637 100644 --- a/src/Views/auth/login.php +++ b/src/Views/auth/login.php @@ -1,46 +1,23 @@ - - - - - Connexion - Tempo - - - +
+

Connexion

-
- -
- - Connexion -
-
- + +
+ +
+ -
-

Connexion

+
+
+ +
+
+ +
+ +
- -
- -
- - -
-
- -
-
- -
- -
- -

- Pas encore de compte ? Inscrivez-vous -

-
- - - +

+ Pas encore de compte ? Inscrivez-vous +

+
diff --git a/src/Views/auth/register.php b/src/Views/auth/register.php index 550eca7..c6219ed 100644 --- a/src/Views/auth/register.php +++ b/src/Views/auth/register.php @@ -1,45 +1,26 @@ - - - - - Inscription - Tempo - - - +
+

Inscription

-
- - Connexion -
+ +
+ +
+ -
-

Inscription

+
+
+ +
+
+ +
+
+ +
+ +
- -
- -
- - -
-
- -
-
- -
-
- -
- -
- -

- Déjà un compte ? Connectez-vous -

-
- - - +

+ Déjà un compte ? Connectez-vous +

+
diff --git a/src/Views/auth/verify.php b/src/Views/auth/verify.php index b8436b5..f05c754 100644 --- a/src/Views/auth/verify.php +++ b/src/Views/auth/verify.php @@ -1,11 +1,5 @@ - - -Vérification - -
-

-

- Se connecter -
- - +
+

+

+ Se connecter +
diff --git a/src/Views/components/player.php b/src/Views/components/player.php new file mode 100644 index 0000000..a536f41 --- /dev/null +++ b/src/Views/components/player.php @@ -0,0 +1,180 @@ + + + + + + + + + diff --git a/src/Views/dashboard/index.php b/src/Views/dashboard/index.php index 47f265c..a5325bb 100644 --- a/src/Views/dashboard/index.php +++ b/src/Views/dashboard/index.php @@ -1,163 +1,121 @@ - - - - - - - - - - - - - - - - Mon Espace - Tempo - - - - - - -
- -
- - -
-
- - -
- -
- -
-
- - - -
- -
- -

- -

-

- -

- - -
- - - -
-
+ + +
+ +
+ +
+
+ + + +
+ +
+ +

+ +

+

+ +

+ + +
+ + + +
+
+ + +
+ +
+ + +
+
+

Ajouter une musique

+ +
+ - -
- -
- - -
-
-

Ajouter une musique

- - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + +
+
+ +
+

Mes Musiques ()

-
-

Mes Musiques ()

- - 0): ?> -
- -
-
-

- - - • Ajouté le - - -
- - + 0): ?> +
+ +
+
+

+ + + • Ajouté le + +
- -
- -
-

Vous n'avez pas encore posté de musique.

-
- -
+ +
+ +
+ +
+

Vous n'avez pas encore posté de musique.

+
+
- - + +
diff --git a/src/Views/home/index.php b/src/Views/home/index.php index 28274e9..d6f0431 100644 --- a/src/Views/home/index.php +++ b/src/Views/home/index.php @@ -1,152 +1,75 @@ - - - - - - - - Tempo - Accueil + - - + .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 30px; max-width: 1200px; margin: 0 auto; padding: 0 20px 60px; } + .card { background: var(--bg-card); border-radius: 20px; overflow: hidden; box-shadow: 0 10px 30px var(--shadow); width: 100%; max-width: 350px; margin: 0 auto; display: flex; flex-direction: column; } + .card-img { height: 180px; background: var(--bg-input); display: flex; align-items: center; justify-content: center; font-size: 3rem; color: var(--text-muted); overflow:hidden; } + .card-body { padding: 20px; flex-grow: 1; display:flex; flex-direction:column; } + .card-title { font-weight: 800; margin: 0 0 5px 0; font-size: 1.2rem; color: var(--text-main); } + .card-user { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 15px; } + .card-title-link { color: inherit; text-decoration: none; } + .card-title-link:hover { color: var(--primary); } + -
- -
- +
+

Accueil

+ +
- -
-
+ +
+

+ + + +

-
-
-

Accueil

- -
- -
- -
-
- - Cover de <?= htmlspecialchars($m['title']); ?> - - 💿 - -
-
-

- -
-
- - <?= htmlspecialchars($m['username']); ?> - -
- -
- -
- +
+
+ + <?= htmlspecialchars($m['username']); ?> + +
+ +
+
- Écouter ▶ +
-
- -
-
- - - - - + +
+
+ +
diff --git a/src/Views/layouts/base.php b/src/Views/layouts/base.php new file mode 100644 index 0000000..1013ce4 --- /dev/null +++ b/src/Views/layouts/base.php @@ -0,0 +1,148 @@ + + + + + + + + <?= $pageTitle ?? 'Tempo - Communauté Musicale'; ?> + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+
+ +
+ +
+ + + + + + + + + + + diff --git a/src/Views/music/details.php b/src/Views/music/details.php new file mode 100644 index 0000000..9fc24d1 --- /dev/null +++ b/src/Views/music/details.php @@ -0,0 +1,274 @@ + + +
+
+

+ +
+ + <?= htmlspecialchars($music['username']); ?> + +
+ +
+ +
+ +
+
+ +
+ + + + Open Visualization + +
+ +
+
+ + + +
+
+ / 5 +
+
+
+ +
+

Comments ()

+ + 0): ?> + +
+
+ [] +
+ + <?= htmlspecialchars($c['username']); ?> + +
+ +
+ + +
+
+
+ +
+
+ + +

+ No comments yet. Be the first to comment! +

+ + + +
+
+ + + +
+

+ Tip: Comments are timestamped at the current playback position +

+
+ + + +
+
+ + + + diff --git a/src/Views/music/show.php b/src/Views/music/show.php index d5b7186..03b423e 100644 --- a/src/Views/music/show.php +++ b/src/Views/music/show.php @@ -3,9 +3,9 @@ - - - <?= htmlspecialchars($music['title']); ?> - Tempo + + + <?= htmlspecialchars($music['title']); ?> - Tempo Visualization - + - + @@ -34,11 +34,11 @@
- +
@@ -52,7 +52,7 @@

- <?= htmlspecialchars($music['username']); ?> @@ -65,7 +65,7 @@
- +
@@ -74,7 +74,7 @@
@@ -88,16 +88,16 @@
-
- +
TIMELINE
- +
@@ -105,7 +105,7 @@ []
- <?= htmlspecialchars($c['username']); ?>
- +
- + - + + const commentsData = ; + window.isUserLoggedIn = ; + + // sync with persistent player state + document.addEventListener('DOMContentLoaded', () => { + const audio = document.getElementById('audio'); + if (!audio) return; + + // check if there's saved player state + try { + const savedState = localStorage.getItem('playerState'); + if (savedState) { + const state = JSON.parse(savedState); + + // if saved song matches current song, restore position + if (state.currentSong && state.currentSong.id == ) { + audio.currentTime = state.currentTime || 0; + + // auto-play if it was playing before + if (state.isPlaying) { + setTimeout(() => { + audio.play().catch(e => console.log('autoplay prevented')); + }, 100); + } + } + } + } catch (e) { + console.log('failed to restore state:', e); + } + + // save state periodically while playing + let saveTimeout; + audio.addEventListener('timeupdate', () => { + if (!saveTimeout) { + saveTimeout = setTimeout(() => { + try { + localStorage.setItem('playerState', JSON.stringify({ + currentSong: { + id: , + title: '', + artist: '', + streamUrl: '/music/stream?id=' + }, + currentTime: audio.currentTime, + isPlaying: !audio.paused + })); + } catch (e) {} + saveTimeout = null; + }, 2000); + } + }); + }); + - - + + diff --git a/src/Views/music/visualize.php b/src/Views/music/visualize.php new file mode 100644 index 0000000..5d40402 --- /dev/null +++ b/src/Views/music/visualize.php @@ -0,0 +1,153 @@ + + + + + + + + <?= htmlspecialchars($music['title']); ?> - Tempo Visualization + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+
+ +
+

+
+ + <?= htmlspecialchars($music['username']); ?> + +
+ +
+ +
+ +
+
+ +
+ + + +
+
+ + + +
+ +
+
+
+
+ 00:00 +
+
+
+ +
+
+ +
+
+ TIMELINE + +
+ +
+ +
+
+ [] +
+ + <?= htmlspecialchars($c['username']); ?> + +
+ +
+ + +
+
+
+
+ +
+ + +
+ + + + + + + + + + +