Add wishlist batch remove API and UI
Introduce batch removal support for wishlist tracks. Adds a new POST endpoint /api/wishlist/remove-batch that validates input, removes multiple tracks via the wishlist service, logs the result and returns a removed count. Updates the frontend (webui/static/script.js) to provide per-track and per-album checkboxes, a Select All button, a batch action bar with selection count and a Remove Selected action (with confirmation), and logic to refresh the view and wishlist count after removal. Styles (webui/static/style.css) are extended to support unified watchlist/wishlist batch bars, checkbox styling, and a Select All button. Preserves existing single-item removal behavior.
This commit is contained in:
parent
8c145c29cd
commit
e14a317a56
3 changed files with 229 additions and 12 deletions
|
|
@ -13054,6 +13054,35 @@ def remove_album_from_wishlist():
|
|||
logger.error(f"Error removing album from wishlist: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/wishlist/remove-batch', methods=['POST'])
|
||||
def remove_batch_from_wishlist():
|
||||
"""Endpoint to remove multiple tracks from the wishlist."""
|
||||
try:
|
||||
from core.wishlist_service import get_wishlist_service
|
||||
|
||||
data = request.get_json()
|
||||
spotify_track_ids = data.get('spotify_track_ids', [])
|
||||
|
||||
if not spotify_track_ids or not isinstance(spotify_track_ids, list):
|
||||
return jsonify({"success": False, "error": "Missing or invalid spotify_track_ids"}), 400
|
||||
|
||||
wishlist_service = get_wishlist_service()
|
||||
removed = 0
|
||||
for track_id in spotify_track_ids:
|
||||
if wishlist_service.remove_track_from_wishlist(track_id):
|
||||
removed += 1
|
||||
|
||||
logger.info(f"Batch removed {removed} track(s) from wishlist")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"removed": removed,
|
||||
"message": f"Removed {removed} track{'s' if removed != 1 else ''} from wishlist"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error batch removing from wishlist: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/add-album-to-wishlist', methods=['POST'])
|
||||
def add_album_track_to_wishlist():
|
||||
"""Endpoint to add a single track from an album to the wishlist."""
|
||||
|
|
|
|||
|
|
@ -6995,6 +6995,14 @@ async function openWishlistOverviewModal() {
|
|||
<div class="wishlist-category-header">
|
||||
<button class="wishlist-back-btn" onclick="backToCategories()">← Back</button>
|
||||
<span id="wishlist-category-name" class="wishlist-category-name"></span>
|
||||
<button class="wishlist-select-all-btn" id="wishlist-select-all-btn" onclick="toggleWishlistSelectAll()">Select All</button>
|
||||
</div>
|
||||
<div class="wishlist-batch-bar" id="wishlist-batch-bar" style="display: none;">
|
||||
<span class="wishlist-batch-count" id="wishlist-batch-count">0 selected</span>
|
||||
<button class="playlist-modal-btn playlist-modal-btn-secondary wishlist-batch-remove-btn"
|
||||
onclick="batchRemoveFromWishlist()">
|
||||
Remove Selected
|
||||
</button>
|
||||
</div>
|
||||
<div id="wishlist-tracks-list" class="playlist-tracks-scroll">
|
||||
<div class="loading-indicator">Loading tracks...</div>
|
||||
|
|
@ -7312,6 +7320,12 @@ async function selectWishlistCategory(category) {
|
|||
|
||||
const tracksListHTML = albumData.tracks.map(track => `
|
||||
<div class="wishlist-album-track wishlist-track-item">
|
||||
<label class="wishlist-checkbox-wrapper" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" class="wishlist-select-cb"
|
||||
data-track-id="${track.spotifyTrackId}"
|
||||
onchange="updateWishlistBatchBar()">
|
||||
<span class="wishlist-checkbox-custom"></span>
|
||||
</label>
|
||||
<span class="wishlist-album-track-name">${track.name}</span>
|
||||
<button class="wishlist-delete-btn wishlist-delete-btn-small" onclick="removeTrackFromWishlist('${track.spotifyTrackId}', event)" title="Remove from wishlist">
|
||||
🗑️
|
||||
|
|
@ -7328,6 +7342,12 @@ async function selectWishlistCategory(category) {
|
|||
albumsHTML += `
|
||||
<div class="wishlist-album-card">
|
||||
<div class="wishlist-album-header" onclick="toggleAlbumTracks('${albumId}')">
|
||||
<label class="wishlist-checkbox-wrapper" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" class="wishlist-album-select-all-cb"
|
||||
data-album-id="${albumId}"
|
||||
onchange="toggleWishlistAlbumSelection('${albumId}', this.checked)">
|
||||
<span class="wishlist-checkbox-custom"></span>
|
||||
</label>
|
||||
<div class="wishlist-album-image" style="${albumImageStyle}">${albumImageContent}</div>
|
||||
<div class="wishlist-album-info">
|
||||
<div class="wishlist-album-name">${albumData.albumName}</div>
|
||||
|
|
@ -7389,6 +7409,12 @@ async function selectWishlistCategory(category) {
|
|||
|
||||
tracksHTML += `
|
||||
<div class="playlist-track-item-with-image wishlist-track-item">
|
||||
<label class="wishlist-checkbox-wrapper" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" class="wishlist-select-cb"
|
||||
data-track-id="${spotifyTrackId}"
|
||||
onchange="updateWishlistBatchBar()">
|
||||
<span class="wishlist-checkbox-custom"></span>
|
||||
</label>
|
||||
<div class="playlist-track-image" style="background-image: url('${albumImage}')"></div>
|
||||
<div class="playlist-track-info">
|
||||
<div class="playlist-track-name">${trackName}</div>
|
||||
|
|
@ -7414,10 +7440,12 @@ function backToCategories() {
|
|||
const categoryTracksSection = document.getElementById('wishlist-category-tracks');
|
||||
const categoryGrid = document.querySelector('.wishlist-category-grid');
|
||||
const downloadBtn = document.getElementById('wishlist-download-btn');
|
||||
const batchBar = document.getElementById('wishlist-batch-bar');
|
||||
|
||||
categoryTracksSection.style.display = 'none';
|
||||
categoryGrid.style.display = 'grid';
|
||||
downloadBtn.style.display = 'none';
|
||||
if (batchBar) batchBar.style.display = 'none';
|
||||
window.selectedWishlistCategory = null;
|
||||
}
|
||||
|
||||
|
|
@ -7434,6 +7462,132 @@ function toggleAlbumTracks(albumId) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all checked wishlist track checkboxes
|
||||
*/
|
||||
function getCheckedWishlistTracks() {
|
||||
return Array.from(document.querySelectorAll('.wishlist-select-cb:checked'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle select all / deselect all tracks in the current wishlist category
|
||||
*/
|
||||
function toggleWishlistSelectAll() {
|
||||
const allCheckboxes = document.querySelectorAll('.wishlist-select-cb');
|
||||
const albumCheckboxes = document.querySelectorAll('.wishlist-album-select-all-cb');
|
||||
const btn = document.getElementById('wishlist-select-all-btn');
|
||||
const allChecked = allCheckboxes.length > 0 && Array.from(allCheckboxes).every(cb => cb.checked);
|
||||
|
||||
const newState = !allChecked;
|
||||
|
||||
allCheckboxes.forEach(cb => { cb.checked = newState; });
|
||||
albumCheckboxes.forEach(cb => { cb.checked = newState; });
|
||||
|
||||
// Expand all albums when selecting all
|
||||
if (newState) {
|
||||
document.querySelectorAll('.wishlist-album-tracks').forEach(el => {
|
||||
el.style.display = 'block';
|
||||
});
|
||||
document.querySelectorAll('[id^="expand-icon-"]').forEach(icon => {
|
||||
icon.textContent = '▲';
|
||||
});
|
||||
}
|
||||
|
||||
if (btn) btn.textContent = newState ? 'Deselect All' : 'Select All';
|
||||
updateWishlistBatchBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the wishlist batch action bar based on checkbox selection
|
||||
*/
|
||||
function updateWishlistBatchBar() {
|
||||
const checked = getCheckedWishlistTracks();
|
||||
const bar = document.getElementById('wishlist-batch-bar');
|
||||
const countEl = document.getElementById('wishlist-batch-count');
|
||||
|
||||
if (!bar || !countEl) return;
|
||||
|
||||
if (checked.length > 0) {
|
||||
bar.style.display = 'flex';
|
||||
countEl.textContent = `${checked.length} selected`;
|
||||
} else {
|
||||
bar.style.display = 'none';
|
||||
}
|
||||
|
||||
// Sync the Select All button text
|
||||
const btn = document.getElementById('wishlist-select-all-btn');
|
||||
if (btn) {
|
||||
const allCheckboxes = document.querySelectorAll('.wishlist-select-cb');
|
||||
const allChecked = allCheckboxes.length > 0 && Array.from(allCheckboxes).every(cb => cb.checked);
|
||||
btn.textContent = allChecked ? 'Deselect All' : 'Select All';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle all track checkboxes within an album when album header checkbox is clicked
|
||||
*/
|
||||
function toggleWishlistAlbumSelection(albumId, checked) {
|
||||
const tracksContainer = document.getElementById(`tracks-${albumId}`);
|
||||
if (tracksContainer) {
|
||||
// Expand the album tracks if selecting
|
||||
if (checked) {
|
||||
tracksContainer.style.display = 'block';
|
||||
const expandIcon = document.getElementById(`expand-icon-${albumId}`);
|
||||
if (expandIcon) expandIcon.textContent = '▲';
|
||||
}
|
||||
tracksContainer.querySelectorAll('.wishlist-select-cb').forEach(cb => {
|
||||
cb.checked = checked;
|
||||
});
|
||||
}
|
||||
updateWishlistBatchBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch remove selected tracks from wishlist
|
||||
*/
|
||||
async function batchRemoveFromWishlist() {
|
||||
const checked = getCheckedWishlistTracks();
|
||||
if (checked.length === 0) return;
|
||||
|
||||
const count = checked.length;
|
||||
const confirmed = await showConfirmationModal(
|
||||
'Remove Tracks',
|
||||
`Remove ${count} track${count !== 1 ? 's' : ''} from your wishlist?`,
|
||||
'🗑️'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const trackIds = checked.map(cb => cb.getAttribute('data-track-id'));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/wishlist/remove-batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ spotify_track_ids: trackIds })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(`Removed ${data.removed} track(s) from wishlist`, 'success');
|
||||
|
||||
// Reload the current category to refresh the list
|
||||
if (window.selectedWishlistCategory) {
|
||||
await selectWishlistCategory(window.selectedWishlistCategory);
|
||||
}
|
||||
|
||||
// Update wishlist count in sidebar
|
||||
await updateWishlistCount();
|
||||
} else {
|
||||
showToast(`Failed to remove tracks: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error batch removing from wishlist:', error);
|
||||
showToast('Failed to remove tracks from wishlist', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirmationModal(title, message, icon = '⚠️') {
|
||||
return new Promise((resolve) => {
|
||||
// Create modal if it doesn't exist
|
||||
|
|
|
|||
|
|
@ -8437,8 +8437,9 @@ body {
|
|||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Watchlist Batch Action Bar */
|
||||
.watchlist-batch-bar {
|
||||
/* Watchlist & Wishlist Batch Action Bar */
|
||||
.watchlist-batch-bar,
|
||||
.wishlist-batch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -8462,24 +8463,28 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.watchlist-batch-count {
|
||||
.watchlist-batch-count,
|
||||
.wishlist-batch-count {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.watchlist-batch-remove-btn {
|
||||
.watchlist-batch-remove-btn,
|
||||
.wishlist-batch-remove-btn {
|
||||
border-color: rgba(255, 59, 48, 0.4) !important;
|
||||
color: #ff6b6b !important;
|
||||
}
|
||||
|
||||
.watchlist-batch-remove-btn:hover {
|
||||
.watchlist-batch-remove-btn:hover,
|
||||
.wishlist-batch-remove-btn:hover {
|
||||
background: rgba(255, 59, 48, 0.2) !important;
|
||||
border-color: rgba(255, 59, 48, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Watchlist Checkbox */
|
||||
.watchlist-checkbox-wrapper {
|
||||
/* Watchlist & Wishlist Checkbox */
|
||||
.watchlist-checkbox-wrapper,
|
||||
.wishlist-checkbox-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -8489,14 +8494,16 @@ body {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.watchlist-checkbox-wrapper input[type="checkbox"] {
|
||||
.watchlist-checkbox-wrapper input[type="checkbox"],
|
||||
.wishlist-checkbox-wrapper input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.watchlist-checkbox-custom {
|
||||
.watchlist-checkbox-custom,
|
||||
.wishlist-checkbox-custom {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
|
|
@ -8508,17 +8515,20 @@ body {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.watchlist-checkbox-wrapper:hover .watchlist-checkbox-custom {
|
||||
.watchlist-checkbox-wrapper:hover .watchlist-checkbox-custom,
|
||||
.wishlist-checkbox-wrapper:hover .wishlist-checkbox-custom {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.watchlist-checkbox-wrapper input:checked+.watchlist-checkbox-custom {
|
||||
.watchlist-checkbox-wrapper input:checked+.watchlist-checkbox-custom,
|
||||
.wishlist-checkbox-wrapper input:checked+.wishlist-checkbox-custom {
|
||||
background: rgba(29, 185, 84, 0.3);
|
||||
border-color: #1db954;
|
||||
}
|
||||
|
||||
.watchlist-checkbox-wrapper input:checked+.watchlist-checkbox-custom::after {
|
||||
.watchlist-checkbox-wrapper input:checked+.watchlist-checkbox-custom::after,
|
||||
.wishlist-checkbox-wrapper input:checked+.wishlist-checkbox-custom::after {
|
||||
content: '✓';
|
||||
color: #1db954;
|
||||
font-size: 13px;
|
||||
|
|
@ -8784,6 +8794,30 @@ body {
|
|||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.wishlist-select-all-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: #e0e0e0;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wishlist-select-all-btn:hover {
|
||||
background: rgba(29, 185, 84, 0.15);
|
||||
border-color: rgba(29, 185, 84, 0.4);
|
||||
color: #1db954;
|
||||
}
|
||||
|
||||
.wishlist-batch-bar {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
|
|
|
|||
Loading…
Reference in a new issue