foundation for allowing user to decide which releases to grab per watchlist artist
This commit is contained in:
parent
fd206f552d
commit
1749bb62a5
5 changed files with 712 additions and 2 deletions
|
|
@ -86,6 +86,9 @@ class WatchlistArtist:
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
image_url: Optional[str] = None
|
image_url: Optional[str] = None
|
||||||
|
include_albums: bool = True
|
||||||
|
include_eps: bool = True
|
||||||
|
include_singles: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SimilarArtist:
|
class SimilarArtist:
|
||||||
|
|
@ -262,6 +265,9 @@ class MusicDatabase:
|
||||||
# Add image_url column to watchlist_artists (migration)
|
# Add image_url column to watchlist_artists (migration)
|
||||||
self._add_watchlist_artist_image_column(cursor)
|
self._add_watchlist_artist_image_column(cursor)
|
||||||
|
|
||||||
|
# Add album type filter columns to watchlist_artists (migration)
|
||||||
|
self._add_watchlist_album_type_filters(cursor)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info("Database initialized successfully")
|
logger.info("Database initialized successfully")
|
||||||
|
|
||||||
|
|
@ -591,6 +597,27 @@ class MusicDatabase:
|
||||||
logger.error(f"Error adding image_url column to watchlist_artists: {e}")
|
logger.error(f"Error adding image_url column to watchlist_artists: {e}")
|
||||||
# Don't raise - this is a migration, database can still function
|
# Don't raise - this is a migration, database can still function
|
||||||
|
|
||||||
|
def _add_watchlist_album_type_filters(self, cursor):
|
||||||
|
"""Add album type filter columns to watchlist_artists table"""
|
||||||
|
try:
|
||||||
|
cursor.execute("PRAGMA table_info(watchlist_artists)")
|
||||||
|
columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
|
columns_to_add = {
|
||||||
|
'include_albums': ('INTEGER', '1'), # 1 = True (include albums)
|
||||||
|
'include_eps': ('INTEGER', '1'), # 1 = True (include EPs)
|
||||||
|
'include_singles': ('INTEGER', '1') # 1 = True (include singles)
|
||||||
|
}
|
||||||
|
|
||||||
|
for column_name, (column_type, default_value) in columns_to_add.items():
|
||||||
|
if column_name not in columns:
|
||||||
|
cursor.execute(f"ALTER TABLE watchlist_artists ADD COLUMN {column_name} {column_type} DEFAULT {default_value}")
|
||||||
|
logger.info(f"Added {column_name} column to watchlist_artists table")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding album type filter columns to watchlist_artists: {e}")
|
||||||
|
# Don't raise - this is a migration, database can still function
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close database connection (no-op since we create connections per operation)"""
|
"""Close database connection (no-op since we create connections per operation)"""
|
||||||
# Each operation creates and closes its own connection, so nothing to do here
|
# Each operation creates and closes its own connection, so nothing to do here
|
||||||
|
|
|
||||||
111
web_server.py
111
web_server.py
|
|
@ -12,6 +12,7 @@ import shutil
|
||||||
import glob
|
import glob
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
import re
|
||||||
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
|
@ -14915,6 +14916,116 @@ def get_similar_artists_update_status():
|
||||||
print(f"Error getting similar artists status: {e}")
|
print(f"Error getting similar artists status: {e}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/artist/<artist_id>/config', methods=['GET', 'POST'])
|
||||||
|
def watchlist_artist_config(artist_id):
|
||||||
|
"""Get or update watchlist artist configuration"""
|
||||||
|
try:
|
||||||
|
from database.music_database import get_database
|
||||||
|
|
||||||
|
database = get_database()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
# Get current config from database
|
||||||
|
conn = sqlite3.connect(str(database.database_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT include_albums, include_eps, include_singles, artist_name, image_url
|
||||||
|
FROM watchlist_artists
|
||||||
|
WHERE spotify_artist_id = ?
|
||||||
|
""", (artist_id,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return jsonify({"success": False, "error": "Artist not found in watchlist"}), 404
|
||||||
|
|
||||||
|
# Get artist info from Spotify
|
||||||
|
artist_info = None
|
||||||
|
if spotify_client and spotify_client.is_authenticated():
|
||||||
|
try:
|
||||||
|
artist_data = spotify_client.sp.artist(artist_id)
|
||||||
|
if artist_data:
|
||||||
|
artist_info = {
|
||||||
|
'id': artist_data['id'],
|
||||||
|
'name': artist_data['name'],
|
||||||
|
'image_url': artist_data['images'][0]['url'] if artist_data.get('images') else None,
|
||||||
|
'followers': artist_data.get('followers', {}).get('total', 0),
|
||||||
|
'popularity': artist_data.get('popularity', 0),
|
||||||
|
'genres': artist_data.get('genres', [])
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not fetch artist info from Spotify: {e}")
|
||||||
|
|
||||||
|
# Fallback to database info if Spotify fetch failed
|
||||||
|
if not artist_info:
|
||||||
|
artist_info = {
|
||||||
|
'id': artist_id,
|
||||||
|
'name': result[3], # artist_name
|
||||||
|
'image_url': result[4], # image_url
|
||||||
|
'followers': 0,
|
||||||
|
'popularity': 0,
|
||||||
|
'genres': []
|
||||||
|
}
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'include_albums': bool(result[0]), # Convert INTEGER to boolean
|
||||||
|
'include_eps': bool(result[1]),
|
||||||
|
'include_singles': bool(result[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"config": config,
|
||||||
|
"artist": artist_info
|
||||||
|
})
|
||||||
|
|
||||||
|
else: # POST
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"success": False, "error": "No data provided"}), 400
|
||||||
|
|
||||||
|
include_albums = data.get('include_albums', True)
|
||||||
|
include_eps = data.get('include_eps', True)
|
||||||
|
include_singles = data.get('include_singles', True)
|
||||||
|
|
||||||
|
# Validate at least one is selected
|
||||||
|
if not (include_albums or include_eps or include_singles):
|
||||||
|
return jsonify({"success": False, "error": "At least one release type must be selected"}), 400
|
||||||
|
|
||||||
|
# Update database
|
||||||
|
conn = sqlite3.connect(str(database.database_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE watchlist_artists
|
||||||
|
SET include_albums = ?, include_eps = ?, include_singles = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE spotify_artist_id = ?
|
||||||
|
""", (int(include_albums), int(include_eps), int(include_singles), artist_id))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"success": False, "error": "Artist not found in watchlist"}), 404
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✅ Updated watchlist config for artist {artist_id}: albums={include_albums}, eps={include_eps}, singles={include_singles}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "Artist configuration updated successfully",
|
||||||
|
"config": {
|
||||||
|
'include_albums': include_albums,
|
||||||
|
'include_eps': include_eps,
|
||||||
|
'include_singles': include_singles
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in watchlist artist config: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
def _update_similar_artists_worker():
|
def _update_similar_artists_worker():
|
||||||
"""Background worker to update similar artists for all watchlist artists"""
|
"""Background worker to update similar artists for all watchlist artists"""
|
||||||
global similar_artists_update_state
|
global similar_artists_update_state
|
||||||
|
|
|
||||||
|
|
@ -2790,6 +2790,73 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Watchlist Artist Config Modal -->
|
||||||
|
<div class="modal-overlay hidden" id="watchlist-artist-config-modal-overlay">
|
||||||
|
<div class="watchlist-artist-config-modal" id="watchlist-artist-config-modal">
|
||||||
|
<div class="watchlist-artist-config-header">
|
||||||
|
<div class="watchlist-artist-config-hero" id="watchlist-artist-config-hero">
|
||||||
|
<!-- Hero content will be dynamically populated -->
|
||||||
|
</div>
|
||||||
|
<span class="watchlist-artist-config-close" onclick="closeWatchlistArtistConfigModal()">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="watchlist-artist-config-content">
|
||||||
|
<div class="watchlist-artist-config-body">
|
||||||
|
<div class="config-section">
|
||||||
|
<h3 class="config-section-title">Download Preferences</h3>
|
||||||
|
<p class="config-section-subtitle">Select which types of releases to monitor for this artist</p>
|
||||||
|
|
||||||
|
<div class="config-options">
|
||||||
|
<label class="config-option">
|
||||||
|
<input type="checkbox" id="config-include-albums" checked>
|
||||||
|
<div class="config-option-content">
|
||||||
|
<div class="config-option-icon">💿</div>
|
||||||
|
<div class="config-option-text">
|
||||||
|
<span class="config-option-title">Albums</span>
|
||||||
|
<span class="config-option-description">Full-length studio albums</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="config-option">
|
||||||
|
<input type="checkbox" id="config-include-eps" checked>
|
||||||
|
<div class="config-option-content">
|
||||||
|
<div class="config-option-icon">🎵</div>
|
||||||
|
<div class="config-option-text">
|
||||||
|
<span class="config-option-title">EPs</span>
|
||||||
|
<span class="config-option-description">Extended plays (4-6 tracks)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="config-option">
|
||||||
|
<input type="checkbox" id="config-include-singles" checked>
|
||||||
|
<div class="config-option-content">
|
||||||
|
<div class="config-option-icon">🎶</div>
|
||||||
|
<div class="config-option-text">
|
||||||
|
<span class="config-option-title">Singles</span>
|
||||||
|
<span class="config-option-description">Single tracks and 2-3 track releases</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="watchlist-artist-config-footer">
|
||||||
|
<div class="config-modal-actions">
|
||||||
|
<button class="config-modal-btn config-modal-btn-secondary" onclick="closeWatchlistArtistConfigModal()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="config-modal-btn config-modal-btn-primary" id="save-artist-config-btn">
|
||||||
|
Save Preferences
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Genre Browser Modal -->
|
<!-- Genre Browser Modal -->
|
||||||
<div class="genre-browser-modal-overlay" id="genre-browser-modal">
|
<div class="genre-browser-modal-overlay" id="genre-browser-modal">
|
||||||
<div class="genre-browser-modal-container">
|
<div class="genre-browser-modal-container">
|
||||||
|
|
|
||||||
|
|
@ -19822,7 +19822,10 @@ async function showWatchlistModal() {
|
||||||
|
|
||||||
<div class="watchlist-artists-list" id="watchlist-artists-list">
|
<div class="watchlist-artists-list" id="watchlist-artists-list">
|
||||||
${artistsData.artists.map(artist => `
|
${artistsData.artists.map(artist => `
|
||||||
<div class="watchlist-artist-item" data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '"')}">
|
<div class="watchlist-artist-item"
|
||||||
|
data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '"')}"
|
||||||
|
data-artist-id="${artist.spotify_artist_id}"
|
||||||
|
style="cursor: pointer;">
|
||||||
${artist.image_url ? `
|
${artist.image_url ? `
|
||||||
<img src="${artist.image_url}"
|
<img src="${artist.image_url}"
|
||||||
alt="${escapeHtml(artist.artist_name)}"
|
alt="${escapeHtml(artist.artist_name)}"
|
||||||
|
|
@ -19842,7 +19845,8 @@ async function showWatchlistModal() {
|
||||||
</div>
|
</div>
|
||||||
<button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-remove-btn"
|
<button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-remove-btn"
|
||||||
data-artist-id="${artist.spotify_artist_id}"
|
data-artist-id="${artist.spotify_artist_id}"
|
||||||
data-artist-name="${escapeHtml(artist.artist_name)}">
|
data-artist-name="${escapeHtml(artist.artist_name)}"
|
||||||
|
onclick="event.stopPropagation();">
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19867,6 +19871,22 @@ async function showWatchlistModal() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add click handlers to artist items (except for remove button)
|
||||||
|
modal.querySelectorAll('.watchlist-artist-item').forEach(item => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
// Don't trigger if clicking the remove button
|
||||||
|
if (e.target.closest('.watchlist-remove-btn')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const artistId = item.getAttribute('data-artist-id');
|
||||||
|
const artistName = item.querySelector('.watchlist-artist-name').textContent;
|
||||||
|
|
||||||
|
console.log(`🎵 Artist card clicked: ${artistName} (${artistId})`);
|
||||||
|
openWatchlistArtistConfigModal(artistId, artistName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Show modal
|
// Show modal
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
|
@ -19890,6 +19910,184 @@ function closeWatchlistModal() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open watchlist artist configuration modal
|
||||||
|
* @param {string} artistId - Spotify artist ID
|
||||||
|
* @param {string} artistName - Artist name
|
||||||
|
*/
|
||||||
|
async function openWatchlistArtistConfigModal(artistId, artistName) {
|
||||||
|
try {
|
||||||
|
console.log(`🎨 Opening config modal for artist: ${artistName} (${artistId})`);
|
||||||
|
|
||||||
|
// Fetch artist config and info
|
||||||
|
const response = await fetch(`/api/watchlist/artist/${artistId}/config`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
console.error('Error loading artist config:', data.error);
|
||||||
|
showToast(`Error loading artist configuration: ${data.error}`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config, artist } = data;
|
||||||
|
|
||||||
|
// Generate hero section
|
||||||
|
const heroHTML = `
|
||||||
|
${artist.image_url ? `
|
||||||
|
<img src="${artist.image_url}"
|
||||||
|
alt="${escapeHtml(artist.name)}"
|
||||||
|
class="watchlist-artist-config-hero-image">
|
||||||
|
` : ''}
|
||||||
|
<div class="watchlist-artist-config-hero-info">
|
||||||
|
<h2 class="watchlist-artist-config-hero-name">${escapeHtml(artist.name)}</h2>
|
||||||
|
<div class="watchlist-artist-config-hero-stats">
|
||||||
|
<div class="watchlist-artist-config-stat">
|
||||||
|
<span class="watchlist-artist-config-stat-value">${formatNumber(artist.followers)}</span>
|
||||||
|
<span class="watchlist-artist-config-stat-label">Followers</span>
|
||||||
|
</div>
|
||||||
|
<div class="watchlist-artist-config-stat">
|
||||||
|
<span class="watchlist-artist-config-stat-value">${artist.popularity}/100</span>
|
||||||
|
<span class="watchlist-artist-config-stat-label">Popularity</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${artist.genres && artist.genres.length > 0 ? `
|
||||||
|
<div class="watchlist-artist-config-hero-genres">
|
||||||
|
${artist.genres.slice(0, 3).map(genre =>
|
||||||
|
`<span class="watchlist-artist-config-genre-tag">${escapeHtml(genre)}</span>`
|
||||||
|
).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Populate hero section
|
||||||
|
const heroContainer = document.getElementById('watchlist-artist-config-hero');
|
||||||
|
if (heroContainer) {
|
||||||
|
heroContainer.innerHTML = heroHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set checkbox states
|
||||||
|
document.getElementById('config-include-albums').checked = config.include_albums;
|
||||||
|
document.getElementById('config-include-eps').checked = config.include_eps;
|
||||||
|
document.getElementById('config-include-singles').checked = config.include_singles;
|
||||||
|
|
||||||
|
// Store artist ID for saving
|
||||||
|
const modal = document.getElementById('watchlist-artist-config-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.setAttribute('data-artist-id', artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
const overlay = document.getElementById('watchlist-artist-config-modal-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add save button handler
|
||||||
|
const saveBtn = document.getElementById('save-artist-config-btn');
|
||||||
|
if (saveBtn) {
|
||||||
|
// Remove old listeners
|
||||||
|
const newSaveBtn = saveBtn.cloneNode(true);
|
||||||
|
saveBtn.parentNode.replaceChild(newSaveBtn, saveBtn);
|
||||||
|
|
||||||
|
// Add new listener
|
||||||
|
newSaveBtn.addEventListener('click', () => saveWatchlistArtistConfig(artistId));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening watchlist artist config modal:', error);
|
||||||
|
showToast(`Error: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close watchlist artist configuration modal
|
||||||
|
*/
|
||||||
|
function closeWatchlistArtistConfigModal() {
|
||||||
|
const overlay = document.getElementById('watchlist-artist-config-modal-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear hero content
|
||||||
|
const heroContainer = document.getElementById('watchlist-artist-config-hero');
|
||||||
|
if (heroContainer) {
|
||||||
|
heroContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save watchlist artist configuration
|
||||||
|
* @param {string} artistId - Spotify artist ID
|
||||||
|
*/
|
||||||
|
async function saveWatchlistArtistConfig(artistId) {
|
||||||
|
try {
|
||||||
|
const includeAlbums = document.getElementById('config-include-albums').checked;
|
||||||
|
const includeEps = document.getElementById('config-include-eps').checked;
|
||||||
|
const includeSingles = document.getElementById('config-include-singles').checked;
|
||||||
|
|
||||||
|
// Validate at least one is selected
|
||||||
|
if (!includeAlbums && !includeEps && !includeSingles) {
|
||||||
|
showToast('Please select at least one release type', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable save button
|
||||||
|
const saveBtn = document.getElementById('save-artist-config-btn');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.textContent = 'Saving...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send update to backend
|
||||||
|
const response = await fetch(`/api/watchlist/artist/${artistId}/config`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
include_albums: includeAlbums,
|
||||||
|
include_eps: includeEps,
|
||||||
|
include_singles: includeSingles
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showToast('Artist preferences saved successfully', 'success');
|
||||||
|
closeWatchlistArtistConfigModal();
|
||||||
|
|
||||||
|
// Refresh watchlist modal if it's open
|
||||||
|
const watchlistModal = document.getElementById('watchlist-modal');
|
||||||
|
if (watchlistModal && watchlistModal.style.display === 'flex') {
|
||||||
|
await showWatchlistModal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(`Error saving preferences: ${data.error}`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving watchlist artist config:', error);
|
||||||
|
showToast(`Error: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
// Re-enable save button
|
||||||
|
const saveBtn = document.getElementById('save-artist-config-btn');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.textContent = 'Save Preferences';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format large numbers with commas
|
||||||
|
* @param {number} num - Number to format
|
||||||
|
* @returns {string} Formatted number
|
||||||
|
*/
|
||||||
|
function formatNumber(num) {
|
||||||
|
if (!num) return '0';
|
||||||
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter watchlist artists based on search input
|
* Filter watchlist artists based on search input
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7960,6 +7960,313 @@ body {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== WATCHLIST ARTIST CONFIG MODAL STYLES ===== */
|
||||||
|
|
||||||
|
#watchlist-artist-config-modal-overlay {
|
||||||
|
z-index: 11000; /* Higher than default modal-overlay to appear on top of watchlist modal */
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-modal {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(26, 26, 26, 0.98) 0%,
|
||||||
|
rgba(18, 18, 18, 0.98) 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
|
||||||
|
max-width: 580px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-header {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(180deg,
|
||||||
|
rgba(29, 185, 84, 0.15) 0%,
|
||||||
|
rgba(29, 185, 84, 0.05) 50%,
|
||||||
|
transparent 100%);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero {
|
||||||
|
padding: 32px 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid rgba(29, 185, 84, 0.3);
|
||||||
|
box-shadow: 0 8px 24px rgba(29, 185, 84, 0.2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(29, 185, 84, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero-genres {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-genre-tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
border: 1px solid rgba(29, 185, 84, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(29, 185, 84, 1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-close:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(255, 59, 48, 0.2);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-body {
|
||||||
|
padding: 28px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(29, 185, 84, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option input[type="checkbox"] {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #1db954;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option input[type="checkbox"]:checked + .config-option-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option input[type="checkbox"]:not(:checked) + .config-option-content {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-footer {
|
||||||
|
padding: 20px 28px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn {
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #1ed760, #1db954);
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 4px 16px rgba(29, 185, 84, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #1fdf64, #1ed760);
|
||||||
|
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn-primary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn-primary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn-secondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.watchlist-artist-config-modal {
|
||||||
|
width: 95%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-config-hero-stats {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-actions {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Add these styles to the end of style.css */
|
/* Add these styles to the end of style.css */
|
||||||
|
|
||||||
.sync-progress-indicator {
|
.sync-progress-indicator {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue