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
|
||||
updated_at: Optional[datetime] = None
|
||||
image_url: Optional[str] = None
|
||||
include_albums: bool = True
|
||||
include_eps: bool = True
|
||||
include_singles: bool = True
|
||||
|
||||
@dataclass
|
||||
class SimilarArtist:
|
||||
|
|
@ -262,6 +265,9 @@ class MusicDatabase:
|
|||
# Add image_url column to watchlist_artists (migration)
|
||||
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()
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
|
|
@ -591,6 +597,27 @@ class MusicDatabase:
|
|||
logger.error(f"Error adding image_url column to watchlist_artists: {e}")
|
||||
# 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):
|
||||
"""Close database connection (no-op since we create connections per operation)"""
|
||||
# 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 uuid
|
||||
import re
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
|
||||
|
|
@ -14915,6 +14916,116 @@ def get_similar_artists_update_status():
|
|||
print(f"Error getting similar artists status: {e}")
|
||||
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():
|
||||
"""Background worker to update similar artists for all watchlist artists"""
|
||||
global similar_artists_update_state
|
||||
|
|
|
|||
|
|
@ -2790,6 +2790,73 @@
|
|||
</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 -->
|
||||
<div class="genre-browser-modal-overlay" id="genre-browser-modal">
|
||||
<div class="genre-browser-modal-container">
|
||||
|
|
|
|||
|
|
@ -19822,7 +19822,10 @@ async function showWatchlistModal() {
|
|||
|
||||
<div class="watchlist-artists-list" id="watchlist-artists-list">
|
||||
${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 ? `
|
||||
<img src="${artist.image_url}"
|
||||
alt="${escapeHtml(artist.artist_name)}"
|
||||
|
|
@ -19842,7 +19845,8 @@ async function showWatchlistModal() {
|
|||
</div>
|
||||
<button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-remove-btn"
|
||||
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
|
||||
</button>
|
||||
</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
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7960,6 +7960,313 @@ body {
|
|||
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 */
|
||||
|
||||
.sync-progress-indicator {
|
||||
|
|
|
|||
Loading…
Reference in a new issue