start of library page
This commit is contained in:
parent
180413677c
commit
9485dfb03b
4 changed files with 954 additions and 11 deletions
143
web_server.py
143
web_server.py
|
|
@ -2590,14 +2590,141 @@ def maintain_search_history():
|
||||||
print(f"Error maintaining search history: {e}")
|
print(f"Error maintaining search history: {e}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/artists')
|
def fix_artist_image_url(thumb_url):
|
||||||
def get_artists():
|
"""Convert localhost URLs to proper server URLs using config"""
|
||||||
# Placeholder: returns mock artist data
|
if not thumb_url:
|
||||||
mock_artists = [
|
return None
|
||||||
{"name": "Queen", "album_count": 15, "image": None},
|
|
||||||
{"name": "Led Zeppelin", "album_count": 9, "image": None}
|
try:
|
||||||
]
|
# Check if it's a localhost URL or relative path that needs fixing
|
||||||
return jsonify({"artists": mock_artists})
|
needs_fixing = (
|
||||||
|
thumb_url.startswith('http://localhost:') or
|
||||||
|
thumb_url.startswith('https://localhost:') or
|
||||||
|
thumb_url.startswith('/library/') # Plex relative paths
|
||||||
|
)
|
||||||
|
|
||||||
|
if needs_fixing:
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
print(f"🔧 Fixing URL: {thumb_url}, Active server: {active_server}")
|
||||||
|
|
||||||
|
if active_server == 'plex':
|
||||||
|
plex_config = config_manager.get_plex_config()
|
||||||
|
plex_base_url = plex_config.get('base_url', '')
|
||||||
|
plex_token = plex_config.get('token', '')
|
||||||
|
print(f"🔧 Plex config - base_url: {plex_base_url}, token: {plex_token[:10]}...")
|
||||||
|
|
||||||
|
if plex_base_url and plex_token:
|
||||||
|
# Extract the path from URL
|
||||||
|
if thumb_url.startswith('/library/'):
|
||||||
|
# Already a path
|
||||||
|
path = thumb_url
|
||||||
|
else:
|
||||||
|
# Full localhost URL, extract path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(thumb_url)
|
||||||
|
path = parsed.path
|
||||||
|
|
||||||
|
# Construct proper Plex URL with token
|
||||||
|
fixed_url = f"{plex_base_url.rstrip('/')}{path}?X-Plex-Token={plex_token}"
|
||||||
|
print(f"🔧 Fixed URL: {fixed_url}")
|
||||||
|
return fixed_url
|
||||||
|
|
||||||
|
# For other servers, we might need similar logic later
|
||||||
|
|
||||||
|
# Return original URL if no fixing needed/possible
|
||||||
|
return thumb_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fixing image URL '{thumb_url}': {e}")
|
||||||
|
return thumb_url
|
||||||
|
|
||||||
|
@app.route('/api/library/artists')
|
||||||
|
def get_library_artists():
|
||||||
|
"""Get artists for the library page with search, filtering, and pagination"""
|
||||||
|
try:
|
||||||
|
# Get query parameters
|
||||||
|
search_query = request.args.get('search', '')
|
||||||
|
letter = request.args.get('letter', 'all')
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 75))
|
||||||
|
|
||||||
|
# Get database instance
|
||||||
|
database = get_database()
|
||||||
|
|
||||||
|
# Get artists from database
|
||||||
|
result = database.get_library_artists(
|
||||||
|
search_query=search_query,
|
||||||
|
letter=letter,
|
||||||
|
page=page,
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fix image URLs for all artists
|
||||||
|
for artist in result['artists']:
|
||||||
|
if artist.get('image_url'):
|
||||||
|
artist['image_url'] = fix_artist_image_url(artist['image_url'])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
**result
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error fetching library artists: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"artists": [],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"limit": 75,
|
||||||
|
"total_count": 0,
|
||||||
|
"total_pages": 0,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False
|
||||||
|
}
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/library/debug-photos')
|
||||||
|
def debug_library_photos():
|
||||||
|
"""Debug endpoint to check artist photo URLs"""
|
||||||
|
try:
|
||||||
|
database = get_database()
|
||||||
|
|
||||||
|
with database._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get first 10 artists with their photo URLs
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT name, thumb_url, server_source
|
||||||
|
FROM artists
|
||||||
|
WHERE thumb_url IS NOT NULL AND thumb_url != ''
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
|
||||||
|
artists_with_photos = cursor.fetchall()
|
||||||
|
|
||||||
|
# Get first 10 artists without photos
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT name, thumb_url, server_source
|
||||||
|
FROM artists
|
||||||
|
WHERE thumb_url IS NULL OR thumb_url = ''
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
|
||||||
|
artists_without_photos = cursor.fetchall()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"artists_with_photos": [dict(row) for row in artists_with_photos],
|
||||||
|
"artists_without_photos": [dict(row) for row in artists_without_photos],
|
||||||
|
"total_with_photos": len(artists_with_photos),
|
||||||
|
"total_without_photos": len(artists_without_photos)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/artist/<artist_id>/discography', methods=['GET'])
|
@app.route('/api/artist/<artist_id>/discography', methods=['GET'])
|
||||||
def get_artist_discography(artist_id):
|
def get_artist_discography(artist_id):
|
||||||
|
|
|
||||||
104
webui/index.html
104
webui/index.html
|
|
@ -33,6 +33,10 @@
|
||||||
<span class="nav-icon">🎵</span>
|
<span class="nav-icon">🎵</span>
|
||||||
<span class="nav-text">Artists</span>
|
<span class="nav-text">Artists</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="nav-button" data-page="library">
|
||||||
|
<span class="nav-icon">📚</span>
|
||||||
|
<span class="nav-text">Library</span>
|
||||||
|
</button>
|
||||||
<button class="nav-button" data-page="settings">
|
<button class="nav-button" data-page="settings">
|
||||||
<span class="nav-icon">⚙️</span>
|
<span class="nav-icon">⚙️</span>
|
||||||
<span class="nav-text">Settings</span>
|
<span class="nav-text">Settings</span>
|
||||||
|
|
@ -626,7 +630,105 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Library Page -->
|
||||||
|
<div class="page" id="library-page">
|
||||||
|
<div class="library-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="library-header">
|
||||||
|
<div class="library-header-content">
|
||||||
|
<h2 class="library-title">📚 Music Library</h2>
|
||||||
|
<p class="library-subtitle">Browse your complete music collection</p>
|
||||||
|
</div>
|
||||||
|
<div class="library-stats" id="library-stats">
|
||||||
|
<span class="library-stat">
|
||||||
|
<span class="stat-number" id="library-artist-count">0</span>
|
||||||
|
<span class="stat-label">Artists</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search and Filters -->
|
||||||
|
<div class="library-controls">
|
||||||
|
<div class="library-search-container">
|
||||||
|
<input type="text"
|
||||||
|
id="library-search-input"
|
||||||
|
class="library-search-input"
|
||||||
|
placeholder="Search artists...">
|
||||||
|
<div class="library-search-icon">🔍</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alphabet Selector -->
|
||||||
|
<div class="alphabet-selector" id="alphabet-selector">
|
||||||
|
<div class="alphabet-selector-inner">
|
||||||
|
<button class="alphabet-btn active" data-letter="all">All</button>
|
||||||
|
<button class="alphabet-btn" data-letter="a">A</button>
|
||||||
|
<button class="alphabet-btn" data-letter="b">B</button>
|
||||||
|
<button class="alphabet-btn" data-letter="c">C</button>
|
||||||
|
<button class="alphabet-btn" data-letter="d">D</button>
|
||||||
|
<button class="alphabet-btn" data-letter="e">E</button>
|
||||||
|
<button class="alphabet-btn" data-letter="f">F</button>
|
||||||
|
<button class="alphabet-btn" data-letter="g">G</button>
|
||||||
|
<button class="alphabet-btn" data-letter="h">H</button>
|
||||||
|
<button class="alphabet-btn" data-letter="i">I</button>
|
||||||
|
<button class="alphabet-btn" data-letter="j">J</button>
|
||||||
|
<button class="alphabet-btn" data-letter="k">K</button>
|
||||||
|
<button class="alphabet-btn" data-letter="l">L</button>
|
||||||
|
<button class="alphabet-btn" data-letter="m">M</button>
|
||||||
|
<button class="alphabet-btn" data-letter="n">N</button>
|
||||||
|
<button class="alphabet-btn" data-letter="o">O</button>
|
||||||
|
<button class="alphabet-btn" data-letter="p">P</button>
|
||||||
|
<button class="alphabet-btn" data-letter="q">Q</button>
|
||||||
|
<button class="alphabet-btn" data-letter="r">R</button>
|
||||||
|
<button class="alphabet-btn" data-letter="s">S</button>
|
||||||
|
<button class="alphabet-btn" data-letter="t">T</button>
|
||||||
|
<button class="alphabet-btn" data-letter="u">U</button>
|
||||||
|
<button class="alphabet-btn" data-letter="v">V</button>
|
||||||
|
<button class="alphabet-btn" data-letter="w">W</button>
|
||||||
|
<button class="alphabet-btn" data-letter="x">X</button>
|
||||||
|
<button class="alphabet-btn" data-letter="y">Y</button>
|
||||||
|
<button class="alphabet-btn" data-letter="z">Z</button>
|
||||||
|
<button class="alphabet-btn" data-letter="#">#</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="library-content">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div class="library-loading hidden" id="library-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">Loading artists...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Artist Grid -->
|
||||||
|
<div class="library-artists-grid" id="library-artists-grid">
|
||||||
|
<!-- Artist cards will be populated here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="library-empty hidden" id="library-empty">
|
||||||
|
<div class="empty-icon">🎵</div>
|
||||||
|
<div class="empty-title">No artists found</div>
|
||||||
|
<div class="empty-subtitle">Try adjusting your search or filters</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="library-pagination hidden" id="library-pagination">
|
||||||
|
<button class="pagination-btn" id="prev-page-btn" disabled>
|
||||||
|
<span>← Previous</span>
|
||||||
|
</button>
|
||||||
|
<div class="pagination-info">
|
||||||
|
<span id="page-info">Page 1 of 1</span>
|
||||||
|
</div>
|
||||||
|
<button class="pagination-btn" id="next-page-btn" disabled>
|
||||||
|
<span>Next →</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings Page -->
|
<!-- Settings Page -->
|
||||||
<div class="page" id="settings-page">
|
<div class="page" id="settings-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|
|
||||||
|
|
@ -396,6 +396,15 @@ async function loadPageData(pageId) {
|
||||||
restoreArtistsPageState();
|
restoreArtistsPageState();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'library':
|
||||||
|
// Initialize and load library data
|
||||||
|
if (!libraryPageState.isInitialized) {
|
||||||
|
initializeLibraryPage();
|
||||||
|
} else {
|
||||||
|
// Refresh data when returning to page
|
||||||
|
await loadLibraryArtists();
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
initializeSettings();
|
initializeSettings();
|
||||||
await loadSettingsData();
|
await loadSettingsData();
|
||||||
|
|
@ -13587,4 +13596,299 @@ function cleanupSyncPageLogs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Global Cleanup on Page Unload ---
|
// --- Global Cleanup on Page Unload ---
|
||||||
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
|
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
|
||||||
|
// ===============================
|
||||||
|
// LIBRARY PAGE FUNCTIONALITY
|
||||||
|
// ===============================
|
||||||
|
|
||||||
|
// Library page state
|
||||||
|
const libraryPageState = {
|
||||||
|
isInitialized: false,
|
||||||
|
currentSearch: "",
|
||||||
|
currentLetter: "all",
|
||||||
|
currentPage: 1,
|
||||||
|
limit: 75,
|
||||||
|
debounceTimer: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function initializeLibraryPage() {
|
||||||
|
console.log("🔧 Initializing Library page...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize search functionality
|
||||||
|
initializeLibrarySearch();
|
||||||
|
|
||||||
|
// Initialize alphabet selector
|
||||||
|
initializeAlphabetSelector();
|
||||||
|
|
||||||
|
// Initialize pagination
|
||||||
|
initializeLibraryPagination();
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
loadLibraryArtists();
|
||||||
|
|
||||||
|
libraryPageState.isInitialized = true;
|
||||||
|
console.log("✅ Library page initialized successfully");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error initializing Library page:", error);
|
||||||
|
showToast("Failed to initialize Library page", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeLibrarySearch() {
|
||||||
|
const searchInput = document.getElementById("library-search-input");
|
||||||
|
if (!searchInput) return;
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", (e) => {
|
||||||
|
const query = e.target.value.trim();
|
||||||
|
|
||||||
|
// Clear existing debounce timer
|
||||||
|
if (libraryPageState.debounceTimer) {
|
||||||
|
clearTimeout(libraryPageState.debounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search requests
|
||||||
|
libraryPageState.debounceTimer = setTimeout(() => {
|
||||||
|
libraryPageState.currentSearch = query;
|
||||||
|
libraryPageState.currentPage = 1; // Reset to first page
|
||||||
|
loadLibraryArtists();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear search on Escape key
|
||||||
|
searchInput.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
searchInput.value = "";
|
||||||
|
libraryPageState.currentSearch = "";
|
||||||
|
libraryPageState.currentPage = 1;
|
||||||
|
loadLibraryArtists();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeAlphabetSelector() {
|
||||||
|
const alphabetButtons = document.querySelectorAll(".alphabet-btn");
|
||||||
|
|
||||||
|
alphabetButtons.forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const letter = button.getAttribute("data-letter");
|
||||||
|
|
||||||
|
// Update active state
|
||||||
|
alphabetButtons.forEach(btn => btn.classList.remove("active"));
|
||||||
|
button.classList.add("active");
|
||||||
|
|
||||||
|
// Update state and load data
|
||||||
|
libraryPageState.currentLetter = letter;
|
||||||
|
libraryPageState.currentPage = 1; // Reset to first page
|
||||||
|
loadLibraryArtists();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeLibraryPagination() {
|
||||||
|
const prevBtn = document.getElementById("prev-page-btn");
|
||||||
|
const nextBtn = document.getElementById("next-page-btn");
|
||||||
|
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.addEventListener("click", () => {
|
||||||
|
if (libraryPageState.currentPage > 1) {
|
||||||
|
libraryPageState.currentPage--;
|
||||||
|
loadLibraryArtists();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.addEventListener("click", () => {
|
||||||
|
libraryPageState.currentPage++;
|
||||||
|
loadLibraryArtists();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLibraryArtists() {
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
showLibraryLoading(true);
|
||||||
|
|
||||||
|
// Build query parameters
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
search: libraryPageState.currentSearch,
|
||||||
|
letter: libraryPageState.currentLetter,
|
||||||
|
page: libraryPageState.currentPage,
|
||||||
|
limit: libraryPageState.limit
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch artists from API
|
||||||
|
const response = await fetch(`/api/library/artists?${params}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || "Failed to load artists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI with artists
|
||||||
|
displayLibraryArtists(data.artists);
|
||||||
|
updateLibraryPagination(data.pagination);
|
||||||
|
updateLibraryStats(data.pagination.total_count);
|
||||||
|
|
||||||
|
// Hide loading state
|
||||||
|
showLibraryLoading(false);
|
||||||
|
|
||||||
|
// Show empty state if no artists
|
||||||
|
if (data.artists.length === 0) {
|
||||||
|
showLibraryEmpty(true);
|
||||||
|
} else {
|
||||||
|
showLibraryEmpty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error loading library artists:", error);
|
||||||
|
showToast("Failed to load artists", "error");
|
||||||
|
showLibraryLoading(false);
|
||||||
|
showLibraryEmpty(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayLibraryArtists(artists) {
|
||||||
|
const grid = document.getElementById("library-artists-grid");
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
// Clear existing content
|
||||||
|
grid.innerHTML = "";
|
||||||
|
|
||||||
|
// Create artist cards
|
||||||
|
artists.forEach(artist => {
|
||||||
|
const card = createLibraryArtistCard(artist);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLibraryArtistCard(artist) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "library-artist-card";
|
||||||
|
card.setAttribute("data-artist-id", artist.id);
|
||||||
|
|
||||||
|
// Create image element
|
||||||
|
const imageContainer = document.createElement("div");
|
||||||
|
imageContainer.className = "library-artist-image";
|
||||||
|
|
||||||
|
if (artist.image_url && artist.image_url.trim() !== "") {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = artist.image_url;
|
||||||
|
img.alt = artist.name;
|
||||||
|
img.onerror = () => {
|
||||||
|
console.log(`Failed to load image for ${artist.name}: ${artist.image_url}`);
|
||||||
|
// Replace with fallback on error
|
||||||
|
imageContainer.innerHTML = `<div class="library-artist-image-fallback">🎵</div>`;
|
||||||
|
};
|
||||||
|
img.onload = () => {
|
||||||
|
console.log(`Successfully loaded image for ${artist.name}: ${artist.image_url}`);
|
||||||
|
};
|
||||||
|
imageContainer.appendChild(img);
|
||||||
|
} else {
|
||||||
|
console.log(`No image URL for ${artist.name}: '${artist.image_url}'`);
|
||||||
|
imageContainer.innerHTML = `<div class="library-artist-image-fallback">🎵</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create info section
|
||||||
|
const info = document.createElement("div");
|
||||||
|
info.className = "library-artist-info";
|
||||||
|
|
||||||
|
const name = document.createElement("h3");
|
||||||
|
name.className = "library-artist-name";
|
||||||
|
name.textContent = artist.name;
|
||||||
|
name.title = artist.name; // For tooltip on long names
|
||||||
|
|
||||||
|
const stats = document.createElement("div");
|
||||||
|
stats.className = "library-artist-stats";
|
||||||
|
|
||||||
|
if (artist.album_count > 0 || artist.track_count > 0) {
|
||||||
|
const albumStat = document.createElement("span");
|
||||||
|
albumStat.className = "library-artist-stat";
|
||||||
|
albumStat.textContent = `${artist.album_count} album${artist.album_count !== 1 ? "s" : ""}`;
|
||||||
|
|
||||||
|
const trackStat = document.createElement("span");
|
||||||
|
trackStat.className = "library-artist-stat";
|
||||||
|
trackStat.textContent = `${artist.track_count} track${artist.track_count !== 1 ? "s" : ""}`;
|
||||||
|
|
||||||
|
stats.appendChild(albumStat);
|
||||||
|
stats.appendChild(trackStat);
|
||||||
|
}
|
||||||
|
|
||||||
|
info.appendChild(name);
|
||||||
|
info.appendChild(stats);
|
||||||
|
|
||||||
|
// Assemble card
|
||||||
|
card.appendChild(imageContainer);
|
||||||
|
card.appendChild(info);
|
||||||
|
|
||||||
|
// Add click handler (for future functionality)
|
||||||
|
card.addEventListener("click", () => {
|
||||||
|
// TODO: Implement artist detail view
|
||||||
|
console.log(`Clicked on artist: ${artist.name}`);
|
||||||
|
showToast(`Artist details coming soon!`, "info");
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLibraryPagination(pagination) {
|
||||||
|
const prevBtn = document.getElementById("prev-page-btn");
|
||||||
|
const nextBtn = document.getElementById("next-page-btn");
|
||||||
|
const pageInfo = document.getElementById("page-info");
|
||||||
|
const paginationContainer = document.getElementById("library-pagination");
|
||||||
|
|
||||||
|
if (!paginationContainer) return;
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.disabled = !pagination.has_prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.disabled = !pagination.has_next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update page info
|
||||||
|
if (pageInfo) {
|
||||||
|
pageInfo.textContent = `Page ${pagination.page} of ${pagination.total_pages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide pagination based on total pages
|
||||||
|
if (pagination.total_pages > 1) {
|
||||||
|
paginationContainer.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
paginationContainer.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLibraryStats(totalCount) {
|
||||||
|
const countElement = document.getElementById("library-artist-count");
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = totalCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLibraryLoading(show) {
|
||||||
|
const loadingElement = document.getElementById("library-loading");
|
||||||
|
if (loadingElement) {
|
||||||
|
if (show) {
|
||||||
|
loadingElement.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
loadingElement.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLibraryEmpty(show) {
|
||||||
|
const emptyElement = document.getElementById("library-empty");
|
||||||
|
if (emptyElement) {
|
||||||
|
if (show) {
|
||||||
|
emptyElement.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
emptyElement.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8459,4 +8459,414 @@ body {
|
||||||
#metadata-updater-card{
|
#metadata-updater-card{
|
||||||
display: flex!important;
|
display: flex!important;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
/* ===============================
|
||||||
|
LIBRARY PAGE STYLING
|
||||||
|
=============================== */
|
||||||
|
|
||||||
|
.library-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Library Header */
|
||||||
|
.library-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-header-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
background: linear-gradient(135deg, #1ed760, #1db954);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
display: block;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1ed760;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Library Controls */
|
||||||
|
.library-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Container */
|
||||||
|
.library-search-container {
|
||||||
|
position: relative;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 20px 14px 50px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search-input:focus {
|
||||||
|
border-color: rgba(29, 185, 84, 0.6);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 0 16px rgba(29, 185, 84, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alphabet Selector */
|
||||||
|
.alphabet-selector {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-selector::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-selector-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-btn.active {
|
||||||
|
background: linear-gradient(135deg, #1db954, #1ed760);
|
||||||
|
border-color: rgba(29, 185, 84, 0.4);
|
||||||
|
color: #000000;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Library Content */
|
||||||
|
.library-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artists Grid */
|
||||||
|
.library-artists-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Library Artist Card */
|
||||||
|
.library-artist-card {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(26, 26, 26, 0.95) 0%,
|
||||||
|
rgba(18, 18, 18, 0.98) 100%);
|
||||||
|
backdrop-filter: blur(12px) saturate(1.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: rgba(29, 185, 84, 0.3);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 20px rgba(29, 185, 84, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-card:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artist Image */
|
||||||
|
.library-artist-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0.05) 100%);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-card:hover .library-artist-image {
|
||||||
|
border-color: rgba(29, 185, 84, 0.4);
|
||||||
|
box-shadow: 0 0 20px rgba(29, 185, 84, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-card:hover .library-artist-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artist Image Fallback */
|
||||||
|
.library-artist-image-fallback {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 48px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(255, 255, 255, 0.08) 0%,
|
||||||
|
rgba(255, 255, 255, 0.03) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artist Info */
|
||||||
|
.library-artist-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-stats {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-stat {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.library-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.library-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.library-artists-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.library-container {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-stats {
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artists-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-artist-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue