Merge branch 'main' into pwa
This commit is contained in:
commit
60126ab333
8 changed files with 274 additions and 38 deletions
|
|
@ -110,6 +110,10 @@
|
||||||
<h2 class="section-title">Recent Albums</h2>
|
<h2 class="section-title">Recent Albums</h2>
|
||||||
<div class="card-grid" id="home-recent-albums"></div>
|
<div class="card-grid" id="home-recent-albums"></div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Recent Playlists</h2>
|
||||||
|
<div class="card-grid" id="home-recent-playlists"></div>
|
||||||
|
</section>
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<h2 class="section-title">Recent Artists</h2>
|
<h2 class="section-title">Recent Artists</h2>
|
||||||
<div class="card-grid" id="home-recent-artists"></div>
|
<div class="card-grid" id="home-recent-artists"></div>
|
||||||
|
|
@ -122,6 +126,7 @@
|
||||||
<button class="search-tab active" data-tab="tracks">Tracks</button>
|
<button class="search-tab active" data-tab="tracks">Tracks</button>
|
||||||
<button class="search-tab" data-tab="albums">Albums</button>
|
<button class="search-tab" data-tab="albums">Albums</button>
|
||||||
<button class="search-tab" data-tab="artists">Artists</button>
|
<button class="search-tab" data-tab="artists">Artists</button>
|
||||||
|
<button class="search-tab" data-tab="playlists">Playlists</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-tab-content active" id="search-tab-tracks">
|
<div class="search-tab-content active" id="search-tab-tracks">
|
||||||
<div class="track-list" id="search-tracks-container"></div>
|
<div class="track-list" id="search-tracks-container"></div>
|
||||||
|
|
@ -132,6 +137,9 @@
|
||||||
<div class="search-tab-content" id="search-tab-artists">
|
<div class="search-tab-content" id="search-tab-artists">
|
||||||
<div class="card-grid" id="search-artists-container"></div>
|
<div class="card-grid" id="search-artists-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="search-tab-content" id="search-tab-playlists">
|
||||||
|
<div class="card-grid" id="search-playlists-container"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-album" class="page">
|
<div id="page-album" class="page">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
[
|
[
|
||||||
"https://aether.squid.wtf",
|
"https://triton.squid.wtf",
|
||||||
"https://zeus.squid.wtf",
|
|
||||||
"https://kraken.squid.wtf",
|
|
||||||
"https://wolf.qqdl.site",
|
"https://wolf.qqdl.site",
|
||||||
"https://maus.qqdl.site",
|
"https://maus.qqdl.site",
|
||||||
"https://vogel.qqdl.site",
|
"https://vogel.qqdl.site",
|
||||||
"https://katze.qqdl.site",
|
"https://katze.qqdl.site",
|
||||||
"https://hund.qqdl.site",
|
"https://hund.qqdl.site",
|
||||||
"https://phoenix.squid.wtf",
|
"https://tidal.kinoplus.online",
|
||||||
"https://shiva.squid.wtf",
|
"https://tidal-api.binimum.org"
|
||||||
"https://chaos.squid.wtf",
|
|
||||||
"https://tidal.kinoplus.online"
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
71
js/api.js
71
js/api.js
|
|
@ -157,6 +157,10 @@ export class LosslessAPI {
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preparePlaylist(playlist) {
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
prepareArtist(artist) {
|
prepareArtist(artist) {
|
||||||
if (!artist.type && Array.isArray(artist.artistTypes) && artist.artistTypes.length > 0) {
|
if (!artist.type && Array.isArray(artist.artistTypes) && artist.artistTypes.length > 0) {
|
||||||
return { ...artist, type: artist.artistTypes[0] };
|
return { ...artist, type: artist.artistTypes[0] };
|
||||||
|
|
@ -278,6 +282,27 @@ export class LosslessAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchPlaylists(query) {
|
||||||
|
const cached = await this.cache.get('search_playlists', query);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
const normalized = this.normalizeSearchResponse(data, 'playlists');
|
||||||
|
const result = {
|
||||||
|
...normalized,
|
||||||
|
items: normalized.items.map(p => this.preparePlaylist(p))
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.cache.set('search_playlists', query, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Playlist search failed:', error);
|
||||||
|
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAlbum(id) {
|
async getAlbum(id) {
|
||||||
const cached = await this.cache.get('album', id);
|
const cached = await this.cache.get('album', id);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
@ -354,26 +379,40 @@ export class LosslessAPI {
|
||||||
|
|
||||||
// Unwrap the data property if it exists
|
// Unwrap the data property if it exists
|
||||||
const data = jsonData.data || jsonData;
|
const data = jsonData.data || jsonData;
|
||||||
const entries = Array.isArray(data) ? data : [data];
|
|
||||||
|
let playlist = null;
|
||||||
|
let tracksSection = null;
|
||||||
|
|
||||||
let playlist, tracksSection;
|
// Check for direct playlist property (common in v2 responses)
|
||||||
|
if (data.playlist) {
|
||||||
for (const entry of entries) {
|
playlist = data.playlist;
|
||||||
if (!entry || typeof entry !== 'object') continue;
|
|
||||||
|
|
||||||
if (!playlist && ('uuid' in entry || 'numberOfTracks' in entry || 'title' in entry && 'id' in entry)) {
|
|
||||||
playlist = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tracksSection && 'items' in entry) {
|
|
||||||
tracksSection = entry;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still no playlist found, try using the first valid entry
|
// Check for direct items property
|
||||||
if (!playlist && entries.length > 0) {
|
if (data.items) {
|
||||||
|
tracksSection = { items: data.items };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: iterate if we still missed something or if structure is flat array
|
||||||
|
if (!playlist || !tracksSection) {
|
||||||
|
const entries = Array.isArray(data) ? data : [data];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry && typeof entry === 'object' && ('id' in entry || 'uuid' in entry)) {
|
if (!entry || typeof entry !== 'object') continue;
|
||||||
|
|
||||||
|
if (!playlist && ('uuid' in entry || 'numberOfTracks' in entry || ('title' in entry && 'id' in entry))) {
|
||||||
|
playlist = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tracksSection && 'items' in entry) {
|
||||||
|
tracksSection = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 2: If we have a list of entries but no explicit playlist object, try to find one that looks like a playlist
|
||||||
|
if (!playlist && Array.isArray(data)) {
|
||||||
|
for (const entry of data) {
|
||||||
|
if (entry && typeof entry === 'object' && ('uuid' in entry || 'numberOfTracks' in entry)) {
|
||||||
playlist = entry;
|
playlist = entry;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
js/events.js
15
js/events.js
|
|
@ -11,6 +11,21 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) {
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
const repeatBtn = document.getElementById('repeat-btn');
|
const repeatBtn = document.getElementById('repeat-btn');
|
||||||
|
|
||||||
|
// Sync UI with player state on load
|
||||||
|
if (player.shuffleActive) {
|
||||||
|
shuffleBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.repeatMode !== REPEAT_MODE.OFF) {
|
||||||
|
repeatBtn.classList.add('active');
|
||||||
|
if (player.repeatMode === REPEAT_MODE.ONE) {
|
||||||
|
repeatBtn.classList.add('repeat-one');
|
||||||
|
}
|
||||||
|
repeatBtn.title = player.repeatMode === REPEAT_MODE.ALL ? 'Repeat Queue' : 'Repeat One';
|
||||||
|
} else {
|
||||||
|
repeatBtn.title = 'Repeat';
|
||||||
|
}
|
||||||
|
|
||||||
audioPlayer.addEventListener('play', () => {
|
audioPlayer.addEventListener('play', () => {
|
||||||
if (scrobbler.isAuthenticated() && lastFMStorage.isEnabled() && player.currentTrack) {
|
if (scrobbler.isAuthenticated() && lastFMStorage.isEnabled() && player.currentTrack) {
|
||||||
scrobbler.updateNowPlaying(player.currentTrack);
|
scrobbler.updateNowPlaying(player.currentTrack);
|
||||||
|
|
|
||||||
75
js/player.js
75
js/player.js
|
|
@ -1,5 +1,6 @@
|
||||||
//js/player.js
|
//js/player.js
|
||||||
import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle} from './utils.js';
|
import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle} from './utils.js';
|
||||||
|
import { queueManager } from './storage.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(audioElement, api, quality = 'LOSSLESS') {
|
constructor(audioElement, api, quality = 'LOSSLESS') {
|
||||||
|
|
@ -16,7 +17,58 @@ export class Player {
|
||||||
this.preloadAbortController = null;
|
this.preloadAbortController = null;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
|
|
||||||
|
this.loadQueueState();
|
||||||
this.setupMediaSession();
|
this.setupMediaSession();
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.saveQueueState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadQueueState() {
|
||||||
|
const savedState = queueManager.getQueue();
|
||||||
|
if (savedState) {
|
||||||
|
this.queue = savedState.queue || [];
|
||||||
|
this.shuffledQueue = savedState.shuffledQueue || [];
|
||||||
|
this.originalQueueBeforeShuffle = savedState.originalQueueBeforeShuffle || [];
|
||||||
|
this.currentQueueIndex = savedState.currentQueueIndex ?? -1;
|
||||||
|
this.shuffleActive = savedState.shuffleActive || false;
|
||||||
|
this.repeatMode = savedState.repeatMode || REPEAT_MODE.OFF;
|
||||||
|
|
||||||
|
// Restore current track if queue exists and index is valid
|
||||||
|
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||||
|
if (this.currentQueueIndex >= 0 && this.currentQueueIndex < currentQueue.length) {
|
||||||
|
this.currentTrack = currentQueue[this.currentQueueIndex];
|
||||||
|
|
||||||
|
// Restore UI
|
||||||
|
const track = this.currentTrack;
|
||||||
|
const trackTitle = getTrackTitle(track);
|
||||||
|
const trackArtists = getTrackArtists(track);
|
||||||
|
|
||||||
|
const coverEl = document.querySelector('.now-playing-bar .cover');
|
||||||
|
const titleEl = document.querySelector('.now-playing-bar .title');
|
||||||
|
const artistEl = document.querySelector('.now-playing-bar .artist');
|
||||||
|
|
||||||
|
if (coverEl) coverEl.src = this.api.getCoverUrl(track.album?.cover, '1280');
|
||||||
|
if (titleEl) titleEl.textContent = trackTitle;
|
||||||
|
if (artistEl) artistEl.textContent = trackArtists;
|
||||||
|
document.title = `${trackTitle} • ${track.artist?.name || 'Unknown'}`;
|
||||||
|
|
||||||
|
this.updatePlayingTrackIndicator();
|
||||||
|
this.updateMediaSession(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveQueueState() {
|
||||||
|
queueManager.saveQueue({
|
||||||
|
queue: this.queue,
|
||||||
|
shuffledQueue: this.shuffledQueue,
|
||||||
|
originalQueueBeforeShuffle: this.originalQueueBeforeShuffle,
|
||||||
|
currentQueueIndex: this.currentQueueIndex,
|
||||||
|
shuffleActive: this.shuffleActive,
|
||||||
|
repeatMode: this.repeatMode
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupMediaSession() {
|
setupMediaSession() {
|
||||||
|
|
@ -105,6 +157,8 @@ export class Player {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.saveQueueState();
|
||||||
|
|
||||||
const track = currentQueue[this.currentQueueIndex];
|
const track = currentQueue[this.currentQueueIndex];
|
||||||
this.currentTrack = track;
|
this.currentTrack = track;
|
||||||
|
|
||||||
|
|
@ -187,12 +241,23 @@ export class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayPause() {
|
handlePlayPause() {
|
||||||
if (!this.audio.src) return;
|
if (!this.audio.src || this.audio.error) {
|
||||||
|
if (this.currentTrack) {
|
||||||
|
this.playTrackFromQueue();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.audio.paused) {
|
if (this.audio.paused) {
|
||||||
this.audio.play().catch(console.error);
|
this.audio.play().catch(e => {
|
||||||
|
console.error("Play failed, reloading track:", e);
|
||||||
|
if (this.currentTrack) {
|
||||||
|
this.playTrackFromQueue();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,10 +295,12 @@ export class Player {
|
||||||
|
|
||||||
this.preloadCache.clear();
|
this.preloadCache.clear();
|
||||||
this.preloadNextTracks();
|
this.preloadNextTracks();
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleRepeat() {
|
toggleRepeat() {
|
||||||
this.repeatMode = (this.repeatMode + 1) % 3;
|
this.repeatMode = (this.repeatMode + 1) % 3;
|
||||||
|
this.saveQueueState();
|
||||||
return this.repeatMode;
|
return this.repeatMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,6 +309,7 @@ export class Player {
|
||||||
this.currentQueueIndex = startIndex;
|
this.currentQueueIndex = startIndex;
|
||||||
this.shuffleActive = false;
|
this.shuffleActive = false;
|
||||||
this.preloadCache.clear();
|
this.preloadCache.clear();
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
addToQueue(track) {
|
addToQueue(track) {
|
||||||
|
|
@ -251,6 +319,7 @@ export class Player {
|
||||||
this.currentQueueIndex = this.queue.length - 1;
|
this.currentQueueIndex = this.queue.length - 1;
|
||||||
this.playTrackFromQueue();
|
this.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromQueue(index) {
|
removeFromQueue(index) {
|
||||||
|
|
@ -271,6 +340,7 @@ export class Player {
|
||||||
this.playTrackFromQueue();
|
this.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
moveInQueue(fromIndex, toIndex) {
|
moveInQueue(fromIndex, toIndex) {
|
||||||
|
|
@ -289,6 +359,7 @@ export class Player {
|
||||||
} else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) {
|
} else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) {
|
||||||
this.currentQueueIndex++;
|
this.currentQueueIndex++;
|
||||||
}
|
}
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentQueue() {
|
getCurrentQueue() {
|
||||||
|
|
|
||||||
|
|
@ -193,9 +193,11 @@ export const recentActivityManager = {
|
||||||
_get() {
|
_get() {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||||
return data ? JSON.parse(data) : { artists: [], albums: [] };
|
const parsed = data ? JSON.parse(data) : { artists: [], albums: [], playlists: [] };
|
||||||
|
if (!parsed.playlists) parsed.playlists = [];
|
||||||
|
return parsed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { artists: [], albums: [] };
|
return { artists: [], albums: [], playlists: [] };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -221,6 +223,10 @@ export const recentActivityManager = {
|
||||||
|
|
||||||
addAlbum(album) {
|
addAlbum(album) {
|
||||||
this._add('albums', album);
|
this._add('albums', album);
|
||||||
|
},
|
||||||
|
|
||||||
|
addPlaylist(playlist) {
|
||||||
|
this._add('playlists', playlist);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -326,6 +332,36 @@ export const lyricsSettings = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const queueManager = {
|
||||||
|
STORAGE_KEY: 'monochrome-queue',
|
||||||
|
|
||||||
|
getQueue() {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||||
|
return data ? JSON.parse(data) : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveQueue(queueState) {
|
||||||
|
try {
|
||||||
|
// Only save essential data to avoid quota limits
|
||||||
|
const minimalState = {
|
||||||
|
queue: queueState.queue,
|
||||||
|
shuffledQueue: queueState.shuffledQueue,
|
||||||
|
originalQueueBeforeShuffle: queueState.originalQueueBeforeShuffle,
|
||||||
|
currentQueueIndex: queueState.currentQueueIndex,
|
||||||
|
shuffleActive: queueState.shuffleActive,
|
||||||
|
repeatMode: queueState.repeatMode
|
||||||
|
};
|
||||||
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(minimalState));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save queue to localStorage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// System theme listener
|
// System theme listener
|
||||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
|
|
||||||
82
js/ui.js
82
js/ui.js
|
|
@ -103,6 +103,19 @@ export class UIRenderer {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createPlaylistCardHTML(playlist) {
|
||||||
|
const imageId = playlist.squareImage || playlist.image || playlist.uuid; // Fallback or use a specific cover getter if needed
|
||||||
|
return `
|
||||||
|
<a href="#playlist/${playlist.uuid}" class="card">
|
||||||
|
<div class="card-image-wrapper">
|
||||||
|
<img src="${this.api.getCoverUrl(imageId, '320')}" alt="${playlist.title}" class="card-image" loading="lazy">
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title">${playlist.title}</h3>
|
||||||
|
<p class="card-subtitle">${playlist.numberOfTracks || 0} tracks</p>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
createArtistCardHTML(artist) {
|
createArtistCardHTML(artist) {
|
||||||
return `
|
return `
|
||||||
<a href="#artist/${artist.id}" class="card artist">
|
<a href="#artist/${artist.id}" class="card artist">
|
||||||
|
|
@ -193,6 +206,7 @@ export class UIRenderer {
|
||||||
|
|
||||||
const albumsContainer = document.getElementById('home-recent-albums');
|
const albumsContainer = document.getElementById('home-recent-albums');
|
||||||
const artistsContainer = document.getElementById('home-recent-artists');
|
const artistsContainer = document.getElementById('home-recent-artists');
|
||||||
|
const playlistsContainer = document.getElementById('home-recent-playlists');
|
||||||
|
|
||||||
albumsContainer.innerHTML = recents.albums.length
|
albumsContainer.innerHTML = recents.albums.length
|
||||||
? recents.albums.map(album => this.createAlbumCardHTML(album)).join('')
|
? recents.albums.map(album => this.createAlbumCardHTML(album)).join('')
|
||||||
|
|
@ -201,6 +215,12 @@ export class UIRenderer {
|
||||||
artistsContainer.innerHTML = recents.artists.length
|
artistsContainer.innerHTML = recents.artists.length
|
||||||
? recents.artists.map(artist => this.createArtistCardHTML(artist)).join('')
|
? recents.artists.map(artist => this.createArtistCardHTML(artist)).join('')
|
||||||
: createPlaceholder("You haven't viewed any artists yet. Search for music to get started!");
|
: createPlaceholder("You haven't viewed any artists yet. Search for music to get started!");
|
||||||
|
|
||||||
|
if (playlistsContainer) {
|
||||||
|
playlistsContainer.innerHTML = recents.playlists && recents.playlists.length
|
||||||
|
? recents.playlists.map(playlist => this.createPlaylistCardHTML(playlist)).join('')
|
||||||
|
: createPlaceholder("You haven't viewed any playlists yet. Search for music to get started!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderSearchPage(query) {
|
async renderSearchPage(query) {
|
||||||
|
|
@ -210,21 +230,25 @@ export class UIRenderer {
|
||||||
const tracksContainer = document.getElementById('search-tracks-container');
|
const tracksContainer = document.getElementById('search-tracks-container');
|
||||||
const artistsContainer = document.getElementById('search-artists-container');
|
const artistsContainer = document.getElementById('search-artists-container');
|
||||||
const albumsContainer = document.getElementById('search-albums-container');
|
const albumsContainer = document.getElementById('search-albums-container');
|
||||||
|
const playlistsContainer = document.getElementById('search-playlists-container');
|
||||||
|
|
||||||
tracksContainer.innerHTML = this.createSkeletonTracks(8, true);
|
tracksContainer.innerHTML = this.createSkeletonTracks(8, true);
|
||||||
artistsContainer.innerHTML = this.createSkeletonCards(6, true);
|
artistsContainer.innerHTML = this.createSkeletonCards(6, true);
|
||||||
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||||
|
playlistsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [tracksResult, artistsResult, albumsResult] = await Promise.all([
|
const [tracksResult, artistsResult, albumsResult, playlistsResult] = await Promise.all([
|
||||||
this.api.searchTracks(query),
|
this.api.searchTracks(query),
|
||||||
this.api.searchArtists(query),
|
this.api.searchArtists(query),
|
||||||
this.api.searchAlbums(query)
|
this.api.searchAlbums(query),
|
||||||
|
this.api.searchPlaylists(query)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let finalTracks = tracksResult.items;
|
let finalTracks = tracksResult.items;
|
||||||
let finalArtists = artistsResult.items;
|
let finalArtists = artistsResult.items;
|
||||||
let finalAlbums = albumsResult.items;
|
let finalAlbums = albumsResult.items;
|
||||||
|
let finalPlaylists = playlistsResult.items;
|
||||||
|
|
||||||
if (finalArtists.length === 0 && finalTracks.length > 0) {
|
if (finalArtists.length === 0 && finalTracks.length > 0) {
|
||||||
const artistMap = new Map();
|
const artistMap = new Map();
|
||||||
|
|
@ -267,12 +291,17 @@ export class UIRenderer {
|
||||||
? finalAlbums.map(album => this.createAlbumCardHTML(album)).join('')
|
? finalAlbums.map(album => this.createAlbumCardHTML(album)).join('')
|
||||||
: createPlaceholder('No albums found.');
|
: createPlaceholder('No albums found.');
|
||||||
|
|
||||||
|
playlistsContainer.innerHTML = finalPlaylists.length
|
||||||
|
? finalPlaylists.map(playlist => this.createPlaylistCardHTML(playlist)).join('')
|
||||||
|
: createPlaceholder('No playlists found.');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Search failed:", error);
|
console.error("Search failed:", error);
|
||||||
const errorMsg = createPlaceholder(`Error during search. ${error.message}`);
|
const errorMsg = createPlaceholder(`Error during search. ${error.message}`);
|
||||||
tracksContainer.innerHTML = errorMsg;
|
tracksContainer.innerHTML = errorMsg;
|
||||||
artistsContainer.innerHTML = errorMsg;
|
artistsContainer.innerHTML = errorMsg;
|
||||||
albumsContainer.innerHTML = errorMsg;
|
albumsContainer.innerHTML = errorMsg;
|
||||||
|
playlistsContainer.innerHTML = errorMsg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,6 +380,46 @@ export class UIRenderer {
|
||||||
recentActivityManager.addAlbum(album);
|
recentActivityManager.addAlbum(album);
|
||||||
|
|
||||||
document.title = `${album.title} - ${album.artist.name} - Monochrome`;
|
document.title = `${album.title} - ${album.artist.name} - Monochrome`;
|
||||||
|
|
||||||
|
// "More from Artist" Section
|
||||||
|
try {
|
||||||
|
// Remove any existing "More from" section if re-rendering
|
||||||
|
const existingMoreSection = document.getElementById('album-more-from-artist');
|
||||||
|
if (existingMoreSection) existingMoreSection.remove();
|
||||||
|
|
||||||
|
const moreSection = document.createElement('section');
|
||||||
|
moreSection.id = 'album-more-from-artist';
|
||||||
|
moreSection.className = 'content-section';
|
||||||
|
moreSection.style.marginTop = '3rem';
|
||||||
|
moreSection.innerHTML = `
|
||||||
|
<h2 class="section-title">More from ${album.artist.name}</h2>
|
||||||
|
<div class="card-grid" id="album-more-albums">
|
||||||
|
${this.createSkeletonCards(6, false)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('page-album').appendChild(moreSection);
|
||||||
|
|
||||||
|
const artistData = await this.api.getArtist(album.artist.id);
|
||||||
|
// Filter out current album and duplicates
|
||||||
|
const otherAlbums = artistData.albums
|
||||||
|
.filter(a => a.id != album.id)
|
||||||
|
.filter((a, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.title === a.title) // Dedup by title
|
||||||
|
)
|
||||||
|
.slice(0, 12); // Limit to 12
|
||||||
|
|
||||||
|
const moreContainer = document.getElementById('album-more-albums');
|
||||||
|
|
||||||
|
if (otherAlbums.length > 0) {
|
||||||
|
moreContainer.innerHTML = otherAlbums.map(a => this.createAlbumCardHTML(a)).join('');
|
||||||
|
} else {
|
||||||
|
moreSection.remove(); // Remove section if no other albums
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load "More from artist":', err);
|
||||||
|
document.getElementById('album-more-from-artist')?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load album:", error);
|
console.error("Failed to load album:", error);
|
||||||
tracklistContainer.innerHTML = createPlaceholder(`Could not load album details. ${error.message}`);
|
tracklistContainer.innerHTML = createPlaceholder(`Could not load album details. ${error.message}`);
|
||||||
|
|
@ -405,10 +474,11 @@ async renderPlaylistPage(playlistId) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.renderListWithTracks(tracklistContainer, tracks, true);
|
this.renderListWithTracks(tracklistContainer, tracks, true);
|
||||||
|
|
||||||
document.title = `${playlist.title} - Monochrome`;
|
recentActivityManager.addPlaylist(playlist);
|
||||||
} catch (error) {
|
|
||||||
|
document.title = `${playlist.title || 'Artist Mix'} - Monochrome`; } catch (error) {
|
||||||
console.error("Failed to load playlist:", error);
|
console.error("Failed to load playlist:", error);
|
||||||
tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`);
|
tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
styles.css
11
styles.css
|
|
@ -666,8 +666,8 @@ kbd {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
margin-bottom: var(--spacing-2xl);
|
margin-bottom: var(--spacing-lg);
|
||||||
padding-bottom: var(--spacing-xl);
|
padding-bottom: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header-image {
|
.detail-header-image {
|
||||||
|
|
@ -2622,15 +2622,16 @@ input:checked + .slider::before {
|
||||||
.now-playing-bar .cover:hover::after {
|
.now-playing-bar .cover:hover::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-playlist .detail-header {
|
#page-playlist .detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
margin-bottom: var(--spacing-2xl);
|
margin-bottom: var(--spacing-lg);
|
||||||
padding-bottom: var(--spacing-xl);
|
padding-bottom: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-image {
|
#page-playlist .detail-cover {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue