Fix: Correct data attribute usage for user playlist edit/delete buttons

This commit is contained in:
Julien Maille 2026-01-06 21:40:45 +01:00
parent 21c947fd68
commit e0528d512b
9 changed files with 327 additions and 98 deletions

View file

@ -31,4 +31,5 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
publish_branch: deployed-ver
force_orphan: true

38
CONTRIBUTE.md Normal file
View file

@ -0,0 +1,38 @@
# Monochrome Music
A minimalist music streaming application designed for high-fidelity audio playback.
## Development
This project uses [Vite](https://vitejs.dev/) for local development and optimized builds.
### Prerequisites
- [Node.js](https://nodejs.org/) (Version 20+ or 22+ recommended)
### Getting Started
1. Install dependencies:
```bash
npm install
```
2. Start the development server:
```bash
npm run dev
```
The app will be available at `http://localhost:5173/monochrome/`.
### Why Vite?
- **Instant Updates**: Support for Hot Module Replacement (HMR) means changes to JS/CSS are reflected instantly in the browser.
- **Dependency Management**: No more manual path tracking or broken internal imports.
- **Automated PWA**: Service Worker generation and asset hashing are handled automatically.
## Project Structure
- `/js`: Application source code.
- `/public`: Static assets (images, manifest, instances.json) that are copied directly to the build folder.
- `index.html`: The entry point of the application.
- `vite.config.js`: Build and PWA configuration.
## Deployment
Deployment is automated via **GitHub Actions**.
1. Simply push your changes to the `main` branch.
2. The [Deploy to GitHub Pages](.github/workflows/deploy.yml) workflow will trigger automatically.
3. It builds the project (`npm run build`) and publishes the `dist/` folder to the `deployed-ver` branch.

View file

@ -40,10 +40,18 @@ Firebase will block login attempts from unknown domains.
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"public_playlists": {
".read": true,
"$playlistId": {
".write": "auth != null && (!data.exists() || data.child('uid').val() === auth.uid)"
}
}
}
}
```
* **Note:** The `public_playlists` rule allows anyone to read the playlists. The write rule ensures that only authenticated users can publish, and only the owner (creator) of a playlist can modify or delete it.
3. Click **Publish**.
## 5. Get Your Configuration

View file

@ -80,9 +80,27 @@
<input type="file" id="csv-file-input" class="btn-secondary" accept=".csv" style="width: 100%; margin-bottom: 0.5rem;">
<p style="font-size: 0.8rem; margin: 0;">This Feature Isnt Perfect And is Prone To Errors! Please check Your Playlist After To Remove Weird Songs That Were Added By The System.</p>
</div>
<div class="modal-actions" style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button id="playlist-modal-cancel" class="btn-secondary">Cancel</button>
<button id="playlist-modal-save" class="btn-primary">Save</button>
<div class="setting-item" style="margin-bottom: 1rem; padding: 0; border: none; background: transparent;">
<div class="info">
<span class="label">Public Playlist</span>
<span class="description">Visible to anyone with the link</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="playlist-public-toggle">
<span class="slider"></span>
</label>
</div>
<div class="modal-actions" style="display: flex; gap: 0.5rem; justify-content: space-between; align-items: center;">
<button id="playlist-share-btn" class="btn-secondary" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
Share
</button>
<div style="display: flex; gap: 0.5rem;">
<button id="playlist-modal-cancel" class="btn-secondary">Cancel</button>
<button id="playlist-modal-save" class="btn-primary">Save</button>
</div>
</div>
</div>
</div>
@ -235,7 +253,13 @@
<div id="page-library" class="page">
<section class="content-section">
<h2 class="section-title">My Playlists <button id="create-playlist-btn" class="btn-secondary">Create Playlist</button></h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 class="section-title" style="margin-bottom: 0;">My Playlists</h2>
<div style="display: flex; gap: 0.5rem;">
<button id="create-playlist-btn" class="btn-secondary">Create Playlist</button>
</div>
</div>
<div class="card-grid" id="my-playlists-container"></div>
</section>

View file

@ -241,6 +241,12 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
// Toggle Share Button visibility on switch change
document.getElementById('playlist-public-toggle')?.addEventListener('change', (e) => {
const shareBtn = document.getElementById('playlist-share-btn');
if (shareBtn) shareBtn.style.display = e.target.checked ? 'flex' : 'none';
});
document.getElementById('close-fullscreen-cover-btn')?.addEventListener('click', () => {
ui.closeFullscreenCover();
});
@ -390,23 +396,57 @@ document.addEventListener('DOMContentLoaded', async () => {
modal.dataset.editingId = '';
document.getElementById('csv-import-section').style.display = 'block';
document.getElementById('csv-file-input').value = '';
// Reset Public Toggle
const publicToggle = document.getElementById('playlist-public-toggle');
const shareBtn = document.getElementById('playlist-share-btn');
if (publicToggle) publicToggle.checked = false;
if (shareBtn) shareBtn.style.display = 'none';
modal.style.display = 'flex';
document.getElementById('playlist-name-input').focus();
}
if (e.target.closest('#playlist-modal-save')) {
const name = document.getElementById('playlist-name-input').value.trim();
const isPublic = document.getElementById('playlist-public-toggle')?.checked;
if (name) {
const modal = document.getElementById('playlist-modal');
const editingId = modal.dataset.editingId;
const handlePublicStatus = async (playlist) => {
playlist.isPublic = isPublic;
if (isPublic) {
try {
await syncManager.publishPlaylist(playlist);
} catch (e) {
console.error('Failed to publish playlist:', e);
alert('Failed to publish playlist. Please ensure you are logged in.');
}
} else {
try {
await syncManager.unpublishPlaylist(playlist.id);
} catch (e) {
// Ignore error if it wasn't public
}
}
return playlist;
};
if (editingId) {
// Edit
db.getPlaylist(editingId).then(async (playlist) => {
if (playlist) {
playlist.name = name;
await handlePublicStatus(playlist);
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
syncManager.syncUserPlaylist(playlist, 'update');
ui.renderLibraryPage();
// Also update current page if we are on it
if (window.location.hash === `#userplaylist/${editingId}`) {
ui.renderPlaylistPage(editingId);
}
modal.style.display = 'none';
delete modal.dataset.editingId;
}
@ -473,7 +513,10 @@ document.addEventListener('DOMContentLoaded', async () => {
}
}
db.createPlaylist(name, tracks, '').then(playlist => {
db.createPlaylist(name, [], '').then(async playlist => {
await handlePublicStatus(playlist);
// Update DB again with isPublic flag
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
syncManager.syncUserPlaylist(playlist, 'create');
ui.renderLibraryPage();
modal.style.display = 'none';
@ -488,12 +531,29 @@ document.addEventListener('DOMContentLoaded', async () => {
if (e.target.closest('.edit-playlist-btn')) {
const card = e.target.closest('.user-playlist');
const playlistId = card.dataset.playlistId;
db.getPlaylist(playlistId).then(playlist => {
const playlistId = card.dataset.userPlaylistId;
db.getPlaylist(playlistId).then(async playlist => {
if (playlist) {
const modal = document.getElementById('playlist-modal');
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
document.getElementById('playlist-name-input').value = playlist.name;
// Set Public Toggle
const publicToggle = document.getElementById('playlist-public-toggle');
const shareBtn = document.getElementById('playlist-share-btn');
// Check if actually public in Firebase to be sure (async) or trust local flag
// We trust local flag for UI speed, but could verify.
if (publicToggle) publicToggle.checked = !!playlist.isPublic;
if (shareBtn) {
shareBtn.style.display = playlist.isPublic ? 'flex' : 'none';
shareBtn.onclick = () => {
const url = `${window.location.origin}${window.location.pathname}#userplaylist/${playlist.id}`;
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
};
}
modal.dataset.editingId = playlistId;
document.getElementById('csv-import-section').style.display = 'none';
modal.style.display = 'flex';
@ -504,7 +564,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (e.target.closest('.delete-playlist-btn')) {
const card = e.target.closest('.user-playlist');
const playlistId = card.dataset.playlistId;
const playlistId = card.dataset.userPlaylistId;
if (confirm('Are you sure you want to delete this playlist?')) {
db.deletePlaylist(playlistId).then(() => {
syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
@ -520,6 +580,19 @@ document.addEventListener('DOMContentLoaded', async () => {
const modal = document.getElementById('playlist-modal');
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
document.getElementById('playlist-name-input').value = playlist.name;
const publicToggle = document.getElementById('playlist-public-toggle');
const shareBtn = document.getElementById('playlist-share-btn');
if (publicToggle) publicToggle.checked = !!playlist.isPublic;
if (shareBtn) {
shareBtn.style.display = playlist.isPublic ? 'flex' : 'none';
shareBtn.onclick = () => {
const url = `${window.location.origin}${window.location.pathname}#userplaylist/${playlist.id}`;
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
};
}
modal.dataset.editingId = playlistId;
document.getElementById('csv-import-section').style.display = 'none';
modal.style.display = 'flex';
@ -565,8 +638,18 @@ document.addEventListener('DOMContentLoaded', async () => {
if (userPlaylist) {
tracks = userPlaylist.tracks;
} else {
const { tracks: apiTracks } = await api.getPlaylist(playlistId);
tracks = apiTracks;
// Try API, if fail, try Public Firebase
try {
const { tracks: apiTracks } = await api.getPlaylist(playlistId);
tracks = apiTracks;
} catch (e) {
const publicPlaylist = await syncManager.getPublicPlaylist(playlistId);
if (publicPlaylist) {
tracks = publicPlaylist.tracks;
} else {
throw e;
}
}
}
if (tracks.length > 0) {
player.setQueue(tracks, 0);

View file

@ -212,12 +212,12 @@ export class MusicDatabase {
if (type === 'playlist') {
return {
uuid: item.uuid,
addedAt: item.addedAt,
title: item.title,
uuid: item.uuid || item.id,
addedAt: item.addedAt || item.createdAt || null,
title: item.title || item.name,
// UI checks squareImage || image || uuid
image: item.image || item.squareImage,
numberOfTracks: item.numberOfTracks,
image: item.image || item.squareImage || item.cover || null,
numberOfTracks: item.numberOfTracks || (item.tracks ? item.tracks.length : 0),
user: item.user ? { name: item.user.name } : null
};
}

View file

@ -274,6 +274,57 @@ export class SyncManager {
}
await remove(this.userRef);
}
// Public Playlist API
async publishPlaylist(playlist) {
if (!this.user) throw new Error("Not authenticated");
const minified = db._minifyItem('playlist', playlist);
const playlistId = playlist.id || playlist.uuid;
if (!playlistId) throw new Error("Invalid playlist ID");
// Ensure playlist has necessary data
const publicData = {
...minified,
uid: this.user.uid,
originalId: playlistId,
publishedAt: Date.now(),
tracks: playlist.tracks ? playlist.tracks.map(t => db._minifyItem('track', t)) : []
};
// Use a global 'public_playlists' node
const publicRef = ref(database, `public_playlists/${playlistId}`);
await set(publicRef, publicData);
}
async unpublishPlaylist(playlistId) {
if (!this.user) throw new Error("Not authenticated");
const publicRef = ref(database, `public_playlists/${playlistId}`);
await remove(publicRef);
}
async getPublicPlaylist(playlistId) {
if (!database) {
console.warn("[Sync] Database not initialized, cannot fetch public playlist");
return null;
}
try {
const publicRef = ref(database, `public_playlists/${playlistId}`);
const snapshot = await get(publicRef);
if (!snapshot.exists()) {
console.warn(`[Sync] Public playlist ${playlistId} not found in database.`);
return null;
}
const data = snapshot.val();
console.log(`[Sync] Public playlist fetch for ${playlistId}: Found`);
return data;
} catch (error) {
console.error("[Sync] Failed to fetch public playlist:", error);
return null;
}
}
}
export const syncManager = new SyncManager();

149
js/ui.js
View file

@ -4,6 +4,7 @@ import { openLyricsPanel } from './lyrics.js';
import { recentActivityManager, backgroundSettings, trackListSettings, cardSettings } from './storage.js';
import { db } from './db.js';
import { getVibrantColorFromImage } from './vibrant-color.js';
import { syncManager } from './firebase/sync.js';
export class UIRenderer {
constructor(api, player) {
@ -710,7 +711,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
if (myPlaylists.length) {
myPlaylistsContainer.innerHTML = myPlaylists.map(p => this.createUserPlaylistCardHTML(p)).join('');
myPlaylists.forEach(playlist => {
const el = myPlaylistsContainer.querySelector(`[data-playlist-id="${playlist.id}"]`);
const el = myPlaylistsContainer.querySelector(`[data-user-playlist-id="${playlist.id}"]`);
if (el) {
trackDataStore.set(el, playlist);
}
@ -778,7 +779,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
combinedRecents.forEach(item => {
if (item.isUserPlaylist) {
const el = playlistsContainer.querySelector(`[data-playlist-id="${item.id}"]`);
const el = playlistsContainer.querySelector(`[data-user-playlist-id="${item.id}"]`);
if (el) trackDataStore.set(el, item);
} else if (item.mixType) {
const el = playlistsContainer.querySelector(`[data-mix-id="${item.id}"]`);
@ -1129,21 +1130,36 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
`;
try {
// Check if it's a user playlist
const userPlaylist = await db.getPlaylist(playlistId);
if (userPlaylist) {
// Render user playlist
imageEl.src = userPlaylist.cover || 'assets/appicon.png';
// Check if it's a user playlist (UUID format)
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(playlistId);
const ownedPlaylist = await db.getPlaylist(playlistId);
let playlistData = ownedPlaylist;
// If not in local DB, check if it's a public Firebase playlist
if (!playlistData) {
try {
playlistData = await syncManager.getPublicPlaylist(playlistId);
} catch (e) {
console.warn('Failed to check public Firebase playlists:', e);
}
}
if (playlistData) {
// ... (rest of the logic)
// Render user or public firebase playlist
imageEl.src = playlistData.cover || 'assets/appicon.png';
imageEl.style.backgroundColor = '';
titleEl.textContent = userPlaylist.name;
this.adjustTitleFontSize(titleEl, userPlaylist.name);
titleEl.textContent = playlistData.name || playlistData.title;
this.adjustTitleFontSize(titleEl, titleEl.textContent);
const tracks = userPlaylist.tracks || [];
const tracks = playlistData.tracks || [];
const totalDuration = calculateTotalDuration(tracks);
metaEl.textContent = `${tracks.length} tracks • ${formatDuration(totalDuration)}`;
descEl.textContent = '';
descEl.textContent = playlistData.description || '';
tracklistContainer.innerHTML = `
<div class="track-list-header">
@ -1155,18 +1171,20 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
this.renderListWithTracks(tracklistContainer, tracks, true);
// Add remove buttons to tracks
const trackItems = tracklistContainer.querySelectorAll('.track-item');
trackItems.forEach((item, index) => {
const actionsDiv = item.querySelector('.track-item-actions');
const removeBtn = document.createElement('button');
removeBtn.className = 'track-action-btn remove-from-playlist-btn';
removeBtn.title = 'Remove from playlist';
removeBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
removeBtn.dataset.trackIndex = index;
const menuBtn = actionsDiv.querySelector('.track-menu-btn');
actionsDiv.insertBefore(removeBtn, menuBtn);
});
// Add remove buttons to tracks ONLY IF OWNED
if (ownedPlaylist) {
const trackItems = tracklistContainer.querySelectorAll('.track-item');
trackItems.forEach((item, index) => {
const actionsDiv = item.querySelector('.track-item-actions');
const removeBtn = document.createElement('button');
removeBtn.className = 'track-action-btn remove-from-playlist-btn';
removeBtn.title = 'Remove from playlist';
removeBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
removeBtn.dataset.trackIndex = index;
const menuBtn = actionsDiv.querySelector('.track-menu-btn');
actionsDiv.insertBefore(removeBtn, menuBtn);
});
}
// Update header like button - hide for user playlists
const playlistLikeBtn = document.getElementById('like-playlist-btn');
@ -1184,6 +1202,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
this.player.playTrackFromQueue();
};
// Add edit and delete buttons ONLY IF OWNED
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions');
const existingShuffle = actionsDiv.querySelector('#shuffle-playlist-btn');
@ -1192,22 +1211,39 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
if (existingEdit) existingEdit.remove();
const existingDelete = actionsDiv.querySelector('#delete-playlist-btn');
if (existingDelete) existingDelete.remove();
const existingShare = actionsDiv.querySelector('#share-playlist-btn');
if (existingShare) existingShare.remove();
const editBtn = document.createElement('button');
editBtn.id = 'edit-playlist-btn';
editBtn.className = 'btn-secondary';
editBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg><span>Edit</span>';
const deleteBtn = document.createElement('button');
deleteBtn.id = 'delete-playlist-btn';
deleteBtn.className = 'btn-secondary danger';
deleteBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg><span>Delete</span>';
actionsDiv.appendChild(shuffleBtn);
actionsDiv.appendChild(editBtn);
actionsDiv.appendChild(deleteBtn);
if (ownedPlaylist) {
const editBtn = document.createElement('button');
editBtn.id = 'edit-playlist-btn';
editBtn.className = 'btn-secondary';
editBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg><span>Edit</span>';
const deleteBtn = document.createElement('button');
deleteBtn.id = 'delete-playlist-btn';
deleteBtn.className = 'btn-secondary danger';
deleteBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg><span>Delete</span>';
actionsDiv.appendChild(editBtn);
actionsDiv.appendChild(deleteBtn);
}
if (playlistData.isPublic) {
const shareBtn = document.createElement('button');
shareBtn.id = 'share-playlist-btn';
shareBtn.className = 'btn-secondary';
shareBtn.innerHTML = '<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"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg><span>Share</span>';
shareBtn.onclick = () => {
const url = `${window.location.origin}${window.location.pathname}#userplaylist/${playlistData.id || playlistData.uuid}`;
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
};
actionsDiv.appendChild(shareBtn);
}
const uniqueCovers = [];
const seenCovers = new Set();
const trackList = userPlaylist.tracks || [];
const trackList = playlistData.tracks || [];
for (const track of trackList) {
const cover = track.album?.cover;
if (cover && !seenCovers.has(cover)) {
@ -1218,19 +1254,31 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
}
recentActivityManager.addPlaylist({
id: userPlaylist.id,
name: userPlaylist.name,
title: userPlaylist.name,
uuid: userPlaylist.id,
cover: userPlaylist.cover,
id: playlistData.id || playlistData.uuid,
name: playlistData.name || playlistData.title,
title: playlistData.title || playlistData.name,
uuid: playlistData.uuid || playlistData.id,
cover: playlistData.cover,
images: uniqueCovers,
numberOfTracks: userPlaylist.tracks ? userPlaylist.tracks.length : 0,
numberOfTracks: playlistData.tracks ? playlistData.tracks.length : 0,
isUserPlaylist: true
});
document.title = userPlaylist.name;
document.title = `${playlistData.name || playlistData.title} - Monochrome`;
} else {
// If it is a UUID, we know it won't be in the API.
if (isUUID) {
throw new Error('Playlist not found. If this is a custom playlist, make sure it is set to Public.');
}
// Render API playlist
const { playlist, tracks } = await this.api.getPlaylist(playlistId);
let apiResult;
try {
apiResult = await this.api.getPlaylist(playlistId);
} catch (error) {
throw error;
}
const { playlist, tracks } = apiResult;
const imageId = playlist.squareImage || playlist.image;
if (imageId) {
@ -1297,6 +1345,23 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
if (deleteBtn) {
deleteBtn.style.display = 'none';
}
// Cleanup potentially stale buttons
const existingEdit = actionsDiv.querySelector('#edit-playlist-btn');
if (existingEdit) existingEdit.remove();
const existingShare = actionsDiv.querySelector('#share-playlist-btn');
if (existingShare) existingShare.remove();
// Add Share button
const shareBtn = document.createElement('button');
shareBtn.id = 'share-playlist-btn';
shareBtn.className = 'btn-secondary';
shareBtn.innerHTML = '<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"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg><span>Share</span>';
shareBtn.onclick = () => {
const url = `${window.location.origin}${window.location.pathname}#playlist/${playlist.uuid}`;
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
};
actionsDiv.appendChild(shareBtn);
recentActivityManager.addPlaylist(playlist);
document.title = playlist.title || 'Artist Mix';

View file

@ -21,44 +21,3 @@ This is not the official repository or instance. It is an actively maintained fo
## **I am Not Affiliated with the original Owner.**
## Development
Monochrome is built with Vanilla JavaScript, HTML, and CSS. No build step is required (no Webpack, Vite, etc.), but because it uses ES Modules, you must run it over HTTP(S).
### Prerequisites
- A modern web browser
- A way to serve static files (e.g., Python, VS Code Live Server, Node.js `http-server`)
### Setup
1. **Clone the repository**
```bash
git clone https://github.com/SamidyFR/monochrome.git
cd monochrome
```
2. **Run locally**
You can use any static file server. For example:
**Using Python 3:**
```bash
python3 -m http.server 8000
```
**Using Node.js `http-server`:**
```bash
npx http-server .
```
3. **Open in Browser**
Navigate to `http://localhost:8000` (or whatever port your server uses).
### Contributing
1. Fork the project
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request