Playlist Feature, Lyrics on Fullscreen & More
This commit is contained in:
parent
c33ef02dca
commit
a27be3162d
9 changed files with 814 additions and 87 deletions
39
index.html
39
index.html
|
|
@ -23,6 +23,7 @@
|
|||
<div id="context-menu">
|
||||
<ul>
|
||||
<li data-action="toggle-like">Like</li>
|
||||
<li data-action="add-to-playlist">Add to Playlist</li>
|
||||
<li data-action="play-next">Play Next</li>
|
||||
<li data-action="add-to-queue">Add to Queue</li>
|
||||
<li data-action="download">Download</li>
|
||||
|
|
@ -41,9 +42,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fullscreen-cover-overlay" style="display: none;">
|
||||
<div class="fullscreen-cover-content">
|
||||
<button id="close-fullscreen-cover-btn" title="Close">×</button>
|
||||
<div id="fullscreen-cover-overlay" style="display: none;">
|
||||
<div class="fullscreen-cover-content">
|
||||
<button id="close-fullscreen-cover-btn" title="Close">×</button>
|
||||
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="22"/>
|
||||
<line x1="8" y1="22" x2="16" y2="22"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="fullscreen-main-view">
|
||||
<img id="fullscreen-cover-image" src="" alt="Album Cover">
|
||||
<div class="fullscreen-track-info">
|
||||
<h2 id="fullscreen-track-title"></h2>
|
||||
|
|
@ -53,13 +63,26 @@
|
|||
<span class="value"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fullscreen-controls">
|
||||
<!-- Controls will be cloned or managed here if needed, or we just rely on main controls -->
|
||||
</div>
|
||||
<div class="fullscreen-lyrics-container" id="fullscreen-lyrics-container" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="playlist-modal" class="modal" style="display: none;">
|
||||
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
|
||||
<div class="modal-content" style="background: var(--card); padding: 2rem; border-radius: var(--radius); max-width: 400px; width: 90%;">
|
||||
<h3 id="playlist-modal-title">Create Playlist</h3>
|
||||
<input type="text" id="playlist-name-input" placeholder="Playlist name" style="width: 100%; margin: 1rem 0; padding: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background); color: var(--foreground);">
|
||||
<div class="modal-actions" style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button id="playlist-modal-cancel" class="btn-secondary">Cancel</button>
|
||||
<button id="playlist-modal-save" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="sidebar-overlay"></div>
|
||||
|
||||
<div class="app-container">
|
||||
|
|
@ -177,6 +200,10 @@
|
|||
|
||||
<div id="page-library" class="page">
|
||||
<h2 class="section-title">Your Library</h2>
|
||||
<section class="content-section">
|
||||
<h2 class="section-title">My Playlists <button id="create-playlist-btn" class="btn-secondary">Create Playlist</button></h2>
|
||||
<div class="card-grid" id="my-playlists-container"></div>
|
||||
</section>
|
||||
<div class="search-tabs">
|
||||
<button class="search-tab active" data-tab="tracks">Liked Tracks</button>
|
||||
<button class="search-tab" data-tab="albums">Albums</button>
|
||||
|
|
|
|||
122
js/app.js
122
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');
|
||||
|
|
|
|||
77
js/db.js
77
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
46
js/events.js
46
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 = `
|
||||
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="modal-content" style="background: var(--card); padding: 2rem; border-radius: var(--radius); max-width: 400px; width: 90%;">
|
||||
<h3>Add to Playlist</h3>
|
||||
<div id="playlist-list" style="margin: 1rem 0; max-height: 200px; overflow-y: auto;">
|
||||
${playlists.map(p => `<div class="playlist-option" data-id="${p.id}" style="padding: 0.5rem; cursor: pointer; border-bottom: 1px solid var(--border);">${p.name}</div>`).join('')}
|
||||
</div>
|
||||
<div class="modal-actions" style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button id="cancel-add-playlist" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
115
js/lyrics.js
115
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 = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||
|
||||
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 = '<div class="lyrics-error">Failed to load lyrics</div>';
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
322
js/ui.js
322
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 `
|
||||
<div class="card user-playlist" data-playlist-id="${playlist.id}" data-href="#userplaylist/${playlist.id}" style="cursor: pointer;">
|
||||
<div class="card-image-wrapper">
|
||||
<img src="${playlist.cover || 'assets/appicon.png'}" alt="${playlist.name}" class="card-image" loading="lazy">
|
||||
<button class="edit-playlist-btn" data-action="edit-playlist" title="Edit Playlist">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="delete-playlist-btn" data-action="delete-playlist" title="Delete Playlist">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="play-btn card-play-btn" data-action="play-card" data-type="user-playlist" data-id="${playlist.id}" title="Play">
|
||||
${SVG_PLAY}
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="card-title">${playlist.name}</h3>
|
||||
<p class="card-subtitle">${playlist.tracks ? playlist.tracks.length : 0} tracks</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="card" data-album-id="${album.id}" data-href="#album/${album.id}" style="cursor: pointer;">
|
||||
<div class="card-image-wrapper">
|
||||
<img src="${this.api.getCoverUrl(album.cover, '320')}" alt="${album.title}" class="card-image" loading="lazy">
|
||||
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="album" title="Add to Library">
|
||||
${this.createHeartIcon(false)}
|
||||
</button>
|
||||
<button class="play-btn card-play-btn" data-action="play-card" data-type="album" data-id="${album.id}" title="Play">
|
||||
${SVG_PLAY}
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="card-title">${album.title} ${explicitBadge}</h3>
|
||||
<p class="card-subtitle">${album.artist?.name ?? ''}</p>
|
||||
<p class="card-subtitle">${yearDisplay}${typeLabel}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createArtistCardHTML(artist) {
|
||||
return `
|
||||
<div class="card artist" data-artist-id="${artist.id}" data-href="#artist/${artist.id}" style="cursor: pointer;">
|
||||
|
|
@ -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 = `
|
||||
<div class="track-list-header">
|
||||
<span style="width: 40px; text-align: center;">#</span>
|
||||
<span>Title</span>
|
||||
<span class="duration-header">Duration</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
|
||||
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 = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg><span>Edit</span>';
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.id = 'delete-playlist-btn';
|
||||
deleteBtn.className = 'btn-secondary danger';
|
||||
deleteBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg><span>Delete</span>';
|
||||
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 = `
|
||||
<div class="track-list-header">
|
||||
<span style="width: 40px; text-align: center;">#</span>
|
||||
<span>Title</span>
|
||||
<span class="duration-header">Duration</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="track-list-header">
|
||||
<span style="width: 40px; text-align: center;">#</span>
|
||||
<span>Title</span>
|
||||
<span class="duration-header">Duration</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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}`);
|
||||
|
|
|
|||
139
styles.css
139
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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue