Fix: Correct data attribute usage for user playlist edit/delete buttons
This commit is contained in:
parent
21c947fd68
commit
e0528d512b
9 changed files with 327 additions and 98 deletions
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
|
@ -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
38
CONTRIBUTE.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
32
index.html
32
index.html
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
95
js/app.js
95
js/app.js
|
|
@ -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);
|
||||
|
|
|
|||
10
js/db.js
10
js/db.js
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
149
js/ui.js
|
|
@ -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';
|
||||
|
|
|
|||
41
readme.md
41
readme.md
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue