diff --git a/js/app.js b/js/app.js
index f950ffa..2ffd530 100644
--- a/js/app.js
+++ b/js/app.js
@@ -5,7 +5,7 @@ import { apiSettings, themeManager, nowPlayingSettings, trackListSettings } from
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
import { LastFMScrobbler } from './lastfm.js';
-import { LyricsManager, openLyricsPanel, clearLyricsPanelSync } from './lyrics.js';
+import { LyricsManager, openLyricsPanel, clearLyricsPanelSync, renderLyricsInFullscreen, clearFullscreenLyricsSync } from './lyrics.js';
import { createRouter, updateTabTitle } from './router.js';
import { initializeSettings } from './settings.js';
import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js';
@@ -13,6 +13,8 @@ import { initializeUIInteractions } from './ui-interactions.js';
import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip } from './downloads.js';
import { debounce, SVG_PLAY } from './utils.js';
import { sidePanelManager } from './side-panel.js';
+import { db } from './db.js';
+import { syncManager } from './firebase/sync.js';
function initializeCasting(audioPlayer, castBtn) {
if (!castBtn) return;
@@ -229,7 +231,7 @@ document.addEventListener('DOMContentLoaded', async () => {
ui.closeFullscreenCover();
} else {
const nextTrack = player.getNextTrack();
- ui.showFullscreenCover(player.currentTrack, nextTrack);
+ ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, audioPlayer);
}
} else {
// Default to 'album' mode - navigate to album
@@ -292,7 +294,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const fullscreenOverlay = document.getElementById('fullscreen-cover-overlay');
if (fullscreenOverlay && getComputedStyle(fullscreenOverlay).display !== 'none') {
const nextTrack = player.getNextTrack();
- ui.showFullscreenCover(player.currentTrack, nextTrack);
+ ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, audioPlayer);
}
});
@@ -338,6 +340,111 @@ document.addEventListener('DOMContentLoaded', async () => {
btn.innerHTML = originalHTML;
}
}
+ if (e.target.closest('#create-playlist-btn')) {
+ const modal = document.getElementById('playlist-modal');
+ document.getElementById('playlist-modal-title').textContent = 'Create Playlist';
+ document.getElementById('playlist-name-input').value = '';
+ modal.style.display = 'flex';
+ document.getElementById('playlist-name-input').focus();
+}
+
+if (e.target.closest('#playlist-modal-save')) {
+ const name = document.getElementById('playlist-name-input').value.trim();
+ if (name) {
+ const modal = document.getElementById('playlist-modal');
+ const editingId = modal.dataset.editingId;
+ if (editingId) {
+ // Edit
+ db.getPlaylist(editingId).then(async (playlist) => {
+ if (playlist) {
+ playlist.name = name;
+ await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
+ syncManager.syncUserPlaylist(playlist, 'update');
+ ui.renderLibraryPage();
+ modal.style.display = 'none';
+ delete modal.dataset.editingId;
+ }
+ });
+ } else {
+ // Create
+ db.createPlaylist(name, [], '').then(playlist => {
+ syncManager.syncUserPlaylist(playlist, 'create');
+ ui.renderLibraryPage();
+ modal.style.display = 'none';
+ });
+ }
+ }
+}
+
+
+if (e.target.closest('#playlist-modal-cancel')) {
+ document.getElementById('playlist-modal').style.display = 'none';
+}
+
+if (e.target.closest('.edit-playlist-btn')) {
+ const card = e.target.closest('.user-playlist');
+ const playlistId = card.dataset.playlistId;
+ db.getPlaylist(playlistId).then(playlist => {
+ if (playlist) {
+ const modal = document.getElementById('playlist-modal');
+ document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
+ document.getElementById('playlist-name-input').value = playlist.name;
+ modal.dataset.editingId = playlistId;
+ modal.style.display = 'flex';
+ document.getElementById('playlist-name-input').focus();
+ }
+ });
+}
+if (e.target.closest('.delete-playlist-btn')) {
+ const card = e.target.closest('.user-playlist');
+ const playlistId = card.dataset.playlistId;
+ if (confirm('Are you sure you want to delete this playlist?')) {
+ db.deletePlaylist(playlistId).then(() => {
+ syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
+ ui.renderLibraryPage();
+ });
+ }
+}
+
+if (e.target.closest('#edit-playlist-btn')) {
+ const playlistId = window.location.hash.split('/')[1];
+ db.getPlaylist(playlistId).then(playlist => {
+ if (playlist) {
+ const modal = document.getElementById('playlist-modal');
+ document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
+ document.getElementById('playlist-name-input').value = playlist.name;
+ modal.dataset.editingId = playlistId;
+ modal.style.display = 'flex';
+ document.getElementById('playlist-name-input').focus();
+ }
+ });
+}
+
+if (e.target.closest('#delete-playlist-btn')) {
+ const playlistId = window.location.hash.split('/')[1];
+ if (confirm('Are you sure you want to delete this playlist?')) {
+ db.deletePlaylist(playlistId).then(() => {
+ window.location.hash = '#library';
+ });
+ }
+}
+
+
+
+ if (e.target.closest('.remove-from-playlist-btn')) {
+ const btn = e.target.closest('.remove-from-playlist-btn');
+ const index = parseInt(btn.dataset.trackIndex);
+ const playlistId = window.location.hash.split('/')[1];
+ db.getPlaylist(playlistId).then(async (playlist) => {
+ if (playlist && playlist.tracks[index]) {
+ const trackId = playlist.tracks[index].id;
+ await db.removeTrackFromPlaylist(playlistId, trackId);
+ ui.renderPlaylistPage(playlistId);
+ }
+ });
+}
+
+
if (e.target.closest('#play-playlist-btn')) {
const btn = e.target.closest('#play-playlist-btn');
if (btn.disabled) return;
@@ -346,7 +453,14 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!playlistId) return;
try {
- const { tracks } = await api.getPlaylist(playlistId);
+ let tracks;
+ const userPlaylist = await db.getPlaylist(playlistId);
+ if (userPlaylist) {
+ tracks = userPlaylist.tracks;
+ } else {
+ const { tracks: apiTracks } = await api.getPlaylist(playlistId);
+ tracks = apiTracks;
+ }
if (tracks.length > 0) {
player.setQueue(tracks, 0);
document.getElementById('shuffle-btn').classList.remove('active');
diff --git a/js/db.js b/js/db.js
index eff82fa..7e344b9 100644
--- a/js/db.js
+++ b/js/db.js
@@ -1,7 +1,7 @@
export class MusicDatabase {
constructor() {
this.dbName = 'MonochromeDB';
- this.version = 3;
+ this.version = 4;
this.db = null;
}
@@ -45,6 +45,10 @@ export class MusicDatabase {
const store = db.createObjectStore('history_tracks', { keyPath: 'timestamp' });
store.createIndex('timestamp', 'timestamp', { unique: true });
}
+ if (!db.objectStoreNames.contains('user_playlists')) {
+ const store = db.createObjectStore('user_playlists', { keyPath: 'id' });
+ store.createIndex('createdAt', 'createdAt', { unique: false });
+ }
};
});
}
@@ -220,12 +224,14 @@ export class MusicDatabase {
const playlists = await this.getFavorites('playlist');
const history = await this.getHistory();
+ const userPlaylists = await this.getPlaylists();
const data = {
favorites_tracks: tracks.map(t => this._minifyItem('track', t)),
favorites_albums: albums.map(a => this._minifyItem('album', a)),
favorites_artists: artists.map(a => this._minifyItem('artist', a)),
favorites_playlists: playlists.map(p => this._minifyItem('playlist', p)),
- history_tracks: history.map(t => this._minifyItem('track', t))
+ history_tracks: history.map(t => this._minifyItem('track', t)),
+ user_playlists: userPlaylists
};
return data;
}
@@ -255,6 +261,73 @@ export class MusicDatabase {
await importStore('favorites_artists', data.favorites_artists);
await importStore('favorites_playlists', data.favorites_playlists);
await importStore('history_tracks', data.history_tracks);
+ if (data.user_playlists) {
+ await importStore('user_playlists', data.user_playlists);
+ }
+ }
+
+ // User Playlists API
+ async createPlaylist(name, tracks = [], cover = '') {
+ const id = crypto.randomUUID();
+ const playlist = {
+ id: id,
+ name: name,
+ tracks: tracks.map(t => this._minifyItem('track', t)),
+ cover: cover,
+ createdAt: Date.now()
+ };
+ await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
+ return playlist;
+ }
+
+ async addTrackToPlaylist(playlistId, track) {
+ const playlist = await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
+ if (!playlist) throw new Error('Playlist not found');
+ playlist.tracks = playlist.tracks || [];
+ const minifiedTrack = this._minifyItem('track', track);
+ if (playlist.tracks.some(t => t.id === track.id)) return;
+ playlist.tracks.push(minifiedTrack);
+ await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
+ return playlist;
+ }
+
+ async removeTrackFromPlaylist(playlistId, trackId) {
+ const playlist = await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
+ if (!playlist) throw new Error('Playlist not found');
+ playlist.tracks = playlist.tracks || [];
+ playlist.tracks = playlist.tracks.filter(t => t.id !== trackId);
+ await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
+ return playlist;
+ }
+
+ async deletePlaylist(playlistId) {
+ await this.performTransaction('user_playlists', 'readwrite', (store) => store.delete(playlistId));
+ }
+
+ async getPlaylist(playlistId) {
+ return await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
+ }
+
+ async getPlaylists() {
+ const db = await this.open();
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction('user_playlists', 'readonly');
+ const store = transaction.objectStore('user_playlists');
+ const index = store.index('createdAt');
+ const request = index.getAll();
+ request.onsuccess = () => {
+ resolve(request.result.reverse()); // Newest first
+ };
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ async updatePlaylistName(playlistId, newName) {
+ const playlist = await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
+ if (!playlist) throw new Error('Playlist not found');
+ playlist.name = newName;
+ await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
+ return playlist;
}
}
diff --git a/js/events.js b/js/events.js
index 3a698d8..c0d3ba0 100644
--- a/js/events.js
+++ b/js/events.js
@@ -336,6 +336,9 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
} else if (type === 'playlist') {
const data = await api.getPlaylist(item.uuid);
tracks = data.tracks;
+ } else if (type === 'user-playlist') {
+ const playlist = await db.getPlaylist(item.id);
+ tracks = playlist ? playlist.tracks : [];
}
if (tracks.length > 0) {
@@ -343,7 +346,8 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
const shuffleBtn = document.getElementById('shuffle-btn');
if (shuffleBtn) shuffleBtn.classList.remove('active');
player.playAtIndex(0);
- showNotification(`Playing ${type}: ${item.title}`);
+ const name = type === 'user-playlist' ? item.name : item.title;
+ showNotification(`Playing ${type.replace('user-', '')}: ${name}`);
} else {
showNotification(`No tracks found in this ${type}`);
}
@@ -430,6 +434,46 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
}
}
}
+ } else if (action === 'add-to-playlist') {
+ const playlists = await db.getPlaylists();
+ if (playlists.length === 0) {
+ showNotification('No playlists yet. Create one first.');
+ return;
+ }
+
+ const modal = document.createElement('div');
+ modal.className = 'playlist-select-modal';
+ modal.innerHTML = `
+
+
+
Add to Playlist
+
+ ${playlists.map(p => `
${p.name}
`).join('')}
+
+
+
+
+
+
+ `;
+ document.body.appendChild(modal);
+
+ modal.addEventListener('click', async (e) => {
+ if (e.target.id === 'cancel-add-playlist') {
+ modal.remove();
+ return;
+ }
+
+ const option = e.target.closest('.playlist-option');
+ if (option) {
+ const playlistId = option.dataset.id;
+ await db.addTrackToPlaylist(playlistId, item);
+ const updatedPlaylist = await db.getPlaylist(playlistId);
+ syncManager.syncUserPlaylist(updatedPlaylist, 'update');
+ showNotification(`Added to playlist: ${option.textContent}`);
+ modal.remove();
+ }
+ });
}
}
diff --git a/js/firebase/sync.js b/js/firebase/sync.js
index 5b46d35..62dc30b 100644
--- a/js/firebase/sync.js
+++ b/js/firebase/sync.js
@@ -60,7 +60,8 @@ export class SyncManager {
favorites_albums: mergedData.library?.albums ? Object.values(mergedData.library.albums) : [],
favorites_artists: mergedData.library?.artists ? Object.values(mergedData.library.artists) : [],
favorites_playlists: mergedData.library?.playlists ? Object.values(mergedData.library.playlists) : [],
- history_tracks: mergedData.history?.recentTracks ? Object.values(mergedData.history.recentTracks) : []
+ history_tracks: mergedData.history?.recentTracks ? Object.values(mergedData.history.recentTracks) : [],
+ user_playlists: mergedData.user_playlists ? Object.values(mergedData.user_playlists) : []
};
await db.importData(importData, true);
@@ -119,6 +120,7 @@ export class SyncManager {
history: {
recentTracks: this.arrayToObject(mergeStores(local.history_tracks, cloud.history?.recentTracks, 'timestamp'), 'timestamp')
},
+ user_playlists: this.arrayToObject(mergeStores(local.user_playlists, cloud.user_playlists), 'id'),
// Settings are NOT synced (device specific)
lastUpdated: Date.now()
};
@@ -181,6 +183,26 @@ export class SyncManager {
});
this.unsubscribeFunctions.push(() => off(historyRef, 'value', unsubHistory));
+
+ // Listen for changes in user playlists
+ const userPlaylistsRef = child(this.userRef, 'user_playlists');
+
+ const unsubUserPlaylists = onValue(userPlaylistsRef, (snapshot) => {
+ if (this.isSyncing) return;
+
+ const val = snapshot.val();
+ if (val) {
+ const importData = {
+ user_playlists: Object.values(val)
+ };
+ db.importData(importData, true).then(() => {
+ // Notify UI to refresh library
+ window.dispatchEvent(new Event('library-changed'));
+ });
+ }
+ });
+
+ this.unsubscribeFunctions.push(() => off(userPlaylistsRef, 'value', unsubUserPlaylists));
}
// --- Public API for Broadcasters ---
@@ -232,6 +254,20 @@ export class SyncManager {
}
}
+ async syncUserPlaylist(playlist, action) {
+ if (!this.user || !this.userRef) return;
+
+ const id = playlist.id;
+ const path = `user_playlists/${id}`;
+ const itemRef = child(this.userRef, path);
+
+ if (action === 'create' || action === 'update') {
+ await set(itemRef, playlist);
+ } else if (action === 'delete') {
+ await remove(itemRef);
+ }
+ }
+
async clearCloudData() {
if (!this.user || !this.userRef) {
throw new Error("Not authenticated");
diff --git a/js/lyrics.js b/js/lyrics.js
index 015f4f3..22a31db 100644
--- a/js/lyrics.js
+++ b/js/lyrics.js
@@ -312,3 +312,118 @@ export function clearLyricsPanelSync(audioPlayer, panel) {
panel.lyricsCleanup = null;
}
}
+
+
+export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {
+ container.innerHTML = '
Loading lyrics...
';
+
+ try {
+ await lyricsManager.ensureComponentLoaded();
+
+ const title = track.title;
+ const artist = getTrackArtists(track);
+ const album = track.album?.title;
+ const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined;
+ const isrc = track.isrc || '';
+
+ container.innerHTML = '';
+ const amLyrics = document.createElement('am-lyrics');
+ amLyrics.setAttribute('song-title', title);
+ amLyrics.setAttribute('song-artist', artist);
+ if (album) amLyrics.setAttribute('song-album', album);
+ if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
+ amLyrics.setAttribute('query', `${title} ${artist}`.trim());
+ if (isrc) amLyrics.setAttribute('isrc', isrc);
+
+ amLyrics.setAttribute('highlight-color', '#93c5fd');
+ amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
+ amLyrics.setAttribute('autoscroll', '');
+ amLyrics.setAttribute('interpolate', '');
+ amLyrics.style.height = '100%';
+ amLyrics.style.width = '100%';
+
+ container.appendChild(amLyrics);
+
+ setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager, amLyrics);
+
+ return amLyrics;
+ } catch (error) {
+ console.error('Failed to load lyrics in fullscreen:', error);
+ container.innerHTML = '
Failed to load lyrics
';
+ return null;
+ }
+}
+
+function setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager, amLyrics) {
+ let baseTimeMs = 0;
+ let lastTimestamp = performance.now();
+
+ const updateTime = () => {
+ const currentMs = audioPlayer.currentTime * 1000;
+ baseTimeMs = currentMs;
+ lastTimestamp = performance.now();
+ amLyrics.currentTime = currentMs;
+ };
+
+ const tick = () => {
+ if (!audioPlayer.paused) {
+ const now = performance.now();
+ const elapsed = now - lastTimestamp;
+ const nextMs = baseTimeMs + elapsed;
+ amLyrics.currentTime = nextMs;
+ lyricsManager.fullscreenAnimationFrameId = requestAnimationFrame(tick);
+ }
+ };
+
+ const onPlay = () => {
+ baseTimeMs = audioPlayer.currentTime * 1000;
+ lastTimestamp = performance.now();
+ tick();
+ };
+
+ const onPause = () => {
+ if (lyricsManager.fullscreenAnimationFrameId) {
+ cancelAnimationFrame(lyricsManager.fullscreenAnimationFrameId);
+ lyricsManager.fullscreenAnimationFrameId = null;
+ }
+ };
+
+ audioPlayer.addEventListener('timeupdate', updateTime);
+ audioPlayer.addEventListener('play', onPlay);
+ audioPlayer.addEventListener('pause', onPause);
+ audioPlayer.addEventListener('seeked', updateTime);
+
+ container.lyricsUpdateHandler = updateTime;
+ container.lyricsPlayHandler = onPlay;
+ container.lyricsPauseHandler = onPause;
+ container.lyricsSeekHandler = updateTime;
+
+ container.lyricsCleanup = () => {
+ if (lyricsManager.fullscreenAnimationFrameId) {
+ cancelAnimationFrame(lyricsManager.fullscreenAnimationFrameId);
+ lyricsManager.fullscreenAnimationFrameId = null;
+ }
+ audioPlayer.removeEventListener('timeupdate', updateTime);
+ audioPlayer.removeEventListener('play', onPlay);
+ audioPlayer.removeEventListener('pause', onPause);
+ audioPlayer.removeEventListener('seeked', updateTime);
+ };
+
+ amLyrics.addEventListener('line-click', (e) => {
+ if (e.detail && e.detail.timestamp) {
+ audioPlayer.currentTime = e.detail.timestamp / 1000;
+ audioPlayer.play();
+ }
+ });
+
+ if (!audioPlayer.paused) {
+ tick();
+ }
+}
+
+export function clearFullscreenLyricsSync(container) {
+ if (container && container.lyricsCleanup) {
+ container.lyricsCleanup();
+ container.lyricsCleanup = null;
+ }
+}
\ No newline at end of file
diff --git a/js/router.js b/js/router.js
index b66b698..2d87eab 100644
--- a/js/router.js
+++ b/js/router.js
@@ -17,6 +17,9 @@ export function createRouter(ui) {
case 'playlist':
ui.renderPlaylistPage(param);
break;
+ case 'userplaylist':
+ ui.renderPlaylistPage(param);
+ break;
case 'library':
ui.renderLibraryPage();
break;
diff --git a/js/ui.js b/js/ui.js
index f032636..eb33c74 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1,5 +1,6 @@
//js/ui.js
import { SVG_PLAY, SVG_DOWNLOAD, SVG_MENU, SVG_HEART, formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js';
+import { renderLyricsInFullscreen, clearFullscreenLyricsSync } from './lyrics.js';
import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js';
import { db } from './db.js';
@@ -217,6 +218,74 @@ export class UIRenderer {
`;
}
+ createUserPlaylistCardHTML(playlist) {
+ return `
+
+
+

+
+
+
+
+
${playlist.name}
+
${playlist.tracks ? playlist.tracks.length : 0} tracks
+
+ `;
+ }
+
+ createAlbumCardHTML(album) {
+ const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
+ let yearDisplay = '';
+ if (album.releaseDate) {
+ const date = new Date(album.releaseDate);
+ if (!isNaN(date.getTime())) {
+ yearDisplay = `${date.getFullYear()}`;
+ }
+ }
+
+ let typeLabel = '';
+ if (album.type === 'EP') {
+ typeLabel = ' • EP';
+ } else if (album.type === 'SINGLE') {
+ typeLabel = ' • Single';
+ } else if (!album.type && album.numberOfTracks) {
+ if (album.numberOfTracks <= 3) typeLabel = ' • Single';
+ else if (album.numberOfTracks <= 6) typeLabel = ' • EP';
+ }
+
+ return `
+
+
+
})
+
+
+
+
${album.title} ${explicitBadge}
+
${album.artist?.name ?? ''}
+
${yearDisplay}${typeLabel}
+
+ `;
+ }
+
createArtistCardHTML(artist) {
return `
@@ -391,42 +460,78 @@ export class UIRenderer {
root.style.removeProperty('--track-hover-bg');
}
- showFullscreenCover(track, nextTrack) {
- if (!track) return;
-
- const overlay = document.getElementById('fullscreen-cover-overlay');
- const image = document.getElementById('fullscreen-cover-image');
- const title = document.getElementById('fullscreen-track-title');
- const artist = document.getElementById('fullscreen-track-artist');
- const nextTrackEl = document.getElementById('fullscreen-next-track');
+async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
+ if (!track) return;
+ const overlay = document.getElementById('fullscreen-cover-overlay');
+ const image = document.getElementById('fullscreen-cover-image');
+ const title = document.getElementById('fullscreen-track-title');
+ const artist = document.getElementById('fullscreen-track-artist');
+ const nextTrackEl = document.getElementById('fullscreen-next-track');
+ const lyricsContainer = document.getElementById('fullscreen-lyrics-container');
+ const lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn');
+
+ const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
+ image.src = coverUrl;
+ title.textContent = track.title;
+ artist.textContent = track.artist?.name || 'Unknown Artist';
+
+ if (nextTrack) {
+ nextTrackEl.style.display = 'flex';
+ nextTrackEl.querySelector('.value').textContent = `${nextTrack.title} • ${nextTrack.artist?.name || 'Unknown'}`;
- const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
-
- image.src = coverUrl;
- title.textContent = track.title;
- artist.textContent = track.artist?.name || 'Unknown Artist';
-
- if (nextTrack) {
- nextTrackEl.style.display = 'flex';
- nextTrackEl.querySelector('.value').textContent = `${nextTrack.title} • ${nextTrack.artist?.name || 'Unknown'}`;
-
- // Replay animation
- nextTrackEl.classList.remove('animate-in');
- void nextTrackEl.offsetWidth; // Trigger reflow
- nextTrackEl.classList.add('animate-in');
- } else {
- nextTrackEl.style.display = 'none';
- nextTrackEl.classList.remove('animate-in');
- }
-
- // Set the background image via CSS variable for the pseudo-element to use
- overlay.style.setProperty('--bg-image', `url('${coverUrl}')`);
-
- overlay.style.display = 'flex';
+ nextTrackEl.classList.remove('animate-in');
+ void nextTrackEl.offsetWidth;
+ nextTrackEl.classList.add('animate-in');
+ } else {
+ nextTrackEl.style.display = 'none';
+ nextTrackEl.classList.remove('animate-in');
}
+
+ overlay.style.setProperty('--bg-image', `url('${coverUrl}')`);
+
+ if (lyricsManager && audioPlayer) {
+ lyricsToggleBtn.style.display = 'flex';
+ lyricsContainer.style.display = 'none';
+ lyricsContainer.classList.remove('active');
+ lyricsToggleBtn.classList.remove('active');
+
+ const toggleLyrics = async () => {
+ const isActive = lyricsContainer.classList.contains('active');
+ if (isActive) {
+ lyricsContainer.classList.remove('active');
+ lyricsToggleBtn.classList.remove('active');
+ setTimeout(() => {
+ lyricsContainer.style.display = 'none';
+ clearFullscreenLyricsSync(lyricsContainer);
+ }, 300);
+ } else {
+ lyricsContainer.style.display = 'block';
+ setTimeout(() => lyricsContainer.classList.add('active'), 10);
+ lyricsToggleBtn.classList.add('active');
+ await renderLyricsInFullscreen(track, audioPlayer, lyricsManager, lyricsContainer);
+ }
+ };
+
+ const newToggleBtn = lyricsToggleBtn.cloneNode(true);
+ lyricsToggleBtn.parentNode.replaceChild(newToggleBtn, lyricsToggleBtn);
+ newToggleBtn.addEventListener('click', toggleLyrics);
+ } else {
+ lyricsToggleBtn.style.display = 'none';
+ }
+
+ overlay.style.display = 'flex';
+}
closeFullscreenCover() {
- document.getElementById('fullscreen-cover-overlay').style.display = 'none';
+ const overlay = document.getElementById('fullscreen-cover-overlay');
+ const lyricsContainer = document.getElementById('fullscreen-lyrics-container');
+ clearFullscreenLyricsSync(lyricsContainer);
+
+ lyricsContainer.style.display = 'none';
+ lyricsContainer.classList.remove('active');
+ lyricsContainer.innderHTML = '';
+
+ overlay.style.display = 'none';
}
showPage(pageId) {
@@ -507,6 +612,14 @@ export class UIRenderer {
} else {
playlistsContainer.innerHTML = createPlaceholder('No liked playlists yet.');
}
+
+ const myPlaylistsContainer = document.getElementById('my-playlists-container');
+ const myPlaylists = await db.getPlaylists();
+ if (myPlaylists.length) {
+ myPlaylistsContainer.innerHTML = myPlaylists.map(p => this.createUserPlaylistCardHTML(p)).join('');
+ } else {
+ myPlaylistsContainer.innerHTML = createPlaceholder('No playlists yet. Create your first playlist!');
+ }
}
async renderHomePage() {
@@ -863,50 +976,113 @@ export class UIRenderer {
`;
try {
- const { playlist, tracks } = await this.api.getPlaylist(playlistId);
+ // Check if it's a user playlist
+ const userPlaylist = await db.getPlaylist(playlistId);
+ if (userPlaylist) {
+ // Render user playlist
+ imageEl.src = userPlaylist.cover || 'assets/appicon.png';
+ imageEl.style.backgroundColor = '';
- const imageId = playlist.squareImage || playlist.image;
- if (imageId) {
- imageEl.src = this.api.getCoverUrl(imageId, '1080');
+ titleEl.textContent = userPlaylist.name;
+ this.adjustTitleFontSize(titleEl, userPlaylist.name);
+
+ const tracks = userPlaylist.tracks || [];
+ const totalDuration = calculateTotalDuration(tracks);
+
+ metaEl.textContent = `${tracks.length} tracks • ${formatDuration(totalDuration)}`;
+ descEl.textContent = '';
+
+ tracklistContainer.innerHTML = `
+
+ `;
+
+ this.renderListWithTracks(tracklistContainer, tracks, true);
+
+ // Add remove buttons to tracks
+ const trackItems = tracklistContainer.querySelectorAll('.track-item');
+ trackItems.forEach((item, index) => {
+ const actionsDiv = item.querySelector('.track-item-actions');
+ const removeBtn = document.createElement('button');
+ removeBtn.className = 'track-action-btn remove-from-playlist-btn';
+ removeBtn.title = 'Remove from playlist';
+ removeBtn.innerHTML = '
';
+ removeBtn.dataset.trackIndex = index;
+ actionsDiv.appendChild(removeBtn);
+ });
+
+ // Update header like button - hide for user playlists
+ const playlistLikeBtn = document.getElementById('like-playlist-btn');
+ if (playlistLikeBtn) {
+ playlistLikeBtn.style.display = 'none';
+ }
+
+ // Add edit and delete buttons
+ const actionsDiv = document.querySelector('.detail-header-actions');
+ const editBtn = document.createElement('button');
+ editBtn.id = 'edit-playlist-btn';
+ editBtn.className = 'btn-secondary';
+ editBtn.innerHTML = '
Edit';
+ const deleteBtn = document.createElement('button');
+ deleteBtn.id = 'delete-playlist-btn';
+ deleteBtn.className = 'btn-secondary danger';
+ deleteBtn.innerHTML = '
Delete';
+ actionsDiv.appendChild(editBtn);
+ actionsDiv.appendChild(deleteBtn);
+
+ recentActivityManager.addPlaylist({ title: userPlaylist.name, uuid: userPlaylist.id });
+ document.title = `${userPlaylist.name} - Monochrome`;
} else {
- imageEl.src = 'assets/appicon.png';
+ // Render API playlist
+ const { playlist, tracks } = await this.api.getPlaylist(playlistId);
+
+ const imageId = playlist.squareImage || playlist.image;
+ if (imageId) {
+ imageEl.src = this.api.getCoverUrl(imageId, '1080');
+ } else {
+ imageEl.src = 'assets/appicon.png';
+ }
+ imageEl.style.backgroundColor = '';
+
+ titleEl.textContent = playlist.title;
+ this.adjustTitleFontSize(titleEl, playlist.title);
+
+ const totalDuration = calculateTotalDuration(tracks);
+
+ metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`;
+ descEl.textContent = playlist.description || '';
+
+ tracklistContainer.innerHTML = `
+
+ `;
+
+ this.renderListWithTracks(tracklistContainer, tracks, true);
+
+ // Update header like button
+ const playlistLikeBtn = document.getElementById('like-playlist-btn');
+ if (playlistLikeBtn) {
+ const isLiked = await db.isFavorite('playlist', playlist.uuid);
+ playlistLikeBtn.innerHTML = this.createHeartIcon(isLiked);
+ playlistLikeBtn.classList.toggle('active', isLiked);
+ playlistLikeBtn.style.display = 'flex';
+ }
+
+ // Show/hide Delete button
+ const deleteBtn = document.getElementById('delete-playlist-btn');
+ if (deleteBtn) {
+ deleteBtn.style.display = 'none';
+ }
+
+ recentActivityManager.addPlaylist(playlist);
+ document.title = `${playlist.title || 'Artist Mix'} - Monochrome`;
}
- imageEl.style.backgroundColor = '';
-
- titleEl.textContent = playlist.title;
- this.adjustTitleFontSize(titleEl, playlist.title);
-
- const totalDuration = calculateTotalDuration(tracks);
-
- metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`;
- descEl.textContent = playlist.description || '';
-
- tracklistContainer.innerHTML = `
-
- `;
-
- this.renderListWithTracks(tracklistContainer, tracks, true);
-
- // Update header like button
- const playlistLikeBtn = document.getElementById('like-playlist-btn');
- if (playlistLikeBtn) {
- const isLiked = await db.isFavorite('playlist', playlist.uuid);
- playlistLikeBtn.innerHTML = this.createHeartIcon(isLiked);
- playlistLikeBtn.classList.toggle('active', isLiked);
- }
-
- // Show/hide Delete button
- const deleteBtn = document.getElementById('delete-playlist-btn');
- if (deleteBtn) {
- deleteBtn.style.display = 'none';
- }
-
- recentActivityManager.addPlaylist(playlist);
- document.title = `${playlist.title || 'Artist Mix'} - Monochrome`;
} catch (error) {
console.error("Failed to load playlist:", error);
tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`);
diff --git a/styles.css b/styles.css
index 2453230..4c928cf 100644
--- a/styles.css
+++ b/styles.css
@@ -3298,4 +3298,143 @@ img:not([src]), img[src=''] {
align-items: center;
width: 80%;
max-width: 100%;
+}
+
+
+.fullscreen-cover-content {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+.fullscreen-main-view {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem;
+ transition: flex 0.3s ease;
+}
+
+.fullscreen-lyrics-container {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 0;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.85);
+ backdrop-filter: blur(20px);
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
+ overflow-y: auto;
+ overflow-x: hidden;
+ transition: width 0.3s ease;
+ padding: 0;
+}
+
+.fullscreen-lyrics-container.active {
+ width: 400px;
+ padding: 2rem;
+}
+
+
+.fullscreen-lyrics-container > * {
+ width: 100%;
+ height: 100%;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 2rem;
+ box-sizing: border-box;
+}
+
+.fullscreen-lyrics-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.fullscreen-lyrics-container::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.fullscreen-lyrics-container::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 4px;
+}
+
+.fullscreen-lyrics-container::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+
+.fullscreen-lyrics-toggle {
+ position: absolute;
+ top: 1rem;
+ right: 4rem;
+ background: rgba(0, 0, 0, 0.5);
+ border: none;
+ color: white;
+ padding: 0.75rem;
+ border-radius: 50%;
+ cursor: pointer;
+ z-index: 1001;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.fullscreen-lyrics-toggle:hover {
+ background: rgba(0, 0, 0, 0.7);
+ transform: scale(1.1);
+}
+
+.fullscreen-lyrics-toggle.active {
+ background: var(--primary);
+}
+
+@media (max-width: 768px) {
+ .fullscreen-cover-content {
+ flex-direction: column;
+ }
+
+ .fullscreen-lyrics-container {
+ top: auto;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 0;
+ border-left: none;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ transition: height 0.3s ease;
+ }
+
+ .fullscreen-lyrics-container.active {
+ width: 50vh;
+ width: 100%;
+ }
+
+ .fullscreen-lyrics-toggle {
+ right: 3.5rem;
+ }
+}
+
+
+
+
+#playlist-modal {
+ opacity: 1;
+ animation-name: fadeInOpacity;
+ animation-iteration-count: 1;
+ animation-timing-function: ease-in;
+ animation-duration: 0.1s;
+}
+
+@keyframes fadeInOpacity {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
}
\ No newline at end of file