improved ui for watching
This commit is contained in:
@@ -10,7 +10,7 @@ click==8.2.1
|
||||
click-didyoumean==0.3.1
|
||||
click-plugins==1.1.1
|
||||
click-repl==0.3.0
|
||||
deezspot @ git+https://github.com/Xoconoch/deezspot-fork-again@0b906e94e774cdeb8557a14a53236c0834fb1a2e
|
||||
deezspot @ git+https://github.com/Xoconoch/deezspot-fork-again
|
||||
defusedxml==0.7.1
|
||||
fastapi==0.115.12
|
||||
Flask==3.1.1
|
||||
|
||||
@@ -123,7 +123,6 @@ def get_artist_info():
|
||||
)
|
||||
|
||||
try:
|
||||
from routes.utils.get_info import get_spotify_info
|
||||
artist_info = get_spotify_info(spotify_id, "artist_discography")
|
||||
|
||||
# If artist_info is successfully fetched (it contains album items),
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
# Setup Redis and Celery
|
||||
from routes.utils.celery_config import REDIS_URL, REDIS_BACKEND, REDIS_PASSWORD, get_config_params
|
||||
# Import for playlist watch DB update
|
||||
from routes.utils.watch.db import add_single_track_to_playlist_db
|
||||
from routes.utils.watch.db import add_single_track_to_playlist_db, add_or_update_album_for_artist
|
||||
|
||||
# Import history manager function
|
||||
from .history_manager import add_entry_to_history
|
||||
@@ -840,6 +840,41 @@ class ProgressTrackingTask(Task):
|
||||
countdown=30 # Delay in seconds
|
||||
)
|
||||
|
||||
# If from playlist_watch and successful, add track to DB
|
||||
original_request = task_info.get("original_request", {})
|
||||
if original_request.get("source") == "playlist_watch" and task_info.get("download_type") == "track": # ensure it's a track for playlist
|
||||
playlist_id = original_request.get("playlist_id")
|
||||
track_item_for_db = original_request.get("track_item_for_db")
|
||||
|
||||
if playlist_id and track_item_for_db and track_item_for_db.get('track'):
|
||||
logger.info(f"Task {task_id} was from playlist watch for playlist {playlist_id}. Adding track to DB.")
|
||||
try:
|
||||
add_single_track_to_playlist_db(playlist_id, track_item_for_db)
|
||||
except Exception as db_add_err:
|
||||
logger.error(f"Failed to add track to DB for playlist {playlist_id} after successful download task {task_id}: {db_add_err}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"Task {task_id} was from playlist_watch but missing playlist_id or track_item_for_db for DB update. Original Request: {original_request}")
|
||||
|
||||
# If from artist_watch and successful, update album in DB
|
||||
if original_request.get("source") == "artist_watch" and task_info.get("download_type") == "album":
|
||||
artist_spotify_id = original_request.get("artist_spotify_id")
|
||||
album_data_for_db = original_request.get("album_data_for_db")
|
||||
|
||||
if artist_spotify_id and album_data_for_db and album_data_for_db.get("id"):
|
||||
album_spotify_id = album_data_for_db.get("id")
|
||||
logger.info(f"Task {task_id} was from artist watch for artist {artist_spotify_id}, album {album_spotify_id}. Updating album in DB as complete.")
|
||||
try:
|
||||
add_or_update_album_for_artist(
|
||||
artist_spotify_id=artist_spotify_id,
|
||||
album_data=album_data_for_db,
|
||||
task_id=task_id,
|
||||
is_download_complete=True
|
||||
)
|
||||
except Exception as db_update_err:
|
||||
logger.error(f"Failed to update album {album_spotify_id} in DB for artist {artist_spotify_id} after successful download task {task_id}: {db_update_err}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"Task {task_id} was from artist_watch (album) but missing key data (artist_spotify_id or album_data_for_db) for DB update. Original Request: {original_request}")
|
||||
|
||||
else:
|
||||
# Generic done for other types
|
||||
logger.info(f"Task {task_id} completed: {content_type.upper()}")
|
||||
@@ -870,21 +905,16 @@ def task_prerun_handler(task_id=None, task=None, *args, **kwargs):
|
||||
def task_postrun_handler(task_id=None, task=None, retval=None, state=None, *args, **kwargs):
|
||||
"""Signal handler when a task finishes"""
|
||||
try:
|
||||
# Skip if task is already marked as complete or error in Redis for history logging purposes
|
||||
last_status_for_history = get_last_task_status(task_id)
|
||||
if last_status_for_history and last_status_for_history.get("status") in [ProgressState.COMPLETE, ProgressState.ERROR, ProgressState.CANCELLED, "ERROR_RETRIED", "ERROR_AUTO_CLEANED"]:
|
||||
# Check if it was a REVOKED (cancelled) task, if so, ensure it's logged.
|
||||
if state == states.REVOKED and last_status_for_history.get("status") != ProgressState.CANCELLED:
|
||||
logger.info(f"Task {task_id} was REVOKED (likely cancelled), logging to history.")
|
||||
_log_task_to_history(task_id, 'CANCELLED', "Task was revoked/cancelled.")
|
||||
# else:
|
||||
# logger.debug(f"History: Task {task_id} already in terminal state {last_status_for_history.get('status')} in Redis. History logging likely handled.")
|
||||
# return # Do not return here, let the normal status update proceed for Redis if necessary
|
||||
# return # Let status update proceed if necessary
|
||||
|
||||
task_info = get_task_info(task_id)
|
||||
current_redis_status = last_status_for_history.get("status") if last_status_for_history else None
|
||||
|
||||
# Update task status based on Celery task state
|
||||
if state == states.SUCCESS:
|
||||
if current_redis_status != ProgressState.COMPLETE:
|
||||
store_task_status(task_id, {
|
||||
@@ -898,16 +928,15 @@ def task_postrun_handler(task_id=None, task=None, retval=None, state=None, *args
|
||||
logger.info(f"Task {task_id} completed successfully: {task_info.get('name', 'Unknown')}")
|
||||
_log_task_to_history(task_id, 'COMPLETED')
|
||||
|
||||
# If the task was a single track, schedule its data for deletion after a delay
|
||||
if task_info.get("download_type") == "track":
|
||||
if task_info.get("download_type") == "track": # Applies to single track downloads and tracks from playlists/albums
|
||||
delayed_delete_task_data.apply_async(
|
||||
args=[task_id, "Task completed successfully and auto-cleaned."],
|
||||
countdown=30 # Delay in seconds
|
||||
countdown=30
|
||||
)
|
||||
|
||||
# If from playlist_watch and successful, add track to DB
|
||||
original_request = task_info.get("original_request", {})
|
||||
if original_request.get("source") == "playlist_watch":
|
||||
# Handle successful track from playlist watch
|
||||
if original_request.get("source") == "playlist_watch" and task_info.get("download_type") == "track":
|
||||
playlist_id = original_request.get("playlist_id")
|
||||
track_item_for_db = original_request.get("track_item_for_db")
|
||||
|
||||
@@ -919,9 +948,29 @@ def task_postrun_handler(task_id=None, task=None, retval=None, state=None, *args
|
||||
logger.error(f"Failed to add track to DB for playlist {playlist_id} after successful download task {task_id}: {db_add_err}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"Task {task_id} was from playlist_watch but missing playlist_id or track_item_for_db for DB update. Original Request: {original_request}")
|
||||
|
||||
# Handle successful album from artist watch
|
||||
if original_request.get("source") == "artist_watch" and task_info.get("download_type") == "album":
|
||||
artist_spotify_id = original_request.get("artist_spotify_id")
|
||||
album_data_for_db = original_request.get("album_data_for_db")
|
||||
|
||||
if artist_spotify_id and album_data_for_db and album_data_for_db.get("id"):
|
||||
album_spotify_id = album_data_for_db.get("id")
|
||||
logger.info(f"Task {task_id} was from artist watch for artist {artist_spotify_id}, album {album_spotify_id}. Updating album in DB as complete.")
|
||||
try:
|
||||
add_or_update_album_for_artist(
|
||||
artist_spotify_id=artist_spotify_id,
|
||||
album_data=album_data_for_db,
|
||||
task_id=task_id,
|
||||
is_download_complete=True
|
||||
)
|
||||
except Exception as db_update_err:
|
||||
logger.error(f"Failed to update album {album_spotify_id} in DB for artist {artist_spotify_id} after successful download task {task_id}: {db_update_err}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"Task {task_id} was from artist_watch (album) but missing key data (artist_spotify_id or album_data_for_db) for DB update. Original Request: {original_request}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in task_postrun_handler: {e}")
|
||||
logger.error(f"Error in task_postrun_handler: {e}", exc_info=True)
|
||||
|
||||
@task_failure.connect
|
||||
def task_failure_handler(task_id=None, exception=None, traceback=None, *args, **kwargs):
|
||||
|
||||
@@ -179,25 +179,31 @@ def get_playlist_track_ids_from_db(playlist_spotify_id: str):
|
||||
return track_ids
|
||||
|
||||
def add_tracks_to_playlist_db(playlist_spotify_id: str, tracks_data: list):
|
||||
"""Adds or updates a list of tracks in the specified playlist's tracks table in playlists.db."""
|
||||
"""
|
||||
Updates existing tracks in the playlist's DB table to mark them as currently present
|
||||
in Spotify and updates their last_seen timestamp. Also refreshes metadata.
|
||||
Does NOT insert new tracks. New tracks are only added upon successful download.
|
||||
"""
|
||||
table_name = f"playlist_{playlist_spotify_id.replace('-', '_')}"
|
||||
if not tracks_data:
|
||||
return
|
||||
|
||||
current_time = int(time.time())
|
||||
tracks_to_insert = []
|
||||
tracks_to_update = []
|
||||
for track_item in tracks_data:
|
||||
track = track_item.get('track')
|
||||
if not track or not track.get('id'):
|
||||
logger.warning(f"Skipping track due to missing data or ID in playlist {playlist_spotify_id}: {track_item}")
|
||||
logger.warning(f"Skipping track update due to missing data or ID in playlist {playlist_spotify_id}: {track_item}")
|
||||
continue
|
||||
|
||||
# Ensure 'artists' and 'album' -> 'artists' are lists and extract names
|
||||
artist_names = ", ".join([artist['name'] for artist in track.get('artists', []) if artist.get('name')])
|
||||
album_artist_names = ", ".join([artist['name'] for artist in track.get('album', {}).get('artists', []) if artist.get('name')])
|
||||
|
||||
tracks_to_insert.append((
|
||||
track['id'],
|
||||
# Prepare tuple for UPDATE statement.
|
||||
# Order: title, artist_names, album_name, album_artist_names, track_number,
|
||||
# album_spotify_id, duration_ms, added_at_playlist,
|
||||
# is_present_in_spotify, last_seen_in_spotify, spotify_track_id (for WHERE)
|
||||
tracks_to_update.append((
|
||||
track.get('name', 'N/A'),
|
||||
artist_names,
|
||||
track.get('album', {}).get('name', 'N/A'),
|
||||
@@ -205,30 +211,44 @@ def add_tracks_to_playlist_db(playlist_spotify_id: str, tracks_data: list):
|
||||
track.get('track_number'),
|
||||
track.get('album', {}).get('id'),
|
||||
track.get('duration_ms'),
|
||||
track_item.get('added_at'), # From playlist item
|
||||
current_time, # added_to_db
|
||||
1, # is_present_in_spotify
|
||||
current_time # last_seen_in_spotify
|
||||
track_item.get('added_at'), # From playlist item, update if changed
|
||||
1, # is_present_in_spotify flag
|
||||
current_time, # last_seen_in_spotify timestamp
|
||||
# added_to_db is NOT updated here as this function only updates existing records.
|
||||
track['id'] # spotify_track_id for the WHERE clause
|
||||
))
|
||||
|
||||
if not tracks_to_insert:
|
||||
logger.info(f"No valid tracks to insert for playlist {playlist_spotify_id}.")
|
||||
if not tracks_to_update:
|
||||
logger.info(f"No valid tracks to prepare for update for playlist {playlist_spotify_id}.")
|
||||
return
|
||||
|
||||
try:
|
||||
with _get_playlists_db_connection() as conn: # Use playlists connection
|
||||
cursor = conn.cursor()
|
||||
_create_playlist_tracks_table(playlist_spotify_id) # Ensure table exists
|
||||
# The table should have been created when the playlist was added to watch
|
||||
# or when the first track was successfully downloaded.
|
||||
# _create_playlist_tracks_table(playlist_spotify_id) # Not strictly needed here if table creation is robust elsewhere.
|
||||
|
||||
# The fields in SET must match the order of ?s, excluding the last one for WHERE.
|
||||
# This will only update rows where spotify_track_id matches.
|
||||
cursor.executemany(f"""
|
||||
INSERT OR REPLACE INTO {table_name}
|
||||
(spotify_track_id, title, artist_names, album_name, album_artist_names, track_number, album_spotify_id, duration_ms, added_at_playlist, added_to_db, is_present_in_spotify, last_seen_in_spotify)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", tracks_to_insert)
|
||||
UPDATE {table_name} SET
|
||||
title = ?,
|
||||
artist_names = ?,
|
||||
album_name = ?,
|
||||
album_artist_names = ?,
|
||||
track_number = ?,
|
||||
album_spotify_id = ?,
|
||||
duration_ms = ?,
|
||||
added_at_playlist = ?,
|
||||
is_present_in_spotify = ?,
|
||||
last_seen_in_spotify = ?
|
||||
WHERE spotify_track_id = ?
|
||||
""", tracks_to_update)
|
||||
conn.commit()
|
||||
logger.info(f"Added/updated {len(tracks_to_insert)} tracks in DB for playlist {playlist_spotify_id} in {PLAYLISTS_DB_PATH}.")
|
||||
logger.info(f"Attempted to update metadata for {len(tracks_to_update)} tracks from API in DB for playlist {playlist_spotify_id}. Actual rows updated: {cursor.rowcount if cursor.rowcount != -1 else 'unknown'}.")
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Error adding tracks to playlist {playlist_spotify_id} in table {table_name} in {PLAYLISTS_DB_PATH}: {e}", exc_info=True)
|
||||
logger.error(f"Error updating tracks in playlist {playlist_spotify_id} in table {table_name} in {PLAYLISTS_DB_PATH}: {e}", exc_info=True)
|
||||
# Not raising here to allow other operations to continue if one batch fails.
|
||||
|
||||
def mark_tracks_as_not_present_in_spotify(playlist_spotify_id: str, track_ids_to_mark: list):
|
||||
|
||||
@@ -322,16 +322,14 @@ def check_watched_artists(specific_artist_id: str = None):
|
||||
task_id_or_none = download_queue_manager.add_task(task_payload, from_watch_job=True)
|
||||
|
||||
if task_id_or_none: # Task was newly queued
|
||||
add_or_update_album_for_artist(artist_spotify_id, album_data, task_id=task_id_or_none, is_download_complete=False)
|
||||
logger.info(f"Artist Watch Manager: Queued download task {task_id_or_none} for new album '{album_name}' from artist '{artist_name}'.")
|
||||
# REMOVED: add_or_update_album_for_artist(artist_spotify_id, album_data, task_id=task_id_or_none, is_download_complete=False)
|
||||
# The album will be added/updated in the DB by celery_tasks.py upon successful download completion.
|
||||
logger.info(f"Artist Watch Manager: Queued download task {task_id_or_none} for new album '{album_name}' from artist '{artist_name}'. DB entry will be created/updated on success.")
|
||||
queued_for_download_count += 1
|
||||
# If task_id_or_none is None, it was a duplicate. We can still log/record album_data if needed, but without task_id or as already seen.
|
||||
# add_or_update_album_for_artist(artist_spotify_id, album_data, task_id=None) # This would just log metadata if not a duplicate.
|
||||
# The current add_task logic in celery_manager might create an error task for duplicates,
|
||||
# so we might not need to do anything special here for duplicates apart from not incrementing count.
|
||||
# If task_id_or_none is None, it was a duplicate. Celery manager handles logging.
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Artist Watch Manager: Failed to queue/record download for new album {album_id} ('{album_name}') from artist '{artist_name}': {e}", exc_info=True)
|
||||
logger.error(f"Artist Watch Manager: Failed to queue download for new album {album_id} ('{album_name}') from artist '{artist_name}': {e}", exc_info=True)
|
||||
else:
|
||||
logger.info(f"Artist Watch Manager: Album '{album_name}' ({album_id}) by '{artist_name}' already known in DB (ID found in db_album_ids). Skipping queue.")
|
||||
# Optionally, update its entry (e.g. last_seen, or if details changed), but for now, we only queue new ones.
|
||||
|
||||
386
src/js/artist.ts
386
src/js/artist.ts
@@ -76,13 +76,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// This is done inside renderArtist after button element is potentially created.
|
||||
});
|
||||
|
||||
function renderArtist(artistData: ArtistData, artistId: string) {
|
||||
async function renderArtist(artistData: ArtistData, artistId: string) {
|
||||
const loadingEl = document.getElementById('loading');
|
||||
if (loadingEl) loadingEl.classList.add('hidden');
|
||||
|
||||
const errorEl = document.getElementById('error');
|
||||
if (errorEl) errorEl.classList.add('hidden');
|
||||
|
||||
// Fetch watch status upfront to avoid race conditions for album button rendering
|
||||
const isArtistActuallyWatched = await getArtistWatchStatus(artistId);
|
||||
|
||||
// Check if explicit filter is enabled
|
||||
const isExplicitFilterEnabled = downloadQueue.isExplicitFilterEnabled();
|
||||
|
||||
@@ -107,7 +110,7 @@ function renderArtist(artistData: ArtistData, artistId: string) {
|
||||
// Initialize Watch Button after other elements are rendered
|
||||
const watchArtistBtn = document.getElementById('watchArtistBtn') as HTMLButtonElement | null;
|
||||
if (watchArtistBtn) {
|
||||
initializeWatchButton(artistId);
|
||||
initializeWatchButton(artistId, isArtistActuallyWatched);
|
||||
} else {
|
||||
console.warn("Watch artist button not found in HTML.");
|
||||
}
|
||||
@@ -202,8 +205,9 @@ function renderArtist(artistData: ArtistData, artistId: string) {
|
||||
if (groupsContainer) {
|
||||
groupsContainer.innerHTML = '';
|
||||
|
||||
// Determine if the artist is being watched to show/hide management buttons for albums
|
||||
const isArtistWatched = watchArtistBtn && watchArtistBtn.dataset.watching === 'true';
|
||||
// Use the definitively fetched watch status for rendering album buttons
|
||||
// const isArtistWatched = watchArtistBtn && watchArtistBtn.dataset.watching === 'true'; // Old way
|
||||
const useThisWatchStatusForAlbums = isArtistActuallyWatched; // New way
|
||||
|
||||
for (const [groupType, albums] of Object.entries(albumGroups)) {
|
||||
const groupSection = document.createElement('section');
|
||||
@@ -230,58 +234,75 @@ function renderArtist(artistData: ArtistData, artistId: string) {
|
||||
if (!album) return;
|
||||
const albumElement = document.createElement('div');
|
||||
albumElement.className = 'album-card';
|
||||
albumElement.dataset.albumId = album.id;
|
||||
|
||||
let albumCardHTML = `
|
||||
<a href="/album/${album.id || ''}" class="album-link">
|
||||
<img src="${album.images?.[1]?.url || album.images?.[0]?.url || '/static/images/placeholder.jpg'}"
|
||||
alt="Album cover"
|
||||
class="album-cover">
|
||||
class="album-cover ${album.is_locally_known === false ? 'album-missing-in-db' : ''}">
|
||||
</a>
|
||||
<div class="album-info">
|
||||
<div class="album-title">${album.name || 'Unknown Album'}</div>
|
||||
<div class="album-artist">${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const actionsContainer = document.createElement('div');
|
||||
actionsContainer.className = 'album-actions-container';
|
||||
|
||||
if (!isExplicitFilterEnabled) {
|
||||
const downloadBtnHTML = `
|
||||
<button class="download-btn download-btn--circle album-download-btn"
|
||||
data-id="${album.id || ''}"
|
||||
data-type="${album.album_type || 'album'}"
|
||||
data-name="${album.name || 'Unknown Album'}"
|
||||
title="Download">
|
||||
<img src="/static/images/download.svg" alt="Download">
|
||||
</button>
|
||||
`;
|
||||
actionsContainer.innerHTML += downloadBtnHTML;
|
||||
}
|
||||
|
||||
if (isArtistWatched) {
|
||||
// Initial state is set based on album.is_locally_known
|
||||
const isKnown = album.is_locally_known === true;
|
||||
const initialStatus = isKnown ? "known" : "missing";
|
||||
const initialIcon = isKnown ? "/static/images/check.svg" : "/static/images/missing.svg";
|
||||
const initialTitle = isKnown ? "Click to mark as missing from DB" : "Click to mark as known in DB";
|
||||
|
||||
const toggleKnownBtnHTML = `
|
||||
<button class="action-btn toggle-known-status-btn"
|
||||
data-id="${album.id || ''}"
|
||||
data-artist-id="${artistId}"
|
||||
data-status="${initialStatus}"
|
||||
title="${initialTitle}">
|
||||
<img src="${initialIcon}" alt="Mark as Missing/Known">
|
||||
</button>
|
||||
`;
|
||||
actionsContainer.innerHTML += toggleKnownBtnHTML;
|
||||
}
|
||||
|
||||
albumElement.innerHTML = albumCardHTML;
|
||||
if (actionsContainer.hasChildNodes()) {
|
||||
albumElement.appendChild(actionsContainer);
|
||||
|
||||
const albumCardActions = document.createElement('div');
|
||||
albumCardActions.className = 'album-card-actions';
|
||||
|
||||
// Persistent Mark as Known/Missing button (if artist is watched) - Appears first (left)
|
||||
if (useThisWatchStatusForAlbums && album.id) {
|
||||
const toggleKnownBtn = document.createElement('button');
|
||||
toggleKnownBtn.className = 'toggle-known-status-btn persistent-album-action-btn';
|
||||
toggleKnownBtn.dataset.albumId = album.id;
|
||||
|
||||
if (album.is_locally_known) {
|
||||
toggleKnownBtn.dataset.status = 'known';
|
||||
toggleKnownBtn.innerHTML = '<img src="/static/images/check.svg" alt="Mark as missing">';
|
||||
toggleKnownBtn.title = 'Mark album as not in local library (Missing)';
|
||||
toggleKnownBtn.classList.add('status-known'); // Green
|
||||
} else {
|
||||
toggleKnownBtn.dataset.status = 'missing';
|
||||
toggleKnownBtn.innerHTML = '<img src="/static/images/missing.svg" alt="Mark as known">';
|
||||
toggleKnownBtn.title = 'Mark album as in local library (Known)';
|
||||
toggleKnownBtn.classList.add('status-missing'); // Red
|
||||
}
|
||||
albumCardActions.appendChild(toggleKnownBtn); // Add to actions container
|
||||
}
|
||||
|
||||
// Persistent Download Button (if not explicit filter) - Appears second (right)
|
||||
if (!isExplicitFilterEnabled) {
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.className = 'download-btn download-btn--circle persistent-download-btn';
|
||||
downloadBtn.innerHTML = '<img src="/static/images/download.svg" alt="Download album">';
|
||||
downloadBtn.title = 'Download this album';
|
||||
downloadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
downloadBtn.disabled = true;
|
||||
downloadBtn.innerHTML = '<img src="/static/images/refresh.svg" alt="Queueing..." class="icon-spin">';
|
||||
startDownload(album.id, 'album', { name: album.name, artist: album.artists?.[0]?.name || 'Unknown Artist', type: 'album' })
|
||||
.then(() => {
|
||||
downloadBtn.innerHTML = '<img src="/static/images/check.svg" alt="Queued">';
|
||||
showNotification(`Album '${album.name}' queued for download.`);
|
||||
downloadQueue.toggleVisibility(true);
|
||||
})
|
||||
.catch(err => {
|
||||
downloadBtn.disabled = false;
|
||||
downloadBtn.innerHTML = '<img src="/static/images/download.svg" alt="Download album">';
|
||||
showError(`Failed to queue album: ${err?.message || 'Unknown error'}`);
|
||||
});
|
||||
});
|
||||
albumCardActions.appendChild(downloadBtn); // Add to actions container
|
||||
}
|
||||
|
||||
// Only append albumCardActions if it has any buttons
|
||||
if (albumCardActions.hasChildNodes()) {
|
||||
albumElement.appendChild(albumCardActions);
|
||||
}
|
||||
|
||||
albumsListContainer.appendChild(albumElement);
|
||||
});
|
||||
groupSection.appendChild(albumsListContainer);
|
||||
@@ -311,56 +332,74 @@ function renderArtist(artistData: ArtistData, artistId: string) {
|
||||
if (!album) return;
|
||||
const albumElement = document.createElement('div');
|
||||
albumElement.className = 'album-card';
|
||||
albumElement.dataset.albumId = album.id; // Set dataset for appears_on albums too
|
||||
|
||||
let albumCardHTML = `
|
||||
<a href="/album/${album.id || ''}" class="album-link">
|
||||
<img src="${album.images?.[1]?.url || album.images?.[0]?.url || '/static/images/placeholder.jpg'}"
|
||||
alt="Album cover"
|
||||
class="album-cover">
|
||||
class="album-cover ${album.is_locally_known === false ? 'album-missing-in-db' : ''}">
|
||||
</a>
|
||||
<div class="album-info">
|
||||
<div class="album-title">${album.name || 'Unknown Album'}</div>
|
||||
<div class="album-artist">${album.artists?.map(a => a?.name || 'Unknown Artist').join(', ') || 'Unknown Artist'}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const actionsContainer = document.createElement('div');
|
||||
actionsContainer.className = 'album-actions-container';
|
||||
|
||||
if (!isExplicitFilterEnabled) {
|
||||
const downloadBtnHTML = `
|
||||
<button class="download-btn download-btn--circle album-download-btn"
|
||||
data-id="${album.id || ''}"
|
||||
data-type="${album.album_type || 'album'}"
|
||||
data-name="${album.name || 'Unknown Album'}"
|
||||
title="Download">
|
||||
<img src="/static/images/download.svg" alt="Download">
|
||||
</button>
|
||||
`;
|
||||
actionsContainer.innerHTML += downloadBtnHTML;
|
||||
}
|
||||
|
||||
if (isArtistWatched) {
|
||||
// Initial state is set based on album.is_locally_known
|
||||
const isKnown = album.is_locally_known === true;
|
||||
const initialStatus = isKnown ? "known" : "missing";
|
||||
const initialIcon = isKnown ? "/static/images/check.svg" : "/static/images/missing.svg";
|
||||
const initialTitle = isKnown ? "Click to mark as missing from DB" : "Click to mark as known in DB";
|
||||
|
||||
const toggleKnownBtnHTML = `
|
||||
<button class="action-btn toggle-known-status-btn"
|
||||
data-id="${album.id || ''}"
|
||||
data-artist-id="${artistId}"
|
||||
data-status="${initialStatus}"
|
||||
title="${initialTitle}">
|
||||
<img src="${initialIcon}" alt="Mark as Missing/Known">
|
||||
</button>
|
||||
`;
|
||||
actionsContainer.innerHTML += toggleKnownBtnHTML;
|
||||
}
|
||||
albumElement.innerHTML = albumCardHTML;
|
||||
if (actionsContainer.hasChildNodes()) {
|
||||
albumElement.appendChild(actionsContainer);
|
||||
|
||||
const albumCardActions_AppearsOn = document.createElement('div');
|
||||
albumCardActions_AppearsOn.className = 'album-card-actions';
|
||||
|
||||
// Persistent Mark as Known/Missing button for appearing_on albums (if artist is watched) - Appears first (left)
|
||||
if (useThisWatchStatusForAlbums && album.id) {
|
||||
const toggleKnownBtn = document.createElement('button');
|
||||
toggleKnownBtn.className = 'toggle-known-status-btn persistent-album-action-btn';
|
||||
toggleKnownBtn.dataset.albumId = album.id;
|
||||
if (album.is_locally_known) {
|
||||
toggleKnownBtn.dataset.status = 'known';
|
||||
toggleKnownBtn.innerHTML = '<img src="/static/images/check.svg" alt="Mark as missing">';
|
||||
toggleKnownBtn.title = 'Mark album as not in local library (Missing)';
|
||||
toggleKnownBtn.classList.add('status-known'); // Green
|
||||
} else {
|
||||
toggleKnownBtn.dataset.status = 'missing';
|
||||
toggleKnownBtn.innerHTML = '<img src="/static/images/missing.svg" alt="Mark as known">';
|
||||
toggleKnownBtn.title = 'Mark album as in local library (Known)';
|
||||
toggleKnownBtn.classList.add('status-missing'); // Red
|
||||
}
|
||||
albumCardActions_AppearsOn.appendChild(toggleKnownBtn); // Add to actions container
|
||||
}
|
||||
|
||||
// Persistent Download Button for appearing_on albums (if not explicit filter) - Appears second (right)
|
||||
if (!isExplicitFilterEnabled) {
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.className = 'download-btn download-btn--circle persistent-download-btn';
|
||||
downloadBtn.innerHTML = '<img src="/static/images/download.svg" alt="Download album">';
|
||||
downloadBtn.title = 'Download this album';
|
||||
downloadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
downloadBtn.disabled = true;
|
||||
downloadBtn.innerHTML = '<img src="/static/images/refresh.svg" alt="Queueing..." class="icon-spin">';
|
||||
startDownload(album.id, 'album', { name: album.name, artist: album.artists?.[0]?.name || 'Unknown Artist', type: 'album' })
|
||||
.then(() => {
|
||||
downloadBtn.innerHTML = '<img src="/static/images/check.svg" alt="Queued">';
|
||||
showNotification(`Album '${album.name}' queued for download.`);
|
||||
downloadQueue.toggleVisibility(true);
|
||||
})
|
||||
.catch(err => {
|
||||
downloadBtn.disabled = false;
|
||||
downloadBtn.innerHTML = '<img src="/static/images/download.svg" alt="Download album">';
|
||||
showError(`Failed to queue album: ${err?.message || 'Unknown error'}`);
|
||||
});
|
||||
});
|
||||
albumCardActions_AppearsOn.appendChild(downloadBtn); // Add to actions container
|
||||
}
|
||||
|
||||
// Only append albumCardActions_AppearsOn if it has any buttons
|
||||
if (albumCardActions_AppearsOn.hasChildNodes()) {
|
||||
albumElement.appendChild(albumCardActions_AppearsOn);
|
||||
}
|
||||
|
||||
appearingAlbumsListContainer.appendChild(albumElement);
|
||||
});
|
||||
featuringSection.appendChild(appearingAlbumsListContainer);
|
||||
@@ -410,100 +449,104 @@ function attachGroupDownloadListeners(artistId: string, artistName: string) {
|
||||
}
|
||||
|
||||
function attachAlbumActionListeners(artistIdForContext: string) {
|
||||
document.querySelectorAll('.album-download-btn').forEach(btn => {
|
||||
const button = btn as HTMLButtonElement;
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const currentTarget = e.currentTarget as HTMLButtonElement | null;
|
||||
if (!currentTarget) return;
|
||||
const itemId = currentTarget.dataset.id || '';
|
||||
const name = currentTarget.dataset.name || 'Unknown';
|
||||
const type = 'album';
|
||||
if (!itemId) {
|
||||
showError('Could not get album ID for download');
|
||||
return;
|
||||
}
|
||||
currentTarget.remove();
|
||||
downloadQueue.download(itemId, type, { name, type })
|
||||
.catch((err: any) => showError('Download failed: ' + (err?.message || 'Unknown error')));
|
||||
});
|
||||
});
|
||||
const groupsContainer = document.getElementById('album-groups');
|
||||
if (!groupsContainer) return;
|
||||
|
||||
document.querySelectorAll('.toggle-known-status-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
const albumId = button.dataset.id || '';
|
||||
const artistId = button.dataset.artistId || artistIdForContext;
|
||||
groupsContainer.addEventListener('click', async (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest('.toggle-known-status-btn') as HTMLButtonElement | null;
|
||||
|
||||
if (button && button.dataset.albumId) {
|
||||
const albumId = button.dataset.albumId;
|
||||
const currentStatus = button.dataset.status;
|
||||
const img = button.querySelector('img');
|
||||
|
||||
if (!albumId || !artistId || !img) {
|
||||
showError('Missing data for toggling album status');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Optimistic UI update
|
||||
button.disabled = true;
|
||||
const originalIcon = button.innerHTML; // Save original icon
|
||||
button.innerHTML = '<img src="/static/images/refresh.svg" alt="Updating..." class="icon-spin">';
|
||||
|
||||
try {
|
||||
if (currentStatus === 'missing') {
|
||||
await handleMarkAlbumAsKnown(artistId, albumId);
|
||||
button.dataset.status = 'known';
|
||||
img.src = '/static/images/check.svg';
|
||||
button.title = 'Click to mark as missing from DB';
|
||||
} else {
|
||||
await handleMarkAlbumAsMissing(artistId, albumId);
|
||||
if (currentStatus === 'known') {
|
||||
await handleMarkAlbumAsMissing(artistIdForContext, albumId);
|
||||
button.dataset.status = 'missing';
|
||||
img.src = '/static/images/missing.svg';
|
||||
button.title = 'Click to mark as known in DB';
|
||||
button.innerHTML = '<img src="/static/images/missing.svg" alt="Mark as known">'; // Update to missing.svg
|
||||
button.title = 'Mark album as in local library (Known)';
|
||||
button.classList.remove('status-known');
|
||||
button.classList.add('status-missing');
|
||||
const albumCard = button.closest('.album-card') as HTMLElement | null;
|
||||
if (albumCard) {
|
||||
const coverImg = albumCard.querySelector('.album-cover') as HTMLImageElement | null;
|
||||
if (coverImg) coverImg.classList.add('album-missing-in-db');
|
||||
}
|
||||
showNotification(`Album marked as missing from local library.`);
|
||||
} else {
|
||||
await handleMarkAlbumAsKnown(artistIdForContext, albumId);
|
||||
button.dataset.status = 'known';
|
||||
button.innerHTML = '<img src="/static/images/check.svg" alt="Mark as missing">'; // Update to check.svg
|
||||
button.title = 'Mark album as not in local library (Missing)';
|
||||
button.classList.remove('status-missing');
|
||||
button.classList.add('status-known');
|
||||
const albumCard = button.closest('.album-card') as HTMLElement | null;
|
||||
if (albumCard) {
|
||||
const coverImg = albumCard.querySelector('.album-cover') as HTMLImageElement | null;
|
||||
if (coverImg) coverImg.classList.remove('album-missing-in-db');
|
||||
}
|
||||
showNotification(`Album marked as present in local library.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update album status:', error);
|
||||
showError('Failed to update album status. Please try again.');
|
||||
// Revert UI on error
|
||||
button.dataset.status = currentStatus; // Revert status
|
||||
button.innerHTML = originalIcon; // Revert icon
|
||||
// Revert card style if needed (though if API failed, actual state is unchanged)
|
||||
} finally {
|
||||
button.disabled = false; // Re-enable button
|
||||
}
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleMarkAlbumAsKnown(artistId: string, albumId: string) {
|
||||
try {
|
||||
const response = await fetch(`/api/artist/watch/${artistId}/albums`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify([albumId]),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
showNotification(result.message || 'Album marked as known.');
|
||||
} catch (error: any) {
|
||||
showError(`Failed to mark album as known: ${error.message}`);
|
||||
throw error; // Re-throw for the caller to handle button state if needed
|
||||
// Ensure albumId is a string and not undefined.
|
||||
if (!albumId || typeof albumId !== 'string') {
|
||||
console.error('Invalid albumId provided to handleMarkAlbumAsKnown:', albumId);
|
||||
throw new Error('Invalid album ID.');
|
||||
}
|
||||
const response = await fetch(`/api/artist/watch/${artistId}/albums`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([albumId]) // API expects an array of album IDs
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to mark album as known.' }));
|
||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function handleMarkAlbumAsMissing(artistId: string, albumId: string) {
|
||||
try {
|
||||
const response = await fetch(`/api/artist/watch/${artistId}/albums`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify([albumId]),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
showNotification(result.message || 'Album marked as missing.');
|
||||
} catch (error: any) {
|
||||
showError(`Failed to mark album as missing: ${error.message}`);
|
||||
throw error; // Re-throw
|
||||
// Ensure albumId is a string and not undefined.
|
||||
if (!albumId || typeof albumId !== 'string') {
|
||||
console.error('Invalid albumId provided to handleMarkAlbumAsMissing:', albumId);
|
||||
throw new Error('Invalid album ID.');
|
||||
}
|
||||
const response = await fetch(`/api/artist/watch/${artistId}/albums`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([albumId]) // API expects an array of album IDs
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Failed to mark album as missing.' }));
|
||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// For DELETE, Spotify often returns 204 No Content, or we might return custom JSON.
|
||||
// If expecting JSON:
|
||||
// return response.json();
|
||||
// If handling 204 or simple success message:
|
||||
const result = await response.json(); // Assuming the backend sends a JSON response
|
||||
console.log('Mark as missing result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add startDownload function (similar to track.js and main.js)
|
||||
@@ -619,20 +662,20 @@ function updateWatchButton(artistId: string, isWatching: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeWatchButton(artistId: string) {
|
||||
async function initializeWatchButton(artistId: string, initialIsWatching: boolean) {
|
||||
const watchArtistBtn = document.getElementById('watchArtistBtn') as HTMLButtonElement | null;
|
||||
const syncArtistBtn = document.getElementById('syncArtistBtn') as HTMLButtonElement | null;
|
||||
|
||||
if (!watchArtistBtn) return;
|
||||
|
||||
try {
|
||||
watchArtistBtn.disabled = true; // Disable while fetching status
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = true; // Also disable sync button initially
|
||||
watchArtistBtn.disabled = true;
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = true;
|
||||
|
||||
const isWatching = await getArtistWatchStatus(artistId);
|
||||
updateWatchButton(artistId, isWatching);
|
||||
// const isWatching = await getArtistWatchStatus(artistId); // No longer fetch here, use parameter
|
||||
updateWatchButton(artistId, initialIsWatching); // Use passed status
|
||||
watchArtistBtn.disabled = false;
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = !(watchArtistBtn.dataset.watching === 'true'); // Corrected logic
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = !(watchArtistBtn.dataset.watching === 'true');
|
||||
|
||||
watchArtistBtn.addEventListener('click', async () => {
|
||||
const currentlyWatching = watchArtistBtn.dataset.watching === 'true';
|
||||
@@ -642,15 +685,22 @@ async function initializeWatchButton(artistId: string) {
|
||||
if (currentlyWatching) {
|
||||
await unwatchArtist(artistId);
|
||||
updateWatchButton(artistId, false);
|
||||
// Re-fetch and re-render artist data
|
||||
const newArtistData = await (await fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`)).json() as ArtistData;
|
||||
renderArtist(newArtistData, artistId);
|
||||
} else {
|
||||
await watchArtist(artistId);
|
||||
updateWatchButton(artistId, true);
|
||||
// Re-fetch and re-render artist data
|
||||
const newArtistData = await (await fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`)).json() as ArtistData;
|
||||
renderArtist(newArtistData, artistId);
|
||||
}
|
||||
} catch (error) {
|
||||
updateWatchButton(artistId, currentlyWatching);
|
||||
// On error, revert button to its state before the click attempt
|
||||
updateWatchButton(artistId, currentlyWatching);
|
||||
}
|
||||
watchArtistBtn.disabled = false;
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = !(watchArtistBtn.dataset.watching === 'true'); // Corrected logic
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = !(watchArtistBtn.dataset.watching === 'true');
|
||||
});
|
||||
|
||||
// Add event listener for the sync button
|
||||
@@ -675,8 +725,10 @@ async function initializeWatchButton(artistId: string) {
|
||||
|
||||
} catch (error) {
|
||||
if (watchArtistBtn) watchArtistBtn.disabled = false;
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = true; // Keep sync disabled on error
|
||||
updateWatchButton(artistId, false);
|
||||
if (syncArtistBtn) syncArtistBtn.disabled = true;
|
||||
updateWatchButton(artistId, false); // On error fetching initial status (though now it's passed)
|
||||
// This line might be less relevant if initialIsWatching is guaranteed by caller
|
||||
// but as a fallback it sets to a non-watching state.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -664,10 +664,16 @@ async function watchPlaylist(playlistId: string) {
|
||||
throw new Error(errorData.error || 'Failed to watch playlist');
|
||||
}
|
||||
updateWatchButtons(true, playlistId);
|
||||
showNotification(`Playlist added to watchlist. It will be synced shortly.`);
|
||||
// Re-fetch and re-render playlist data
|
||||
const newPlaylistInfoResponse = await fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`);
|
||||
if (!newPlaylistInfoResponse.ok) throw new Error('Failed to re-fetch playlist info after watch.');
|
||||
const newPlaylistData = await newPlaylistInfoResponse.json() as Playlist;
|
||||
renderPlaylist(newPlaylistData);
|
||||
|
||||
showNotification(`Playlist added to watchlist. Tracks are being updated.`);
|
||||
} catch (error: any) {
|
||||
showError(`Error watching playlist: ${error.message}`);
|
||||
if (watchBtn) watchBtn.disabled = false;
|
||||
if (watchBtn) watchBtn.disabled = false; // Re-enable on error before potential UI revert
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,10 +691,16 @@ async function unwatchPlaylist(playlistId: string) {
|
||||
throw new Error(errorData.error || 'Failed to unwatch playlist');
|
||||
}
|
||||
updateWatchButtons(false, playlistId);
|
||||
showNotification('Playlist removed from watchlist.');
|
||||
// Re-fetch and re-render playlist data
|
||||
const newPlaylistInfoResponse = await fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`);
|
||||
if (!newPlaylistInfoResponse.ok) throw new Error('Failed to re-fetch playlist info after unwatch.');
|
||||
const newPlaylistData = await newPlaylistInfoResponse.json() as Playlist;
|
||||
renderPlaylist(newPlaylistData);
|
||||
|
||||
showNotification('Playlist removed from watchlist. Track statuses updated.');
|
||||
} catch (error: any) {
|
||||
showError(`Error unwatching playlist: ${error.message}`);
|
||||
if (watchBtn) watchBtn.disabled = false;
|
||||
if (watchBtn) watchBtn.disabled = false; // Re-enable on error before potential UI revert
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -484,49 +484,164 @@ a:focus {
|
||||
|
||||
/* Toggle Known Status Button for Tracks/Albums */
|
||||
.toggle-known-status-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--color-text-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 5px;
|
||||
border-radius: 50%; /* Make it circular */
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
margin-left: 0.5rem; /* Spacing from other buttons if any */
|
||||
width: 30px; /* Fixed size */
|
||||
height: 30px; /* Fixed size */
|
||||
transition: background-color 0.2s, border-color 0.2s, color 0.2s, opacity 0.2s; /* Added opacity */
|
||||
/* opacity: 0; Initially hidden, JS will make it visible if artist is watched via persistent-album-action-btn */
|
||||
}
|
||||
|
||||
.toggle-known-status-btn img {
|
||||
width: 18px; /* Adjust icon size as needed */
|
||||
height: 18px;
|
||||
filter: brightness(0) invert(1); /* White icon */
|
||||
width: 16px; /* Adjust icon size */
|
||||
height: 16px;
|
||||
filter: brightness(0) invert(1); /* Make icon white consistently */
|
||||
margin: 0; /* Ensure no accidental margin for centering */
|
||||
}
|
||||
|
||||
.toggle-known-status-btn:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.toggle-known-status-btn[data-status="known"] {
|
||||
background-color: #28a745; /* Green for known/available */
|
||||
/* Optional: specific styles if it's already known, e.g., a slightly different border */
|
||||
border-color: var(--color-success); /* Green border for known items */
|
||||
}
|
||||
|
||||
.toggle-known-status-btn[data-status="known"]:hover {
|
||||
background-color: #218838; /* Darker green on hover */
|
||||
.toggle-known-status-btn[data-status="known"]:hover img {
|
||||
/* REMOVE THE LINE BELOW THIS COMMENT */
|
||||
/* filter: invert(20%) sepia(100%) saturate(500%) hue-rotate(330deg); Removed */
|
||||
}
|
||||
|
||||
.toggle-known-status-btn[data-status="missing"] {
|
||||
background-color: #dc3545; /* Red for missing */
|
||||
/* Optional: specific styles if it's missing, e.g., a warning color */
|
||||
border-color: var(--color-warning); /* Orange border for missing items */
|
||||
}
|
||||
|
||||
.toggle-known-status-btn[data-status="missing"]:hover {
|
||||
background-color: #c82333; /* Darker red on hover */
|
||||
.toggle-known-status-btn[data-status="missing"]:hover img {
|
||||
/* REMOVE THE LINE BELOW THIS COMMENT */
|
||||
/* filter: invert(60%) sepia(100%) saturate(500%) hue-rotate(80deg); Removed */
|
||||
}
|
||||
|
||||
.toggle-known-status-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.album-actions-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* If you want buttons at the bottom of the card or specific positioning, adjust here */
|
||||
/* For now, they will flow naturally. Adding padding if needed. */
|
||||
padding-top: 0.5rem;
|
||||
/* Ensure album download button also fits well within actions container */
|
||||
.album-actions-container .album-download-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 5px; /* Ensure padding doesn't make it too big */
|
||||
}
|
||||
|
||||
.album-actions-container .album-download-btn img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Album actions container */
|
||||
.album-actions-container {
|
||||
/* position: absolute; */ /* No longer needed if buttons are positioned individually */
|
||||
/* bottom: 8px; */
|
||||
/* right: 8px; */
|
||||
/* display: flex; */
|
||||
/* gap: 8px; */
|
||||
/* background-color: rgba(0, 0, 0, 0.6); */
|
||||
/* padding: 5px; */
|
||||
/* border-radius: var(--radius-sm); */
|
||||
/* opacity: 0; */ /* Ensure it doesn't hide buttons if it still wraps them elsewhere */
|
||||
/* transition: opacity 0.2s ease-in-out; */
|
||||
display: none; /* Hide this container if it solely relied on hover and now buttons are persistent */
|
||||
}
|
||||
|
||||
/* .album-card:hover .album-actions-container { */
|
||||
/* opacity: 1; */ /* Remove this hover effect */
|
||||
/* } */
|
||||
|
||||
/* Album card actions container - for persistent buttons at the bottom */
|
||||
.album-card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Pushes children to ends */
|
||||
align-items: center;
|
||||
padding: 8px; /* Spacing around the buttons */
|
||||
border-top: 1px solid var(--color-surface-darker, #2a2a2a); /* Separator line */
|
||||
/* Ensure it takes up full width of the card if not already */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Persistent action button (e.g., toggle known/missing) on album card - BOTTOM-LEFT */
|
||||
.persistent-album-action-btn {
|
||||
/* position: absolute; */ /* No longer absolute */
|
||||
/* bottom: 8px; */
|
||||
/* left: 8px; */
|
||||
/* z-index: 2; */
|
||||
opacity: 1; /* Ensure it is visible */
|
||||
/* Specific margin if needed, but flexbox space-between should handle it */
|
||||
margin: 0; /* Reset any previous margins */
|
||||
}
|
||||
|
||||
/* Persistent download button on album card - BOTTOM-RIGHT */
|
||||
.persistent-download-btn {
|
||||
/* position: absolute; */ /* No longer absolute */
|
||||
/* bottom: 8px; */
|
||||
/* right: 8px; */
|
||||
/* z-index: 2; */
|
||||
opacity: 1; /* Ensure it is visible */
|
||||
/* Specific margin if needed, but flexbox space-between should handle it */
|
||||
margin: 0; /* Reset any previous margins */
|
||||
}
|
||||
|
||||
.album-cover.album-missing-in-db {
|
||||
border: 3px dashed var(--color-warning); /* Example: orange dashed border */
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* NEW STYLES FOR BUTTON STATES */
|
||||
.persistent-album-action-btn.status-missing {
|
||||
background-color: #d9534f; /* Bootstrap's btn-danger red */
|
||||
border-color: #d43f3a;
|
||||
}
|
||||
|
||||
.persistent-album-action-btn.status-missing:hover {
|
||||
background-color: #c9302c;
|
||||
border-color: #ac2925;
|
||||
}
|
||||
|
||||
/* Ensure icon is white on colored background */
|
||||
.persistent-album-action-btn.status-missing img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.persistent-album-action-btn.status-known {
|
||||
background-color: #5cb85c; /* Bootstrap's btn-success green */
|
||||
border-color: #4cae4c;
|
||||
}
|
||||
|
||||
.persistent-album-action-btn.status-known:hover {
|
||||
background-color: #449d44;
|
||||
border-color: #398439;
|
||||
}
|
||||
|
||||
/* Ensure icon is white on colored background */
|
||||
.persistent-album-action-btn.status-known img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
/* END OF NEW STYLES */
|
||||
|
||||
/* Spinning Icon Animation */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(-360deg); }
|
||||
}
|
||||
|
||||
.icon-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@ -389,6 +389,7 @@ a:focus {
|
||||
height: 20px;
|
||||
filter: brightness(0) invert(1); /* Ensure the icon appears white */
|
||||
display: block;
|
||||
margin: 0; /* Explicitly remove any margin */
|
||||
}
|
||||
|
||||
/* Hover and active states for the circular download button */
|
||||
|
||||
@@ -172,11 +172,6 @@ body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.item-actions .check-item-now-btn,
|
||||
.item-actions .unwatch-item-btn {
|
||||
/* Shared properties are in .item-actions .btn-icon */
|
||||
}
|
||||
|
||||
.item-actions .check-item-now-btn {
|
||||
background-color: var(--color-accent-green);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user