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}
+
${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',