IMP: liked mixes
This commit is contained in:
parent
6ee3c57bc5
commit
3f1124f1f0
6 changed files with 134 additions and 25 deletions
|
|
@ -177,7 +177,7 @@
|
|||
<div class="card-grid" id="home-recent-artists"></div>
|
||||
</section>
|
||||
<section class="content-section">
|
||||
<h2 class="section-title">Recent Playlists</h2>
|
||||
<h2 class="section-title">Recent Playlists and Mixes</h2>
|
||||
<div class="card-grid" id="home-recent-playlists"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
<button class="search-tab active" data-tab="tracks">Liked Tracks</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="playlists">Playlists</button>
|
||||
<button class="search-tab" data-tab="playlists">Playlists and Mixes</button>
|
||||
</div>
|
||||
<div class="search-tab-content active" id="library-tab-tracks">
|
||||
<div class="track-list" id="library-tracks-container"></div>
|
||||
|
|
@ -324,6 +324,9 @@
|
|||
<button id="download-mix-btn" class="btn-primary">
|
||||
<span>Download</span>
|
||||
</button>
|
||||
<button id="like-mix-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="mix" title="Save to Favorites" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
30
js/db.js
30
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);
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
107
js/ui.js
107
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 `
|
||||
<div class="card" data-mix-id="${mix.id}" data-href="#mix/${mix.id}" style="cursor: pointer;">
|
||||
<div class="card-image-wrapper">
|
||||
<img src="${imageSrc}" alt="${mix.title}" class="card-image" loading="lazy">
|
||||
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="mix" title="Add to Liked">
|
||||
${this.createHeartIcon(false)}
|
||||
</button>
|
||||
<button class="play-btn card-play-btn" data-action="play-card" data-type="mix" data-id="${mix.id}" title="Play">
|
||||
${SVG_PLAY}
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="card-title">${mix.title}</h3>
|
||||
<p class="card-subtitle">${description}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
3
sw.js
3
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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue