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>
|
<div class="card-grid" id="home-recent-artists"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="content-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>
|
<div class="card-grid" id="home-recent-playlists"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
<button class="search-tab active" data-tab="tracks">Liked Tracks</button>
|
<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="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>
|
<button class="search-tab" data-tab="playlists">Playlists and Mixes</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-tab-content active" id="library-tab-tracks">
|
<div class="search-tab-content active" id="library-tab-tracks">
|
||||||
<div class="track-list" id="library-tracks-container"></div>
|
<div class="track-list" id="library-tracks-container"></div>
|
||||||
|
|
@ -324,6 +324,9 @@
|
||||||
<button id="download-mix-btn" class="btn-primary">
|
<button id="download-mix-btn" class="btn-primary">
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
30
js/db.js
30
js/db.js
|
|
@ -1,7 +1,7 @@
|
||||||
export class MusicDatabase {
|
export class MusicDatabase {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbName = 'MonochromeDB';
|
this.dbName = 'MonochromeDB';
|
||||||
this.version = 4;
|
this.version = 5;
|
||||||
this.db = null;
|
this.db = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +41,10 @@ export class MusicDatabase {
|
||||||
const store = db.createObjectStore('favorites_playlists', { keyPath: 'uuid' });
|
const store = db.createObjectStore('favorites_playlists', { keyPath: 'uuid' });
|
||||||
store.createIndex('addedAt', 'addedAt', { unique: false });
|
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')) {
|
if (!db.objectStoreNames.contains('history_tracks')) {
|
||||||
const store = db.createObjectStore('history_tracks', { keyPath: 'timestamp' });
|
const store = db.createObjectStore('history_tracks', { keyPath: 'timestamp' });
|
||||||
store.createIndex('timestamp', 'timestamp', { unique: true });
|
store.createIndex('timestamp', 'timestamp', { unique: true });
|
||||||
|
|
@ -107,7 +111,8 @@ export class MusicDatabase {
|
||||||
|
|
||||||
// Favorites API
|
// Favorites API
|
||||||
async toggleFavorite(type, item) {
|
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 key = type === 'playlist' ? item.uuid : item.id;
|
||||||
const exists = await this.isFavorite(type, key);
|
const exists = await this.isFavorite(type, key);
|
||||||
|
|
||||||
|
|
@ -123,7 +128,8 @@ export class MusicDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
async isFavorite(type, id) {
|
async isFavorite(type, id) {
|
||||||
const storeName = `favorites_${type}s`;
|
const plural = type === 'mix' ? 'mixes' : `${type}s`;
|
||||||
|
const storeName = `favorites_${plural}`;
|
||||||
try {
|
try {
|
||||||
const result = await this.performTransaction(storeName, 'readonly', (store) => store.get(id));
|
const result = await this.performTransaction(storeName, 'readonly', (store) => store.get(id));
|
||||||
return !!result;
|
return !!result;
|
||||||
|
|
@ -133,7 +139,8 @@ export class MusicDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFavorites(type) {
|
async getFavorites(type) {
|
||||||
const storeName = `favorites_${type}s`;
|
const plural = type === 'mix' ? 'mixes' : `${type}s`;
|
||||||
|
const storeName = `favorites_${plural}`;
|
||||||
const db = await this.open();
|
const db = await this.open();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readonly');
|
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;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +241,7 @@ export class MusicDatabase {
|
||||||
const albums = await this.getFavorites('album');
|
const albums = await this.getFavorites('album');
|
||||||
const artists = await this.getFavorites('artist');
|
const artists = await this.getFavorites('artist');
|
||||||
const playlists = await this.getFavorites('playlist');
|
const playlists = await this.getFavorites('playlist');
|
||||||
|
const mixes = await this.getFavorites('mix');
|
||||||
const history = await this.getHistory();
|
const history = await this.getHistory();
|
||||||
|
|
||||||
const userPlaylists = await this.getPlaylists();
|
const userPlaylists = await this.getPlaylists();
|
||||||
|
|
@ -230,6 +250,7 @@ export class MusicDatabase {
|
||||||
favorites_albums: albums.map(a => this._minifyItem('album', a)),
|
favorites_albums: albums.map(a => this._minifyItem('album', a)),
|
||||||
favorites_artists: artists.map(a => this._minifyItem('artist', a)),
|
favorites_artists: artists.map(a => this._minifyItem('artist', a)),
|
||||||
favorites_playlists: playlists.map(p => this._minifyItem('playlist', p)),
|
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)),
|
history_tracks: history.map(t => this._minifyItem('track', t)),
|
||||||
user_playlists: userPlaylists
|
user_playlists: userPlaylists
|
||||||
};
|
};
|
||||||
|
|
@ -260,6 +281,7 @@ export class MusicDatabase {
|
||||||
await importStore('favorites_albums', data.favorites_albums);
|
await importStore('favorites_albums', data.favorites_albums);
|
||||||
await importStore('favorites_artists', data.favorites_artists);
|
await importStore('favorites_artists', data.favorites_artists);
|
||||||
await importStore('favorites_playlists', data.favorites_playlists);
|
await importStore('favorites_playlists', data.favorites_playlists);
|
||||||
|
await importStore('favorites_mixes', data.favorites_mixes);
|
||||||
await importStore('history_tracks', data.history_tracks);
|
await importStore('history_tracks', data.history_tracks);
|
||||||
if (data.user_playlists) {
|
if (data.user_playlists) {
|
||||||
await importStore('user_playlists', 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') {
|
} else if (type === 'playlist') {
|
||||||
const data = await api.getPlaylist(id);
|
const data = await api.getPlaylist(id);
|
||||||
item = data.playlist;
|
item = data.playlist;
|
||||||
|
} else if (type === 'mix') {
|
||||||
|
const data = await api.getMix(id);
|
||||||
|
item = data.mix;
|
||||||
}
|
}
|
||||||
} catch (err) { console.error(err); }
|
} catch (err) { console.error(err); }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -249,11 +249,12 @@ export const recentActivityManager = {
|
||||||
_get() {
|
_get() {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
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.playlists) parsed.playlists = [];
|
||||||
|
if (!parsed.mixes) parsed.mixes = [];
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { artists: [], albums: [], playlists: [] };
|
return { artists: [], albums: [], playlists: [], mixes: [] };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -283,6 +284,10 @@ export const recentActivityManager = {
|
||||||
|
|
||||||
addPlaylist(playlist) {
|
addPlaylist(playlist) {
|
||||||
this._add('playlists', 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) {
|
createUserPlaylistCardHTML(playlist) {
|
||||||
let imageHTML = '';
|
let imageHTML = '';
|
||||||
|
|
||||||
|
|
@ -607,8 +628,22 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const likedPlaylists = await db.getFavorites('playlist');
|
const likedPlaylists = await db.getFavorites('playlist');
|
||||||
if (likedPlaylists.length) {
|
const likedMixes = await db.getFavorites('mix');
|
||||||
playlistsContainer.innerHTML = likedPlaylists.map(p => this.createPlaylistCardHTML(p)).join('');
|
|
||||||
|
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 => {
|
likedPlaylists.forEach(playlist => {
|
||||||
const el = playlistsContainer.querySelector(`[data-playlist-id="${playlist.uuid}"]`);
|
const el = playlistsContainer.querySelector(`[data-playlist-id="${playlist.uuid}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|
@ -616,8 +651,16 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
this.updateLikeState(el, 'playlist', playlist.uuid);
|
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 {
|
} else {
|
||||||
playlistsContainer.innerHTML = createPlaceholder('No liked playlists yet.');
|
playlistsContainer.innerHTML = createPlaceholder('No liked playlists or mixes yet.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const myPlaylistsContainer = document.getElementById('my-playlists-container');
|
const myPlaylistsContainer = document.getElementById('my-playlists-container');
|
||||||
|
|
@ -670,26 +713,47 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistsContainer) {
|
if (playlistsContainer) {
|
||||||
if (recents.playlists && recents.playlists.length) {
|
const playlists = recents.playlists || [];
|
||||||
playlistsContainer.innerHTML = recents.playlists.map(playlist => {
|
const mixes = recents.mixes || [];
|
||||||
if (playlist.isUserPlaylist) {
|
|
||||||
return this.createUserPlaylistCardHTML(playlist);
|
// 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('');
|
}).join('');
|
||||||
|
|
||||||
recents.playlists.forEach(playlist => {
|
combinedRecents.forEach(item => {
|
||||||
const id = playlist.isUserPlaylist ? playlist.id : playlist.uuid;
|
if (item.isUserPlaylist) {
|
||||||
const el = playlistsContainer.querySelector(`[data-playlist-id="${id}"]`);
|
const el = playlistsContainer.querySelector(`[data-playlist-id="${item.id}"]`);
|
||||||
if (el) {
|
if (el) trackDataStore.set(el, item);
|
||||||
trackDataStore.set(el, playlist);
|
} else if (item.mixType) {
|
||||||
if (!playlist.isUserPlaylist) {
|
const el = playlistsContainer.querySelector(`[data-mix-id="${item.id}"]`);
|
||||||
this.updateLikeState(el, 'playlist', playlist.uuid);
|
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 {
|
} 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.setQueue(tracks, 0);
|
||||||
this.player.playTrackFromQueue();
|
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`;
|
document.title = `${displayTitle} - Monochrome`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
3
sw.js
3
sw.js
|
|
@ -1,5 +1,5 @@
|
||||||
// sw.js
|
// 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 CACHE_NAME = `monochrome-${SW_VERSION}`;
|
||||||
|
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
|
|
@ -23,6 +23,7 @@ const ASSETS = [
|
||||||
'/js/downloads.js',
|
'/js/downloads.js',
|
||||||
'/js/db.js',
|
'/js/db.js',
|
||||||
'/js/metadata.js',
|
'/js/metadata.js',
|
||||||
|
'/js/vibrant-color.js',
|
||||||
|
|
||||||
'/manifest.json',
|
'/manifest.json',
|
||||||
'/assets/logo.svg',
|
'/assets/logo.svg',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue