diff --git a/static/js/admin.js b/static/js/admin.js
new file mode 100644
index 0000000..74b4458
--- /dev/null
+++ b/static/js/admin.js
@@ -0,0 +1,302 @@
+// Admin Dashboard JavaScript
+let tracks = [];
+let currentTrackId = null;
+
+// Pagination variables
+let currentPage = 1;
+let tracksPerPage = 20;
+let filteredTracks = [];
+
+// Load tracks on page load
+document.addEventListener('DOMContentLoaded', function() {
+ loadTracks();
+ updatePlayerStatus();
+
+ // Setup event listeners
+ document.getElementById('scan-library').addEventListener('click', scanLibrary);
+ document.getElementById('refresh-tracks').addEventListener('click', loadTracks);
+ document.getElementById('track-search').addEventListener('input', filterTracks);
+ document.getElementById('sort-tracks').addEventListener('change', sortTracks);
+ document.getElementById('copy-files').addEventListener('click', copyFiles);
+ document.getElementById('open-incoming').addEventListener('click', openIncomingFolder);
+
+ // Player controls
+ document.getElementById('player-play').addEventListener('click', () => playTrack(currentTrackId));
+ document.getElementById('player-pause').addEventListener('click', pausePlayer);
+ document.getElementById('player-stop').addEventListener('click', stopPlayer);
+ document.getElementById('player-resume').addEventListener('click', resumePlayer);
+});
+
+// Load tracks from API
+async function loadTracks() {
+ try {
+ const response = await fetch('/admin/tracks');
+ const data = await response.json();
+
+ if (data.status === 'success') {
+ tracks = data.tracks || [];
+ document.getElementById('track-count').textContent = tracks.length;
+ displayTracks(tracks);
+ }
+ } catch (error) {
+ console.error('Error loading tracks:', error);
+ document.getElementById('tracks-container').innerHTML = '
Error loading tracks
';
+ }
+}
+
+// Display tracks in the UI with pagination
+function displayTracks(trackList) {
+ filteredTracks = trackList;
+ currentPage = 1; // Reset to first page
+ renderPage();
+}
+
+function renderPage() {
+ const container = document.getElementById('tracks-container');
+ const paginationControls = document.getElementById('pagination-controls');
+
+ if (filteredTracks.length === 0) {
+ container.innerHTML = 'No tracks found. Click "Scan Library" to add tracks.
';
+ paginationControls.style.display = 'none';
+ return;
+ }
+
+ // Calculate pagination
+ const totalPages = Math.ceil(filteredTracks.length / tracksPerPage);
+ const startIndex = (currentPage - 1) * tracksPerPage;
+ const endIndex = startIndex + tracksPerPage;
+ const tracksToShow = filteredTracks.slice(startIndex, endIndex);
+
+ // Render tracks for current page
+ const tracksHtml = tracksToShow.map(track => `
+
+
+
${track.title || 'Unknown Title'}
+
${track.artist || 'Unknown Artist'}
+
${track.album || 'Unknown Album'}
+
+
+
+
+
+
+
+ `).join('');
+
+ container.innerHTML = tracksHtml;
+
+ // Update pagination controls
+ document.getElementById('page-info').textContent = `Page ${currentPage} of ${totalPages} (${filteredTracks.length} tracks)`;
+ paginationControls.style.display = totalPages > 1 ? 'block' : 'none';
+}
+
+// Pagination functions
+function goToPage(page) {
+ const totalPages = Math.ceil(filteredTracks.length / tracksPerPage);
+ if (page >= 1 && page <= totalPages) {
+ currentPage = page;
+ renderPage();
+ }
+}
+
+function previousPage() {
+ if (currentPage > 1) {
+ currentPage--;
+ renderPage();
+ }
+}
+
+function nextPage() {
+ const totalPages = Math.ceil(filteredTracks.length / tracksPerPage);
+ if (currentPage < totalPages) {
+ currentPage++;
+ renderPage();
+ }
+}
+
+function goToLastPage() {
+ const totalPages = Math.ceil(filteredTracks.length / tracksPerPage);
+ currentPage = totalPages;
+ renderPage();
+}
+
+function changeTracksPerPage() {
+ tracksPerPage = parseInt(document.getElementById('tracks-per-page').value);
+ currentPage = 1;
+ renderPage();
+}
+
+// Scan music library
+async function scanLibrary() {
+ const statusEl = document.getElementById('scan-status');
+ const scanBtn = document.getElementById('scan-library');
+
+ statusEl.textContent = 'Scanning...';
+ scanBtn.disabled = true;
+
+ try {
+ const response = await fetch('/admin/scan-library', { method: 'POST' });
+ const data = await response.json();
+
+ if (data.status === 'success') {
+ statusEl.textContent = `â
Added ${data['tracks-added']} tracks`;
+ loadTracks(); // Refresh track list
+ } else {
+ statusEl.textContent = 'â Scan failed';
+ }
+ } catch (error) {
+ statusEl.textContent = 'â Scan error';
+ console.error('Scan error:', error);
+ } finally {
+ scanBtn.disabled = false;
+ setTimeout(() => statusEl.textContent = '', 3000);
+ }
+}
+
+// Filter tracks based on search
+function filterTracks() {
+ const query = document.getElementById('track-search').value.toLowerCase();
+ const filtered = tracks.filter(track =>
+ (track.title || '').toLowerCase().includes(query) ||
+ (track.artist || '').toLowerCase().includes(query) ||
+ (track.album || '').toLowerCase().includes(query)
+ );
+ displayTracks(filtered);
+}
+
+// Sort tracks
+function sortTracks() {
+ const sortBy = document.getElementById('sort-tracks').value;
+ const sorted = [...tracks].sort((a, b) => {
+ const aVal = a[sortBy] || '';
+ const bVal = b[sortBy] || '';
+ return aVal.localeCompare(bVal);
+ });
+ displayTracks(sorted);
+}
+
+// Audio player element
+let audioPlayer = null;
+
+// Initialize audio player
+function initAudioPlayer() {
+ if (!audioPlayer) {
+ audioPlayer = new Audio();
+ audioPlayer.addEventListener('ended', () => {
+ currentTrackId = null;
+ updatePlayerStatus();
+ });
+ audioPlayer.addEventListener('error', (e) => {
+ console.error('Audio playback error:', e);
+ alert('Error playing audio file');
+ });
+ }
+ return audioPlayer;
+}
+
+// Player functions
+async function playTrack(trackId) {
+ if (!trackId) {
+ alert('Please select a track to play');
+ return;
+ }
+
+ try {
+ const player = initAudioPlayer();
+ player.src = `/asteroid/tracks/${trackId}/stream`;
+ player.play();
+ currentTrackId = trackId;
+ updatePlayerStatus();
+ } catch (error) {
+ console.error('Play error:', error);
+ alert('Error playing track');
+ }
+}
+
+async function pausePlayer() {
+ try {
+ if (audioPlayer && !audioPlayer.paused) {
+ audioPlayer.pause();
+ updatePlayerStatus();
+ }
+ } catch (error) {
+ console.error('Pause error:', error);
+ }
+}
+
+async function stopPlayer() {
+ try {
+ if (audioPlayer) {
+ audioPlayer.pause();
+ audioPlayer.currentTime = 0;
+ currentTrackId = null;
+ updatePlayerStatus();
+ }
+ } catch (error) {
+ console.error('Stop error:', error);
+ }
+}
+
+async function resumePlayer() {
+ try {
+ if (audioPlayer && audioPlayer.paused && currentTrackId) {
+ audioPlayer.play();
+ updatePlayerStatus();
+ }
+ } catch (error) {
+ console.error('Resume error:', error);
+ }
+}
+
+async function updatePlayerStatus() {
+ try {
+ const response = await fetch('/asteroid/api/player-status');
+ const data = await response.json();
+
+ if (data.status === 'success') {
+ const player = data.player;
+ document.getElementById('player-state').textContent = player.state;
+ document.getElementById('current-track').textContent = player['current-track'] || 'None';
+ // document.getElementById('current-position').textContent = player.position;
+ }
+ } catch (error) {
+ console.error('Error updating player status:', error);
+ }
+}
+
+function streamTrack(trackId) {
+ window.open(`/asteroid/tracks/${trackId}/stream`, '_blank');
+}
+
+function deleteTrack(trackId) {
+ if (confirm('Are you sure you want to delete this track?')) {
+ // TODO: Implement track deletion API
+ alert('Track deletion not yet implemented');
+ }
+}
+
+// Copy files from incoming to library
+async function copyFiles() {
+ try {
+ const response = await fetch('/admin/copy-files');
+ const data = await response.json();
+
+ if (data.status === 'success') {
+ alert(`${data.message}`);
+ await loadTracks(); // Refresh track list
+ } else {
+ alert(`Error: ${data.message}`);
+ }
+ } catch (error) {
+ console.error('Error copying files:', error);
+ alert('Failed to copy files');
+ }
+}
+
+// Open incoming folder (for convenience)
+function openIncomingFolder() {
+ alert('Copy your MP3 files to: /home/glenn/Projects/Code/asteroid/music/incoming/\n\nThen click "Copy Files to Library" to add them to your music collection.');
+}
+
+// Update player status every 5 seconds
+setInterval(updatePlayerStatus, 5000);
diff --git a/template/admin.chtml b/template/admin.chtml
index 9d62054..807379d 100644
--- a/template/admin.chtml
+++ b/template/admin.chtml
@@ -5,6 +5,7 @@
+
@@ -109,13 +110,13 @@
đĩ Player Control
-
-
-
-
+
+
+
+
- Status: Unknown
+ Status: Unknown
Current Track: None
@@ -131,308 +132,6 @@