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}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/artists')
|
||||
def get_artists():
|
||||
# Placeholder: returns mock artist data
|
||||
mock_artists = [
|
||||
{"name": "Queen", "album_count": 15, "image": None},
|
||||
{"name": "Led Zeppelin", "album_count": 9, "image": None}
|
||||
]
|
||||
return jsonify({"artists": mock_artists})
|
||||
def fix_artist_image_url(thumb_url):
|
||||
"""Convert localhost URLs to proper server URLs using config"""
|
||||
if not thumb_url:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Check if it's a localhost URL or relative path that needs fixing
|
||||
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'])
|
||||
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-text">Artists</span>
|
||||
</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">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span class="nav-text">Settings</span>
|
||||
|
|
@ -626,7 +630,105 @@
|
|||
</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 -->
|
||||
<div class="page" id="settings-page">
|
||||
<div class="page-header">
|
||||
|
|
|
|||
|
|
@ -396,6 +396,15 @@ async function loadPageData(pageId) {
|
|||
restoreArtistsPageState();
|
||||
}
|
||||
break;
|
||||
case 'library':
|
||||
// Initialize and load library data
|
||||
if (!libraryPageState.isInitialized) {
|
||||
initializeLibraryPage();
|
||||
} else {
|
||||
// Refresh data when returning to page
|
||||
await loadLibraryArtists();
|
||||
}
|
||||
break;
|
||||
case 'settings':
|
||||
initializeSettings();
|
||||
await loadSettingsData();
|
||||
|
|
@ -13587,4 +13596,299 @@ function cleanupSyncPageLogs() {
|
|||
}
|
||||
|
||||
// --- 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{
|
||||
display: flex!important;
|
||||
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