diff --git a/index.html b/index.html index d121e54..bbf5e08 100644 --- a/index.html +++ b/index.html @@ -177,7 +177,7 @@
-

Recent Playlists

+

Recent Playlists and Mixes

@@ -216,7 +216,7 @@ - +
@@ -324,6 +324,9 @@ +
diff --git a/js/db.js b/js/db.js index 7e344b9..0fc0d8f 100644 --- a/js/db.js +++ b/js/db.js @@ -1,7 +1,7 @@ export class MusicDatabase { constructor() { this.dbName = 'MonochromeDB'; - this.version = 4; + this.version = 5; this.db = null; } @@ -41,6 +41,10 @@ export class MusicDatabase { const store = db.createObjectStore('favorites_playlists', { keyPath: 'uuid' }); store.createIndex('addedAt', 'addedAt', { unique: false }); } + if (!db.objectStoreNames.contains('favorites_mixes')) { + const store = db.createObjectStore('favorites_mixes', { keyPath: 'id' }); + store.createIndex('addedAt', 'addedAt', { unique: false }); + } if (!db.objectStoreNames.contains('history_tracks')) { const store = db.createObjectStore('history_tracks', { keyPath: 'timestamp' }); store.createIndex('timestamp', 'timestamp', { unique: true }); @@ -107,7 +111,8 @@ export class MusicDatabase { // Favorites API async toggleFavorite(type, item) { - const storeName = `favorites_${type}s`; // tracks, albums, artists + const plural = type === 'mix' ? 'mixes' : `${type}s`; + const storeName = `favorites_${plural}`; const key = type === 'playlist' ? item.uuid : item.id; const exists = await this.isFavorite(type, key); @@ -123,7 +128,8 @@ export class MusicDatabase { } async isFavorite(type, id) { - const storeName = `favorites_${type}s`; + const plural = type === 'mix' ? 'mixes' : `${type}s`; + const storeName = `favorites_${plural}`; try { const result = await this.performTransaction(storeName, 'readonly', (store) => store.get(id)); return !!result; @@ -133,7 +139,8 @@ export class MusicDatabase { } async getFavorites(type) { - const storeName = `favorites_${type}s`; + const plural = type === 'mix' ? 'mixes' : `${type}s`; + const storeName = `favorites_${plural}`; const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); @@ -214,6 +221,18 @@ export class MusicDatabase { }; } + if (type === 'mix') { + return { + id: item.id, + addedAt: item.addedAt, + title: item.title, + subTitle: item.subTitle, + description: item.description, + mixType: item.mixType, + cover: item.cover + }; + } + return item; } @@ -222,6 +241,7 @@ export class MusicDatabase { const albums = await this.getFavorites('album'); const artists = await this.getFavorites('artist'); const playlists = await this.getFavorites('playlist'); + const mixes = await this.getFavorites('mix'); const history = await this.getHistory(); const userPlaylists = await this.getPlaylists(); @@ -230,6 +250,7 @@ export class MusicDatabase { 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)), + favorites_mixes: mixes.map(m => this._minifyItem('mix', m)), history_tracks: history.map(t => this._minifyItem('track', t)), user_playlists: userPlaylists }; @@ -260,6 +281,7 @@ export class MusicDatabase { await importStore('favorites_albums', data.favorites_albums); await importStore('favorites_artists', data.favorites_artists); await importStore('favorites_playlists', data.favorites_playlists); + await importStore('favorites_mixes', data.favorites_mixes); await importStore('history_tracks', data.history_tracks); if (data.user_playlists) { await importStore('user_playlists', data.user_playlists); diff --git a/js/events.js b/js/events.js index 3ac3b3e..795faa9 100644 --- a/js/events.js +++ b/js/events.js @@ -540,6 +540,9 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen } else if (type === 'playlist') { const data = await api.getPlaylist(id); item = data.playlist; + } else if (type === 'mix') { + const data = await api.getMix(id); + item = data.mix; } } catch (err) { console.error(err); } } diff --git a/js/storage.js b/js/storage.js index 820cbca..098d564 100644 --- a/js/storage.js +++ b/js/storage.js @@ -249,11 +249,12 @@ export const recentActivityManager = { _get() { try { const data = localStorage.getItem(this.STORAGE_KEY); - const parsed = data ? JSON.parse(data) : { artists: [], albums: [], playlists: [] }; + const parsed = data ? JSON.parse(data) : { artists: [], albums: [], playlists: [], mixes: [] }; if (!parsed.playlists) parsed.playlists = []; + if (!parsed.mixes) parsed.mixes = []; return parsed; } catch (e) { - return { artists: [], albums: [], playlists: [] }; + return { artists: [], albums: [], playlists: [], mixes: [] }; } }, @@ -283,6 +284,10 @@ export const recentActivityManager = { addPlaylist(playlist) { this._add('playlists', playlist); + }, + + addMix(mix) { + this._add('mixes', mix); } }; diff --git a/js/ui.js b/js/ui.js index 985d396..35999ee 100644 --- a/js/ui.js +++ b/js/ui.js @@ -207,6 +207,27 @@ export class UIRenderer { `; } + createMixCardHTML(mix) { + const imageSrc = mix.cover || 'assets/appicon.png'; + const description = mix.subTitle || mix.description || ''; + + return ` +
+
+ ${mix.title} + + +
+

${mix.title}

+

${description}

+
+ `; + } + createUserPlaylistCardHTML(playlist) { let imageHTML = ''; @@ -607,8 +628,22 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) { } const likedPlaylists = await db.getFavorites('playlist'); - if (likedPlaylists.length) { - playlistsContainer.innerHTML = likedPlaylists.map(p => this.createPlaylistCardHTML(p)).join(''); + const likedMixes = await db.getFavorites('mix'); + + let mixedContent = []; + if (likedPlaylists.length) mixedContent.push(...likedPlaylists.map(p => ({ ...p, _type: 'playlist' }))); + if (likedMixes.length) mixedContent.push(...likedMixes.map(m => ({ ...m, _type: 'mix' }))); + + // Sort by addedAt descending + mixedContent.sort((a, b) => b.addedAt - a.addedAt); + + if (mixedContent.length) { + playlistsContainer.innerHTML = mixedContent.map(item => { + return item._type === 'playlist' + ? this.createPlaylistCardHTML(item) + : this.createMixCardHTML(item); + }).join(''); + likedPlaylists.forEach(playlist => { const el = playlistsContainer.querySelector(`[data-playlist-id="${playlist.uuid}"]`); if (el) { @@ -616,8 +651,16 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) { this.updateLikeState(el, 'playlist', playlist.uuid); } }); + + likedMixes.forEach(mix => { + const el = playlistsContainer.querySelector(`[data-mix-id="${mix.id}"]`); + if (el) { + trackDataStore.set(el, mix); + this.updateLikeState(el, 'mix', mix.id); + } + }); } else { - playlistsContainer.innerHTML = createPlaceholder('No liked playlists yet.'); + playlistsContainer.innerHTML = createPlaceholder('No liked playlists or mixes yet.'); } const myPlaylistsContainer = document.getElementById('my-playlists-container'); @@ -670,26 +713,47 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) { } if (playlistsContainer) { - if (recents.playlists && recents.playlists.length) { - playlistsContainer.innerHTML = recents.playlists.map(playlist => { - if (playlist.isUserPlaylist) { - return this.createUserPlaylistCardHTML(playlist); + const playlists = recents.playlists || []; + const mixes = recents.mixes || []; + + // Note: Since we don't have a unified timestamp for recents in the separate arrays without normalizing, + // we will just display playlists then mixes, or interleave them if we wanted to be fancy. + // But usually recents are just lists. + // Let's just concatenate them. + + const combinedRecents = [...playlists, ...mixes]; // Order: Playlists then Mixes + + if (combinedRecents.length) { + playlistsContainer.innerHTML = combinedRecents.map(item => { + if (item.isUserPlaylist) { + return this.createUserPlaylistCardHTML(item); } - return this.createPlaylistCardHTML(playlist); + if (item.mixType) { // It's a mix + return this.createMixCardHTML(item); + } + return this.createPlaylistCardHTML(item); }).join(''); - recents.playlists.forEach(playlist => { - const id = playlist.isUserPlaylist ? playlist.id : playlist.uuid; - const el = playlistsContainer.querySelector(`[data-playlist-id="${id}"]`); - if (el) { - trackDataStore.set(el, playlist); - if (!playlist.isUserPlaylist) { - this.updateLikeState(el, 'playlist', playlist.uuid); + combinedRecents.forEach(item => { + if (item.isUserPlaylist) { + const el = playlistsContainer.querySelector(`[data-playlist-id="${item.id}"]`); + if (el) trackDataStore.set(el, item); + } else if (item.mixType) { + const el = playlistsContainer.querySelector(`[data-mix-id="${item.id}"]`); + if (el) { + trackDataStore.set(el, item); + this.updateLikeState(el, 'mix', item.id); + } + } else { + const el = playlistsContainer.querySelector(`[data-playlist-id="${item.uuid}"]`); + if (el) { + trackDataStore.set(el, item); + this.updateLikeState(el, 'playlist', item.uuid); } } }); } else { - playlistsContainer.innerHTML = createPlaceholder("You haven't viewed any playlists yet."); + playlistsContainer.innerHTML = createPlaceholder("You haven't viewed any playlists or mixes yet."); } } } @@ -1245,6 +1309,17 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) { this.player.setQueue(tracks, 0); this.player.playTrackFromQueue(); }; + + recentActivityManager.addMix(mix); + + // Update header like button + const mixLikeBtn = document.getElementById('like-mix-btn'); + if (mixLikeBtn) { + mixLikeBtn.style.display = 'flex'; + const isLiked = await db.isFavorite('mix', mix.id); + mixLikeBtn.innerHTML = this.createHeartIcon(isLiked); + mixLikeBtn.classList.toggle('active', isLiked); + } document.title = `${displayTitle} - Monochrome`; } catch (error) { diff --git a/sw.js b/sw.js index 496589e..6c8fe23 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ // sw.js -const SW_VERSION = 'monochrome-v7'; // Note To Self: Change Every Deploy +const SW_VERSION = 'monochrome-v8'; // Note To Self: Change Every Deploy const CACHE_NAME = `monochrome-${SW_VERSION}`; const ASSETS = [ @@ -23,6 +23,7 @@ const ASSETS = [ '/js/downloads.js', '/js/db.js', '/js/metadata.js', + '/js/vibrant-color.js', '/manifest.json', '/assets/logo.svg',