![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/.cursor-server/data/User/History/-71567ccd/ |
<?php
// Define development mode (set to true for debugging, false for production)
define('DEVELOPMENT_MODE', false);
// Disable error reporting in production for security
error_reporting(0);
ini_set('display_errors', 0);
session_start();
// Include translation system
require_once __DIR__ . '/includes/translations.php';
// Add security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
// ALWAYS check if user is logged in - no exceptions
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
// Debug: Log that we're past the session check (only in development)
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Library.php: Session check passed, user_id: " . ($_SESSION['user_id'] ?? 'not set'));
}
// Handle track edit form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_track') {
// Include database configuration
require_once 'config/database.php';
try {
// Get database connection
$pdo = getDBConnection();
if (!$pdo) {
throw new Exception('Database connection failed');
}
$track_id = $_POST['track_id'] ?? null;
if (!$track_id) {
throw new Exception('Track ID is required');
}
$title = trim($_POST['title'] ?? '');
if (empty($title)) {
throw new Exception('Title is required');
}
$description = trim($_POST['description'] ?? '');
$price = floatval($_POST['price'] ?? 0);
$is_public = isset($_POST['is_public']) ? 1 : 0;
// Validate that the track belongs to the current user
$stmt = $pdo->prepare("SELECT user_id FROM music_tracks WHERE id = ?");
$stmt->execute([$track_id]);
$track = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$track || $track['user_id'] != $_SESSION['user_id']) {
$_SESSION['error_message'] = 'You can only edit your own tracks.';
header('Location: /library.php');
exit;
}
// Update the track
// Note: The database column is 'prompt', not 'description'
$stmt = $pdo->prepare("
UPDATE music_tracks
SET title = ?, prompt = ?, price = ?, is_public = ?, updated_at = NOW()
WHERE id = ? AND user_id = ?
");
$result = $stmt->execute([$title, $description, $price, $is_public, $track_id, $_SESSION['user_id']]);
if ($result) {
$_SESSION['success_message'] = 'Track updated successfully!';
} else {
$_SESSION['error_message'] = 'Failed to update track. Please try again.';
}
} catch (Exception $e) {
// Always log errors for debugging
error_log("Error updating track: " . $e->getMessage());
error_log("Error trace: " . $e->getTraceAsString());
$_SESSION['error_message'] = 'An error occurred while updating the track: ' . htmlspecialchars($e->getMessage());
} catch (Error $e) {
// Catch fatal errors too
error_log("Fatal error updating track: " . $e->getMessage());
error_log("Error trace: " . $e->getTraceAsString());
$_SESSION['error_message'] = 'A fatal error occurred while updating the track.';
}
header('Location: /library.php');
exit;
}
// Helper functions for formatting
function formatBytes($bytes, $precision = 2) {
if ($bytes == 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB'];
$base = log($bytes, 1024);
$unit = $units[floor($base)];
return round(pow(1024, $base - floor($base)), $precision) . ' ' . $unit;
}
function formatTime($seconds) {
if ($seconds < 60) {
return round($seconds, 1) . 's';
} elseif ($seconds < 3600) {
$minutes = floor($seconds / 60);
$remainingSeconds = $seconds % 60;
return $minutes . 'm ' . round($remainingSeconds) . 's';
} else {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
return $hours . 'h ' . $minutes . 'm';
}
}
// Include database configuration
require_once 'config/database.php';
// Debug: Test database connection (only in development)
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Library.php: Database config loaded");
}
// Get user info with error handling
$user_id = $_SESSION['user_id'] ?? null; // Define user_id for JavaScript checks
try {
$user = getUserById($_SESSION['user_id']);
$user_name = $user['name'] ?? 'User';
// Get credits from database, not session (session can be outdated)
$credits = $user['credits'] ?? 0;
$plan = $user['plan'] ?? 'free';
// Update session with current credits for consistency
$_SESSION['credits'] = $credits;
$_SESSION['plan'] = $plan;
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting user info: " . $e->getMessage());
}
$user_name = 'User';
$credits = $_SESSION['credits'] ?? 5;
$plan = $_SESSION['plan'] ?? 'free';
}
// Get user stats
$pdo = getDBConnection();
if (!$pdo) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Database connection failed in library.php");
}
die("Unable to connect to the database. Please try again later.");
}
$stmt = $pdo->prepare("
SELECT
COUNT(*) as total_tracks,
COUNT(CASE WHEN status = 'complete' THEN 1 END) as completed_tracks,
COUNT(CASE WHEN status = 'processing' THEN 1 END) as processing_tracks,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_tracks,
AVG(CASE WHEN status = 'complete' THEN duration END) as avg_duration,
SUM(CASE WHEN status = 'complete' THEN duration END) as total_duration
FROM music_tracks
WHERE user_id = ?
");
try {
$stmt->execute([$_SESSION['user_id']]);
$user_stats = $stmt->fetch();
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting user stats: " . $e->getMessage());
}
$user_stats = [
'total_tracks' => 0,
'completed_tracks' => 0,
'processing_tracks' => 0,
'failed_tracks' => 0,
'avg_duration' => 0,
'total_duration' => 0
];
}
// Get credit transaction history (last 10)
$credit_history = [];
try {
$stmt = $pdo->prepare("
SELECT * FROM credit_transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
");
$stmt->execute([$_SESSION['user_id']]);
$credit_history = $stmt->fetchAll();
} catch (Exception $e) {
// Credit history table might not exist yet
}
// Calculate user level based on activity
$user_level = 1;
$xp = ($user_stats['completed_tracks'] ?? 0) * 10 + ($user_stats['total_duration'] ?? 0);
if ($xp >= 1000) $user_level = 5;
elseif ($xp >= 500) $user_level = 4;
elseif ($xp >= 200) $user_level = 3;
elseif ($xp >= 50) $user_level = 2;
// Get recent activity (last 10 tracks)
$recent_activity = [];
try {
$stmt = $pdo->prepare("
SELECT title, created_at, status
FROM music_tracks
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
");
$stmt->execute([$_SESSION['user_id']]);
$recent_activity = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
// Error getting recent activity
}
// Get user's music tracks with enhanced data and variations
$status_filter = $_GET['status'] ?? 'all';
$sort_filter = $_GET['sort'] ?? 'latest';
$time_filter = $_GET['time'] ?? 'all';
$genre_filter = $_GET['genre'] ?? '';
$search_query = trim($_GET['search'] ?? '');
// Build WHERE clause for status filtering
$where_clause = "WHERE mt.user_id = ?";
$params = [$_SESSION['user_id']];
if ($status_filter !== 'all') {
$where_clause .= " AND mt.status = ?";
$params[] = $status_filter;
}
// Add search filter
if (!empty($search_query)) {
$where_clause .= " AND (mt.title LIKE ? OR mt.prompt LIKE ?)";
$search_param = '%' . $search_query . '%';
$params[] = $search_param;
$params[] = $search_param;
}
// Build time filter condition and add to WHERE clause
switch ($time_filter) {
case 'today':
$where_clause .= " AND mt.created_at >= CURDATE()";
break;
case 'week':
$where_clause .= " AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)";
break;
case 'month':
$where_clause .= " AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)";
break;
case 'all':
default:
// No time filter
break;
}
// Build genre filter condition and add to WHERE clause
if (!empty($genre_filter) && $genre_filter !== 'all') {
// Try to match genre from metadata JSON field
$where_clause .= " AND (JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre')) = ? OR mt.genre = ?)";
$params[] = $genre_filter;
$params[] = $genre_filter;
}
// Build ORDER BY clause and additional JOINs for sorting
$order_clause = "ORDER BY ";
$additional_joins = "";
switch ($sort_filter) {
case 'oldest':
$order_clause .= "mt.created_at ASC";
break;
case 'popular':
$additional_joins = "LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) likes ON mt.id = likes.track_id";
$order_clause = "ORDER BY COALESCE(likes.like_count, 0) DESC, mt.created_at DESC";
break;
case 'most-played':
$additional_joins = "LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) plays ON mt.id = plays.track_id";
$order_clause = "ORDER BY COALESCE(plays.play_count, 0) DESC, mt.created_at DESC";
break;
case 'latest':
default:
$order_clause .= "mt.created_at DESC";
break;
}
// Get tracks
try {
// Check if audio_variations table exists
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
if ($checkTable->rowCount() > 0) {
$stmt = $pdo->prepare("
SELECT
mt.*,
COALESCE(vars.variation_count, 0) as variation_count,
CASE
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR) THEN 'π₯ Hot'
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 'β New'
ELSE ''
END as badge
FROM music_tracks mt
LEFT JOIN (
SELECT track_id, COUNT(*) as variation_count
FROM audio_variations
GROUP BY track_id
) vars ON mt.id = vars.track_id
$additional_joins
$where_clause
$order_clause
");
} else {
// Fallback query without audio_variations table
$stmt = $pdo->prepare("
SELECT
mt.*,
0 as variation_count,
CASE
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR) THEN 'π₯ Hot'
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 'β New'
ELSE ''
END as badge
FROM music_tracks mt
$additional_joins
$where_clause
$order_clause
");
}
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error preparing tracks query: " . $e->getMessage());
}
// Fallback to simple query
$stmt = $pdo->prepare("
SELECT
mt.*,
0 as variation_count,
'' as badge
FROM music_tracks mt
$where_clause
ORDER BY mt.created_at DESC
");
}
try {
$stmt->execute($params);
$user_tracks = $stmt->fetchAll();
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting user tracks: " . $e->getMessage());
}
$user_tracks = [];
}
// Get variations for each track
$tracks_with_variations = [];
foreach ($user_tracks as $track) {
$track['variations'] = [];
if ($track['variation_count'] > 0) {
try {
// Check if audio_variations table exists
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
if ($checkTable->rowCount() > 0) {
$stmt = $pdo->prepare("
SELECT
variation_index,
audio_url,
duration,
title,
tags,
image_url,
source_audio_url,
metadata,
created_at
FROM audio_variations
WHERE track_id = ?
ORDER BY variation_index ASC
");
$stmt->execute([$track['id']]);
$track['variations'] = $stmt->fetchAll();
// Debug: Log variations found
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("π΅ Track {$track['id']}: Found " . count($track['variations']) . " variations");
}
} else {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("audio_variations table does not exist");
}
$track['variations'] = [];
}
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting variations for track {$track['id']}: " . $e->getMessage());
}
$track['variations'] = [];
}
} else {
// Debug: Log when no variations are expected
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("π΅ Track {$track['id']}: No variations expected (variation_count = {$track['variation_count']})");
}
}
$tracks_with_variations[] = $track;
}
// Set page variables for header
$current_page = 'library';
$page_title = t('library.page_title');
$page_description = t('library.page_description');
include 'includes/header.php';
// Enhanced sort order function with proper handling
function getSortOrder($sort_filter) {
// Debug logging
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("π΅ Library: Sorting by '$sort_filter'");
}
switch ($sort_filter) {
case 'latest':
return "mt.created_at DESC";
case 'oldest':
return "mt.created_at ASC";
case 'popular':
return "(SELECT COUNT(*) FROM track_likes WHERE track_id = mt.id) DESC, mt.created_at DESC";
case 'most-played':
return "(SELECT COUNT(*) FROM track_plays WHERE track_id = mt.id) DESC, mt.created_at DESC";
case 'most-viewed':
return "(SELECT COUNT(*) FROM track_views WHERE track_id = mt.id) DESC, mt.created_at DESC";
case 'most-commented':
return "(SELECT COUNT(*) FROM track_comments WHERE track_id = mt.id) DESC, mt.created_at DESC";
case 'most-shared':
return "(SELECT COUNT(*) FROM track_shares WHERE track_id = mt.id) DESC, mt.created_at DESC";
case 'title':
return "mt.title ASC";
case 'artist':
return "u.name ASC, mt.created_at DESC";
case 'duration':
return "mt.duration DESC, mt.created_at DESC";
default:
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("π΅ Library: Unknown sort filter '$sort_filter', using default");
}
return "mt.created_at DESC";
}
}
// Load all tracks at once (no pagination)
$per_page = 1000; // Large number to get all tracks
$offset = 0;
// Get user's tracks with social data and artist info (safe query)
try {
// Build the query using the WHERE clause that filters by user_id
$query = "
SELECT
mt.id,
mt.title,
mt.prompt,
mt.audio_url,
mt.duration,
mt.created_at,
mt.user_id,
mt.price,
mt.metadata,
mt.status,
mt.task_id,
u.name as artist_name,
u.profile_image,
u.plan,
COALESCE((SELECT COUNT(*) FROM track_likes WHERE track_id = mt.id), 0) as like_count,
COALESCE((SELECT COUNT(*) FROM track_comments WHERE track_id = mt.id), 0) as comment_count,
COALESCE((SELECT COUNT(*) FROM track_shares WHERE track_id = mt.id), 0) as share_count,
COALESCE((SELECT COUNT(*) FROM track_plays WHERE track_id = mt.id), 0) as play_count,
COALESCE((SELECT COUNT(*) FROM track_views WHERE track_id = mt.id), 0) as view_count,
COALESCE((SELECT COUNT(*) FROM user_follows WHERE follower_id = ? AND following_id = mt.user_id), 0) as is_following,
CASE WHEN EXISTS(SELECT 1 FROM track_likes WHERE track_id = mt.id AND user_id = ?) THEN 1 ELSE 0 END as user_liked,
COALESCE((SELECT COUNT(*) FROM music_tracks WHERE user_id = mt.user_id AND status = 'complete'), 0) as artist_total_tracks
FROM music_tracks mt
JOIN users u ON mt.user_id = u.id
" . $where_clause . "
AND (
(mt.status = 'complete' AND mt.audio_url IS NOT NULL AND mt.audio_url != '')
OR
(mt.status = 'processing' AND mt.task_id IS NOT NULL)
)
ORDER BY " . getSortOrder($sort_filter) . "
LIMIT $per_page OFFSET $offset
";
$stmt = $pdo->prepare($query);
// Add user_id parameters for is_following and user_liked checks
// $params already contains the user_id from $where_clause, so we add it twice more for the subqueries
// Use $_SESSION['user_id'] directly to ensure we have the correct user ID for like checking
$check_user_id = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
// Debug: Log the user_id being used for like checking
if (empty($recent_tracks) || (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE)) {
error_log("Library query - Session user_id: " . ($_SESSION['user_id'] ?? 'NULL') . ", check_user_id: " . $check_user_id . ", params count: " . count($params));
}
$execute_params = array_merge($params, [$check_user_id, $check_user_id]);
$stmt->execute($execute_params);
$recent_tracks = $stmt->fetchAll();
// Ensure user_liked is an integer (PDO might return it as string)
// Also double-check against database to fix any query issues
foreach ($recent_tracks as &$track) {
$track['user_liked'] = (int)($track['user_liked'] ?? 0);
// Always verify like status directly from database if user is logged in
// This fixes any issues with the CASE WHEN EXISTS subquery
if ($check_user_id > 0) {
$like_check = $pdo->prepare("SELECT COUNT(*) as cnt FROM track_likes WHERE track_id = ? AND user_id = ?");
$like_check->execute([$track['id'], $check_user_id]);
$like_result = $like_check->fetch(PDO::FETCH_ASSOC);
$actual_liked = (int)$like_result['cnt'];
// Use the direct database check result
$track['user_liked'] = $actual_liked;
}
}
unset($track); // Break reference
// Debug: Log first track's user_liked value if in development mode
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE && !empty($recent_tracks)) {
error_log("Library: First track user_liked=" . $recent_tracks[0]['user_liked'] . ", track_id=" . $recent_tracks[0]['id'] . ", session_user_id=" . ($_SESSION['user_id'] ?? 'NULL') . ", check_user_id=" . $check_user_id);
}
} catch (Exception $e) {
// Fallback to basic query if social features fail
$stmt = $pdo->prepare("
SELECT
mt.id,
mt.title,
mt.prompt,
mt.audio_url,
mt.duration,
mt.created_at,
mt.user_id,
mt.price,
mt.metadata,
u.name as artist_name,
u.profile_image,
u.plan,
0 as like_count,
0 as comment_count,
0 as share_count,
0 as play_count,
0 as view_count,
0 as is_following,
0 as user_liked,
1 as artist_total_tracks
FROM music_tracks mt
JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND mt.audio_url IS NOT NULL
AND mt.audio_url != ''
ORDER BY mt.created_at DESC
LIMIT $per_page OFFSET $offset
");
$stmt->execute([]);
$recent_tracks = $stmt->fetchAll();
}
// Get all available genres from user's tracks
$genres_query = $pdo->prepare("
SELECT DISTINCT COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre')),
mt.genre,
'Electronic'
) as genre
FROM music_tracks mt
WHERE mt.user_id = ?
AND (
JSON_EXTRACT(metadata, '$.genre') IS NOT NULL
OR mt.genre IS NOT NULL
)
AND COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre')),
mt.genre,
''
) != ''
ORDER BY genre ASC
");
$genres_query->execute([$_SESSION['user_id']]);
$available_genres = $genres_query->fetchAll(PDO::FETCH_COLUMN);
// Add popular genres if missing from database
$popular_genres = ['Electronic', 'House', 'Techno', 'Pop', 'Hip Hop', 'Rock', 'Jazz', 'Classical', 'Ambient', 'Trance', 'Dubstep', 'R&B', 'Reggae', 'Country', 'Folk', 'Blues', 'Funk', 'Disco', 'Drum & Bass', 'Progressive', 'Chillout', 'Lofi'];
foreach ($popular_genres as $genre) {
if (!in_array($genre, $available_genres)) {
$available_genres[] = $genre;
}
}
$available_genres = array_unique($available_genres);
sort($available_genres);
// Get total count for pagination
$count_stmt = $pdo->prepare("
SELECT COUNT(*) as total
FROM music_tracks mt
WHERE mt.status = 'complete'
AND mt.audio_url IS NOT NULL
AND mt.audio_url != ''
");
$count_stmt->execute();
$total_tracks = $count_stmt->fetch()['total'];
$total_pages = ceil($total_tracks / $per_page);
// Get community stats
$community_stats = [
'total_tracks' => $total_tracks,
'total_artists' => count(array_unique(array_column($recent_tracks, 'artist_name'))),
'total_likes' => 0,
'total_comments' => 0,
'total_shares' => 0
];
?>
<div class="main-content">
<style>
/* ============================================
NEXT-GEN LIBRARY - OPTIMIZED & MODERN DESIGN
Performance-first, beautiful, cutting-edge
============================================ */
/* CSS Variables for easy theming */
:root {
--primary: #667eea;
--secondary: #764ba2;
--accent: #f59e0b;
--success: #48bb78;
--danger: #ef4444;
--bg-dark: #0a0a0a;
--bg-medium: #1a1a1a;
--bg-light: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #a0aec0;
--border: rgba(255, 255, 255, 0.1);
--glass: rgba(255, 255, 255, 0.05);
--shadow: rgba(102, 126, 234, 0.1);
}
/* Reset & Base */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
padding-bottom: 120px;
background: var(--bg-dark);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
}
/* Performance: GPU acceleration for animations */
.hero, .library-content, .track-card, .stat-card {
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
}
/* ============================================
HERO SECTION - Modern & Bold
============================================ */
.hero {
padding: 10rem 0 8rem;
text-align: center;
background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-medium) 50%, var(--bg-dark) 100%);
position: relative;
overflow: hidden;
margin-bottom: 0;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.1) 0%, transparent 50%),
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(102,126,234,0.08)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
opacity: 1;
pointer-events: none;
}
.hero .container {
max-width: 90rem;
margin: 0 auto;
padding: 0 2rem;
position: relative;
z-index: 2;
}
.hero-content {
max-width: 90rem;
margin: 0 auto;
position: relative;
z-index: 2;
}
.hero-badge {
display: inline-block;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.15));
color: var(--primary);
padding: 1rem 2rem;
border-radius: 50px;
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 2rem;
backdrop-filter: blur(20px);
border: 1px solid rgba(102, 126, 234, 0.2);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
animation: fadeInUp 0.6s ease-out;
}
.hero-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 900;
line-height: 1.1;
margin-bottom: 2rem;
background: linear-gradient(135deg, #ffffff 0%, var(--primary) 50%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: fadeInUp 0.8s ease-out 0.2s both;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: clamp(1.4rem, 3vw, 2rem);
font-weight: 400;
margin-bottom: 0;
opacity: 0.85;
max-width: 70rem;
margin-left: auto;
margin-right: auto;
color: var(--text-secondary);
animation: fadeInUp 1s ease-out 0.4s both;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ============================================
LIBRARY CONTENT - Organic Flow
============================================ */
.library-content,
.community-content {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--bg-light) 100%);
padding: 6rem 0;
border-radius: 50px 50px 0 0;
margin-top: -3rem;
position: relative;
z-index: 10;
min-height: 60vh;
}
.library-container,
.community-container {
max-width: 140rem;
margin: 0 auto;
padding: 0 2rem;
}
/* ============================================
STATS CARDS - Modern Grid
============================================ */
.stats-grid,
.community-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-bottom: 4rem;
}
.stat-card {
background: var(--glass);
padding: 2.5rem;
border-radius: 24px;
backdrop-filter: blur(20px);
border: 1px solid var(--border);
text-align: center;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transform: scaleX(0);
transition: transform 0.4s ease;
}
.stat-card:hover::before {
transform: scaleX(1);
}
.stat-card:hover {
transform: translateY(-8px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 20px 60px var(--shadow);
}
.stat-number {
font-size: clamp(2.5rem, 5vw, 4.8rem);
font-weight: 900;
color: var(--primary);
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
color: var(--text-secondary);
font-size: 1.2rem;
font-weight: 500;
}
/* ============================================
FILTERS - Clean & Modern
============================================ */
.filters-section,
.unified-filters {
background: var(--glass);
padding: 2rem;
border-radius: 20px;
backdrop-filter: blur(20px);
border: 1px solid var(--border);
margin-bottom: 4rem;
}
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1.5rem;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-label {
font-size: 0.9rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
padding: 1rem 1.2rem;
font-size: 1rem;
transition: all 0.3s ease;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 1rem center;
background-repeat: no-repeat;
background-size: 1.5rem;
padding-right: 3.5rem;
}
.filter-select:hover {
border-color: rgba(102, 126, 234, 0.4);
background-color: rgba(255, 255, 255, 0.08);
}
.filter-select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
/* ============================================
TRACKS GRID - Modern Feed Layout
============================================ */
.tracks-grid {
display: flex;
flex-direction: column;
gap: 2.5rem;
max-width: 90rem;
margin: 0 auto;
}
/* ============================================
TRACK CARD - Next-Gen Design (Image-Based)
============================================ */
.track-card-modern {
background: var(--glass);
padding: 2rem;
border-radius: 20px;
backdrop-filter: blur(20px);
border: 1px solid var(--border);
transition: all 0.3s ease;
position: relative;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.track-card-modern:hover {
transform: translateY(-4px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 20px 60px var(--shadow);
}
/* Status Badge (Top Right) */
.track-status-badge {
position: absolute;
top: 1.5rem;
right: 1.5rem;
background: linear-gradient(135deg, var(--success), #38a169);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
z-index: 10;
}
.track-status-badge.private {
background: linear-gradient(135deg, #9ca3af, #6b7280);
}
/* Track Card Content */
.track-card-content {
display: flex;
align-items: flex-start;
gap: 2rem;
}
/* Track Main Info (Icon + Details) */
.track-main-info {
display: flex;
gap: 1.5rem;
flex: 1;
min-width: 0;
}
/* Track Icon */
.track-icon {
width: 80px;
height: 80px;
min-width: 80px;
border-radius: 16px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: white;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
flex-shrink: 0;
}
/* Track Image Container */
.track-image-container {
width: 80px;
height: 80px;
min-width: 80px;
border-radius: 16px;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
background: linear-gradient(135deg, var(--primary), var(--secondary));
}
.track-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Track Details */
.track-details {
flex: 1;
min-width: 0;
}
.track-name {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-primary);
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.track-title-link {
color: var(--text-primary);
text-decoration: none;
transition: color 0.2s ease, opacity 0.2s ease;
}
.track-title-link:hover {
color: var(--primary-color);
opacity: 0.9;
}
.track-artist-name {
font-size: 1rem;
color: var(--text-secondary);
margin: 0 0 0.75rem 0;
}
.artist-name-link {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease, opacity 0.2s ease;
}
.artist-name-link:hover {
color: var(--primary-color);
opacity: 0.9;
}
.track-description {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.5;
margin: 0 0 1rem 0;
opacity: 0.8;
}
/* Metadata Row */
.track-metadata-row {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 0.5rem;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.3rem;
}
.meta-item strong {
color: var(--text-primary);
font-weight: 600;
}
/* Action Buttons Row */
.track-actions-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-shrink: 0;
}
/* Large Play Button */
.play-btn-large {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--secondary));
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
flex-shrink: 0;
}
.play-btn-large:hover {
transform: scale(1.1);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.6);
}
/* Icon Action Buttons */
.action-icon-btn {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-secondary);
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.action-icon-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
color: var(--text-primary);
transform: translateY(-2px);
}
.action-icon-btn i.fa-heart:hover {
color: #ef4444;
}
.action-icon-btn.liked {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
.action-icon-btn.liked i.fa-heart {
color: #ef4444;
}
.action-icon-btn.liked:hover i.fa-heart {
color: #dc2626;
}
/* Processing/Failed Indicators */
.processing-indicator,
.failed-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
}
.processing-indicator {
background: rgba(245, 158, 11, 0.1);
color: var(--accent);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.failed-indicator {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.track-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.track-card:hover {
transform: translateY(-8px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 25px 80px var(--shadow);
}
.track-card:hover::before {
opacity: 1;
}
/* Track Header */
.track-header,
.compact-header {
padding: 2.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
border-bottom: 1px solid var(--border);
}
.compact-header {
flex-wrap: wrap;
}
.artist-mini {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 0;
}
.artist-avatar-mini {
flex-shrink: 0;
}
.default-avatar-mini {
width: 50px;
height: 50px;
border-radius: 12px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 1.2rem;
}
.artist-info-mini {
flex: 1;
min-width: 0;
}
.track-title-mini {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 800;
color: var(--text-primary);
margin-bottom: 0.3rem;
line-height: 1.2;
word-wrap: break-word;
}
.artist-name-mini {
font-size: 1.1rem;
color: var(--primary);
font-weight: 600;
}
.track-header-content {
flex: 1;
min-width: 0;
}
.track-title {
font-size: clamp(1.8rem, 4vw, 2.4rem);
font-weight: 800;
color: var(--text-primary);
margin-bottom: 0.5rem;
line-height: 1.2;
word-wrap: break-word;
}
.track-artist {
font-size: 1.3rem;
color: var(--primary);
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.track-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
}
.status-badge {
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-complete {
background: linear-gradient(135deg, var(--success), #38a169);
color: white;
}
.status-processing {
background: linear-gradient(135deg, var(--accent), #d97706);
color: white;
animation: pulse 2s infinite;
}
.status-failed {
background: linear-gradient(135deg, var(--danger), #dc2626);
color: white;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Track Prompt */
.track-prompt {
padding: 2rem 2.5rem;
font-size: 1.3rem;
color: var(--text-secondary);
line-height: 1.7;
background: rgba(255, 255, 255, 0.02);
border-left: 4px solid var(--primary);
margin: 0;
}
/* Track Metadata */
.track-metadata {
padding: 2rem 2.5rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1.5rem;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.metadata-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.metadata-label {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metadata-value {
font-size: 1.1rem;
color: var(--text-primary);
font-weight: 700;
}
/* Track Actions */
.track-actions {
padding: 2rem 2.5rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.action-btn {
flex: 1;
min-width: 140px;
padding: 1.2rem 2rem;
border-radius: 14px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
border: none;
text-decoration: none;
}
.action-btn-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
}
.action-btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.4);
}
.action-btn-secondary {
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--border);
}
.action-btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.action-btn-compact {
padding: 0.8rem 1.5rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
border: none;
text-decoration: none;
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--border);
}
.action-btn-compact:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.action-btn-compact.primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
}
.action-btn-compact.primary:hover {
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
transform: translateY(-3px);
}
.track-number-badge {
position: absolute;
top: 1.5rem;
right: 1.5rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 800;
z-index: 10;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
}
.track-badge {
display: inline-block;
background: linear-gradient(135deg, var(--accent), #d97706);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
}
.error-message-modern {
padding: 1.5rem;
margin: 1.5rem 2.5rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
border-left: 4px solid var(--danger);
}
.error-header {
color: var(--danger);
font-weight: 700;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.error-content {
color: #fca5a5;
font-size: 0.95rem;
line-height: 1.6;
}
/* ============================================
RESPONSIVE - Mobile First
============================================ */
@media (max-width: 768px) {
.hero {
padding: 6rem 0 4rem;
}
.library-content,
.community-content {
padding: 4rem 0;
border-radius: 30px 30px 0 0;
}
.stats-grid,
.community-stats {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.filter-row {
grid-template-columns: 1fr;
}
.track-card-modern {
padding: 1.5rem;
}
.track-card-content {
flex-direction: column;
gap: 1.5rem;
}
.track-main-info {
flex-direction: column;
gap: 1rem;
}
.track-icon {
width: 60px;
height: 60px;
min-width: 60px;
font-size: 1.5rem;
}
.track-name {
font-size: 1.3rem;
}
.track-metadata-row {
flex-direction: column;
gap: 0.75rem;
}
.track-actions-row {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.play-btn-large {
width: 48px;
height: 48px;
font-size: 1rem;
}
.action-icon-btn {
width: 36px;
height: 36px;
font-size: 0.9rem;
}
}
/* ============================================
UTILITIES
============================================ */
.empty-state {
text-align: center;
padding: 6rem 2rem;
color: var(--text-secondary);
}
.empty-icon {
font-size: 5rem;
margin-bottom: 2rem;
opacity: 0.5;
}
.empty-title {
font-size: 2rem;
color: var(--text-primary);
margin-bottom: 1rem;
font-weight: 700;
}
.empty-description {
font-size: 1.2rem;
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
/* Alerts */
.alert {
padding: 1.5rem 2rem;
border-radius: 12px;
margin: 2rem auto;
max-width: 90rem;
backdrop-filter: blur(20px);
border: 1px solid;
}
.alert-success {
background: rgba(72, 187, 120, 0.1);
border-color: rgba(72, 187, 120, 0.3);
color: #48bb78;
}
.alert-error {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
/* ============================================
MODAL BASE STYLES
============================================ */
.modal {
display: none !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.modal-content {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--bg-light) 100%);
border: 1px solid var(--border);
border-radius: 24px;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
backdrop-filter: blur(20px);
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
color: var(--text-primary);
}
/* Edit Modal Form Styles */
#editTrackModal .form-group {
margin-bottom: 1.5rem;
}
#editTrackModal .form-label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-primary);
font-weight: 600;
font-size: 0.95rem;
}
#editTrackModal .form-input {
width: 100%;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.95rem;
transition: all 0.3s ease;
box-sizing: border-box;
}
#editTrackModal .form-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
background: rgba(255, 255, 255, 0.08);
}
#editTrackModal .form-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
#editTrackModal textarea.form-input {
resize: vertical;
min-height: 120px;
font-family: inherit;
}
#editTrackModal .form-label input[type="checkbox"] {
margin-right: 0.5rem;
width: auto;
cursor: pointer;
}
#editTrackModal .form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#editTrackModal .btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
#editTrackModal .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
#editTrackModal .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
#editTrackModal .btn:not(.btn-primary) {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.2);
}
#editTrackModal .btn:not(.btn-primary):hover {
background: rgba(255, 255, 255, 0.15);
}
#editTrackModal h2 {
color: var(--text-primary);
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
#editTrackModal .btn i {
font-size: 0.9rem;
}
/* ============================================
VARIATIONS MODAL
============================================ */
.variations-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
padding: 2rem;
}
.variations-modal.active {
display: flex;
}
/* ============================================
LYRICS MODAL
============================================ */
.lyrics-modal {
display: none !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.lyrics-modal.active {
display: flex !important;
}
/* ============================================
DOWNLOAD MODAL
============================================ */
.download-modal {
display: none !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.download-modal.active {
display: flex !important;
}
.variations-content {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--bg-light) 100%);
border: 1px solid var(--border);
border-radius: 24px;
max-width: 90rem;
max-height: 90vh;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
backdrop-filter: blur(20px);
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
}
.variations-header {
background: linear-gradient(135deg, var(--primary), var(--secondary));
padding: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.variations-title {
color: white;
font-size: 1.8rem;
font-weight: 800;
margin: 0;
}
.close-variations {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-variations:hover {
background: rgba(255, 255, 255, 0.3);
transform: rotate(90deg);
}
.variations-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
padding: 2rem;
overflow-y: auto;
flex: 1;
}
.variation-card {
background: var(--glass);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.variation-card:hover {
transform: translateY(-4px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 12px 32px var(--shadow);
}
.variation-card.selected {
border-color: var(--primary);
background: rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 2px var(--primary);
}
.variation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.variation-title {
font-weight: 700;
color: var(--text-primary);
font-size: 1.1rem;
}
.variation-index {
background: var(--primary);
color: white;
padding: 0.3rem 0.6rem;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 700;
}
.variation-duration {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
}
.variation-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.variation-btn {
background: var(--glass);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 0.6rem 1rem;
border-radius: 10px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.variation-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
}
.variation-btn.play {
background: linear-gradient(135deg, var(--primary), var(--secondary));
border: none;
color: white;
}
.variation-btn.download {
background: linear-gradient(135deg, var(--success), #38a169);
border: none;
color: white;
}
.variation-btn.select {
background: linear-gradient(135deg, var(--accent), #d97706);
border: none;
color: white;
}
.variation-btn.select.selected {
background: linear-gradient(135deg, var(--success), #38a169);
}
.variations-footer {
padding: 2rem;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.variations-info {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1.5rem;
display: flex;
align-items: flex-start;
gap: 0.5rem;
line-height: 1.6;
}
.variations-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.variations-btn {
padding: 0.9rem 2rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
border: none;
font-size: 1rem;
}
.variations-btn.cancel {
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--border);
}
.variations-btn.cancel:hover {
background: rgba(255, 255, 255, 0.1);
}
.variations-btn.save {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
}
.variations-btn.save:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
}
.variations-btn.save:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
</style>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<div class="hero-content">
<div class="hero-badge">π΅ <?= t('library.hero_badge') ?></div>
<h1 class="hero-title"><?= t('library.hero_title') ?></h1>
<p class="hero-subtitle"><?= t('library.hero_subtitle') ?></p>
</div>
</div>
</section>
<!-- Success/Error Messages -->
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success" style="margin: 1rem; padding: 1rem; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 8px;">
<?= htmlspecialchars($_SESSION['success_message']) ?>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-error" style="margin: 1rem; padding: 1rem; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 8px;">
<?= htmlspecialchars($_SESSION['error_message']) ?>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
<!-- Community Content -->
<section class="community-content">
<div class="community-container">
<!-- Quick Actions -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; margin-bottom: 4rem;">
<a href="/#create" style="background: rgba(255, 255, 255, 0.05); padding: 3rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; text-align: center; text-decoration: none; color: white;">
<div style="font-size: 3rem; color: #667eea; margin-bottom: 1.5rem;">π΅</div>
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem; color: white;"><?= t('library.create_music') ?></div>
<div style="color: #a0aec0; font-size: 1.4rem; margin-bottom: 1.5rem; line-height: 1.5;"><?= t('library.create_music_desc') ?></div>
<div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 12px; font-weight: 600; font-size: 1.3rem; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 0.8rem;">
<i class="fas fa-plus"></i> <?= t('btn.start_creating') ?>
</div>
</a>
<a href="/artists.php" style="background: rgba(255, 255, 255, 0.05); padding: 3rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; text-align: center; text-decoration: none; color: white;">
<div style="font-size: 3rem; color: #667eea; margin-bottom: 1.5rem;">π₯</div>
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem; color: white;"><?= t('library.discover_artists') ?></div>
<div style="color: #a0aec0; font-size: 1.4rem; margin-bottom: 1.5rem; line-height: 1.5;"><?= t('library.discover_artists_desc') ?></div>
<div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 12px; font-weight: 600; font-size: 1.3rem; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 0.8rem;">
<i class="fas fa-users"></i> <?= t('library.browse_artists') ?>
</div>
</a>
</div>
<!-- Library Stats -->
<div class="community-stats">
<div class="stat-card">
<div class="stat-number"><?= $user_stats['total_tracks'] ?></div>
<div class="stat-label"><?= t('library.total_tracks') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #10b981;">+<?= $user_stats['completed_tracks'] ?> <?= t('library.completed') ?></div>
</div>
<div class="stat-card">
<div class="stat-number"><?= round(($user_stats['total_duration'] ?? 0) / 60, 1) ?></div>
<div class="stat-label"><?= t('library.total_minutes') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #a0aec0;"><?= $user_stats['total_duration'] ?? 0 ?> <?= t('library.seconds') ?></div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $credits ?></div>
<div class="stat-label"><?= t('library.credits_left') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #a0aec0;"><?= ucfirst($plan) ?> <?= t('library.plan') ?></div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $user_level ?></div>
<div class="stat-label"><?= t('library.creator_level') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #10b981;"><?= $xp ?> XP</div>
</div>
</div>
<!-- Progress & Activity Section -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; margin-bottom: 4rem;">
<!-- Progress Overview -->
<div style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1);">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin-bottom: 2rem;"><?= t('library.progress_overview') ?></h3>
<div style="margin-bottom: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;"><?= t('library.completion_rate') ?></span>
<span style="font-size: 1.4rem; color: #a0aec0;"><?php echo $user_stats['total_tracks'] > 0 ? round(($user_stats['completed_tracks'] / $user_stats['total_tracks']) * 100) : 0; ?>%</span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: <?php echo $user_stats['total_tracks'] > 0 ? ($user_stats['completed_tracks'] / $user_stats['total_tracks']) * 100 : 0; ?>%;"></div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;"><?= t('library.next_level') ?></span>
<span style="font-size: 1.4rem; color: #a0aec0;"><?php echo $user_level < 5 ? $user_level + 1 : t('library.max'); ?></span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<?php
$next_level_xp = $user_level < 5 ? ($user_level * 200) : 1000;
$progress = min(100, ($xp / $next_level_xp) * 100);
?>
<div style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: <?php echo $progress; ?>%;"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;"><?= t('library.credits_usage') ?></span>
<span style="font-size: 1.4rem; color: #a0aec0;"><?php echo $credits; ?> <?= t('library.remaining') ?></span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: <?php echo max(0, 100 - ($credits / 5) * 100); ?>%;"></div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1);">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin-bottom: 2rem;"><?= t('library.recent_activity') ?></h3>
<?php if (empty($recent_activity)): ?>
<div style="text-align: center; padding: 2rem 0; color: #a0aec0;">
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">π</div>
<h4 style="font-size: 1.6rem; color: white; margin-bottom: 0.5rem;"><?= t('library.no_activity') ?></h4>
<p style="font-size: 1.2rem; color: #a0aec0;"><?= t('library.no_activity_desc') ?></p>
</div>
<?php else: ?>
<?php foreach (array_slice($recent_activity, 0, 5) as $activity): ?>
<div style="display: flex; align-items: center; gap: 1.5rem; padding: 1.5rem 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="width: 4rem; height: 4rem; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; font-size: 1.6rem; color: white;">π΅</div>
<div style="flex: 1;">
<div style="font-size: 1.4rem; font-weight: 600; color: white; margin-bottom: 0.5rem;"><?php echo htmlspecialchars($activity['title']); ?></div>
<div style="font-size: 1.2rem; color: #a0aec0;"><?php echo date('M j, g:i A', strtotime($activity['created_at'])); ?></div>
</div>
<div style="padding: 0.5rem 1rem; border-radius: 20px; font-size: 1.2rem; font-weight: 600; background: <?php echo $activity['status'] === 'complete' ? 'rgba(16, 185, 129, 0.2)' : ($activity['status'] === 'processing' ? 'rgba(245, 158, 11, 0.2)' : 'rgba(239, 68, 68, 0.2)'); ?>; color: <?php echo $activity['status'] === 'complete' ? '#10b981' : ($activity['status'] === 'processing' ? '#f59e0b' : '#ef4444'); ?>;">
<?php echo ucfirst($activity['status']); ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- Credit History -->
<?php if (!empty($credit_history)): ?>
<div style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 4rem;">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin-bottom: 2rem;"><?= t('library.credit_history') ?></h3>
<?php foreach ($credit_history as $transaction): ?>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="flex: 1;">
<div style="font-size: 1.4rem; font-weight: 600; color: white; margin-bottom: 0.5rem;"><?php echo htmlspecialchars($transaction['description'] ?? 'Credit transaction'); ?></div>
<div style="font-size: 1.2rem; color: #a0aec0;"><?php echo date('M j, g:i A', strtotime($transaction['created_at'])); ?></div>
</div>
<div style="font-size: 1.6rem; font-weight: 700; color: <?php echo $transaction['amount'] > 0 ? '#10b981' : '#ef4444'; ?>;">
<?php echo $transaction['amount'] > 0 ? '+' : ''; ?><?php echo $transaction['amount']; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Unified Filter Controls -->
<div class="unified-filters">
<!-- Search Bar -->
<div class="filter-row" style="margin-bottom: 1rem;">
<div class="filter-group" style="flex: 1; max-width: 100%;">
<label class="filter-label"><?= t('library.search_tracks') ?></label>
<input type="text"
name="search"
id="trackSearch"
class="filter-select"
placeholder="<?= t('library.search_placeholder') ?>"
value="<?= htmlspecialchars($_GET['search'] ?? '') ?>"
style="width: 100%; padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 10px; color: white; font-size: 0.95rem;">
</div>
</div>
<div class="filter-row">
<div class="filter-group">
<label class="filter-label"><?= t('library.status') ?></label>
<select name="status" class="filter-select">
<option value="all" <?= $status_filter === 'all' ? 'selected' : '' ?>><?= t('library.all_tracks') ?></option>
<option value="complete" <?= $status_filter === 'complete' ? 'selected' : '' ?>><?= t('library.completed_tracks') ?></option>
<option value="processing" <?= $status_filter === 'processing' ? 'selected' : '' ?>><?= t('library.processing_tracks') ?></option>
<option value="failed" <?= $status_filter === 'failed' ? 'selected' : '' ?>><?= t('library.failed_tracks') ?></option>
</select>
</div>
<div class="filter-group">
<label class="filter-label"><?= t('library.sort_by') ?></label>
<select name="sort" class="filter-select">
<option value="latest" <?= $sort_filter === 'latest' ? 'selected' : '' ?>><?= t('library.latest_first') ?></option>
<option value="oldest" <?= $sort_filter === 'oldest' ? 'selected' : '' ?>><?= t('library.oldest_first') ?></option>
<option value="popular" <?= $sort_filter === 'popular' ? 'selected' : '' ?>><?= t('library.most_popular') ?></option>
<option value="most-played" <?= $sort_filter === 'most-played' ? 'selected' : '' ?>><?= t('library.most_played') ?></option>
</select>
</div>
<div class="filter-group">
<label class="filter-label"><?= t('library.time') ?></label>
<select name="time" class="filter-select">
<option value="all" <?= $time_filter === 'all' ? 'selected' : '' ?>><?= t('library.all_time') ?></option>
<option value="today" <?= $time_filter === 'today' ? 'selected' : '' ?>><?= t('library.today') ?></option>
<option value="week" <?= $time_filter === 'week' ? 'selected' : '' ?>><?= t('library.this_week') ?></option>
<option value="month" <?= $time_filter === 'month' ? 'selected' : '' ?>><?= t('library.this_month') ?></option>
</select>
</div>
<div class="filter-group">
<label class="filter-label"><?= t('library.genre') ?></label>
<select name="genre" class="filter-select">
<option value=""><?= t('library.all_genres') ?> (<?= count($available_genres) ?>)</option>
<?php foreach ($available_genres as $genre): ?>
<option value="<?= htmlspecialchars($genre) ?>" <?= $genre_filter === $genre ? 'selected' : '' ?>>
<?= htmlspecialchars($genre) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<!-- Tracks Grid -->
<div class="tracks-grid">
<?php if (empty($recent_tracks)): ?>
<div style="grid-column: 1 / -1; text-align: center; padding: 4rem;">
<h3><?= t('library.no_tracks_found') ?></h3>
<p><?= t('library.no_tracks_desc') ?></p>
</div>
<?php else: ?>
<!-- Library Tracks Grid -->
<div class="tracks-grid">
<?php if (empty($tracks_with_variations)): ?>
<div class="empty-state">
<div class="empty-icon">π΅</div>
<?php if ($status_filter === 'processing'): ?>
<h2 class="empty-title"><?= t('library.no_processing') ?></h2>
<p class="empty-description">
<?= t('library.no_processing_desc') ?>
</p>
<a href="?status=all" class="create-first-btn">
<i class="fas fa-list"></i>
<?= t('library.view_all_tracks') ?>
</a>
<?php elseif ($status_filter === 'failed'): ?>
<h2 class="empty-title"><?= t('library.no_failed') ?></h2>
<p class="empty-description">
<?= t('library.no_failed_desc') ?>
</p>
<a href="?status=all" class="create-first-btn">
<i class="fas fa-list"></i>
<?= t('library.view_all_tracks') ?>
</a>
<?php elseif ($status_filter === 'complete'): ?>
<h2 class="empty-title"><?= t('library.no_completed') ?></h2>
<p class="empty-description">
<?= t('library.no_completed_desc') ?>
</p>
<a href="/#create" class="create-first-btn">
<i class="fas fa-plus"></i>
<?= t('library.create_first_track') ?>
</a>
<?php else: ?>
<h2 class="empty-title"><?= t('library.no_tracks_yet') ?></h2>
<p class="empty-description">
<?= t('library.no_tracks_yet_desc') ?>
</p>
<a href="/#create" class="create-first-btn">
<i class="fas fa-plus"></i>
<?= t('library.create_first_track') ?>
</a>
<?php endif; ?>
</div>
<?php else: ?>
<?php
// Count total tracks first to get the correct numbering
$total_tracks = count($tracks_with_variations);
$track_number = $total_tracks; // Start from the oldest track (highest number)
foreach ($tracks_with_variations as $track):
$displayTitle = $track['title'] ?: 'Untitled Track';
// Get selected variation index from metadata first (needed for duration calculation)
$trackMetadataForDuration = json_decode($track['metadata'] ?? '{}', true) ?: [];
$selectedVariationIndexForDuration = $trackMetadataForDuration['selected_variation'] ?? $track['selected_variation_index'] ?? 0;
// Get the selected variation to use its duration if available
$selectedVariationForDuration = null;
if (!empty($track['variations']) && is_array($track['variations'])) {
foreach ($track['variations'] as $variation) {
if (isset($variation['variation_index']) && $variation['variation_index'] == $selectedVariationIndexForDuration) {
$selectedVariationForDuration = $variation;
break;
}
}
if (!$selectedVariationForDuration && isset($track['variations'][$selectedVariationIndexForDuration])) {
$selectedVariationForDuration = $track['variations'][$selectedVariationIndexForDuration];
}
}
// Use selected variation's duration if available, otherwise use track's duration
$duration_seconds = $track['duration'] ?? 0;
if ($selectedVariationForDuration && !empty($selectedVariationForDuration['duration'])) {
$duration_seconds = $selectedVariationForDuration['duration'];
}
// Format duration as mm:ss
$duration_minutes = floor($duration_seconds / 60);
$duration_secs = $duration_seconds % 60;
$duration = sprintf('%d:%02d', $duration_minutes, $duration_secs);
$created_date = date('M j, Y', strtotime($track['created_at']));
$error_message = $track['error_message'] ?? null;
// Parse metadata - only use actual values, no defaults
$metadata = json_decode($track['metadata'] ?? '{}', true) ?: [];
$genre = $metadata['genre'] ?? null;
$bpm = $metadata['bpm'] ?? null;
$key = $metadata['key'] ?? null;
$mood = $metadata['mood'] ?? null;
// Extract numerical key from metadata or prompt
$numericalKey = $metadata['numerical_key'] ?? '';
if (empty($numericalKey) && !empty($track['prompt'])) {
// Try to extract from prompt if not in metadata
if (preg_match('/key[:\s]+([0-9]+[A-G]?)\s*[β\-]?\s*/i', $track['prompt'], $keyMatches)) {
$numericalKey = trim($keyMatches[1]);
}
}
?>
<?php
// Get selected variation index from metadata
$trackMetadataForAttr = json_decode($track['metadata'] ?? '{}', true) ?: [];
$selectedVariationIndexForAttr = $trackMetadataForAttr['selected_variation'] ?? $track['selected_variation_index'] ?? 0;
// Handle track image - same logic as community_fixed.php
$imageUrl = $track['image_url'] ?? null;
// Reject external URLs
if (!empty($imageUrl) && (strpos($imageUrl, 'http://') === 0 || strpos($imageUrl, 'https://') === 0)) {
$imageUrl = null;
}
// Normalize image URL format
if ($imageUrl && !preg_match('/^https?:\/\//', $imageUrl)) {
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
}
// If still empty, try metadata
if (empty($imageUrl) || $imageUrl === 'null' || $imageUrl === 'NULL') {
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($metadata['image_url']) && !empty($metadata['image_url'])) {
$metaImageUrl = $metadata['image_url'];
if (strpos($metaImageUrl, 'http://') !== 0 && strpos($metaImageUrl, 'https://') !== 0) {
if (!str_starts_with($metaImageUrl, '/')) {
$metaImageUrl = '/' . ltrim($metaImageUrl, '/');
}
$imageUrl = $metaImageUrl;
}
} elseif (isset($metadata['cover_url']) && !empty($metadata['cover_url'])) {
$metaCoverUrl = $metadata['cover_url'];
if (strpos($metaCoverUrl, 'http://') !== 0 && strpos($metaCoverUrl, 'https://') !== 0) {
if (!str_starts_with($metaCoverUrl, '/')) {
$metaCoverUrl = '/' . ltrim($metaCoverUrl, '/');
}
$imageUrl = $metaCoverUrl;
}
}
}
// Try to find image file by task_id pattern
if (empty($imageUrl) && !empty($track['task_id'])) {
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/track_covers/';
if (is_dir($uploadsDir)) {
$pattern = $uploadsDir . "track_{$track['task_id']}_*";
$files = glob($pattern);
if (!empty($files)) {
$mostRecent = end($files);
$imageUrl = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
// Fallback to default
if (empty($imageUrl)) {
$imageUrl = '/assets/images/default-track.jpg';
}
}
?>
<div class="track-card-modern" data-track-id="<?= $track['id'] ?>" data-status="<?= $track['status'] ?>" data-variations="<?= htmlspecialchars(json_encode($track['variations'] ?? [])) ?>" data-selected-variation="<?= $selectedVariationIndexForAttr ?>" data-prompt="<?= htmlspecialchars($track['prompt'] ?? '') ?>">
<!-- Status Badge (Top Right) -->
<?php if ($track['is_public'] ?? 0): ?>
<div class="track-status-badge"><?= t('library.card.public') ?></div>
<?php else: ?>
<div class="track-status-badge private"><?= t('library.card.private') ?></div>
<?php endif; ?>
<!-- Track Content -->
<div class="track-card-content">
<!-- Track Image/Icon & Info -->
<div class="track-main-info">
<!-- Track Image or Icon -->
<?php if ($imageUrl && $imageUrl !== '/assets/images/default-track.jpg'): ?>
<div class="track-image-container">
<img src="<?= htmlspecialchars($imageUrl) ?>" alt="<?= htmlspecialchars($displayTitle) ?>" class="track-image" loading="lazy">
</div>
<?php else: ?>
<div class="track-icon">
<i class="fas fa-music"></i>
</div>
<?php endif; ?>
<!-- Track Details -->
<div class="track-details">
<h3 class="track-name"><a href="/track.php?id=<?= $track['id'] ?>" class="track-title-link"><?= htmlspecialchars($displayTitle) ?></a></h3>
<p class="track-artist-name"><?= t('library.card.by') ?> <a href="/artist_profile.php?id=<?= $track['user_id'] ?>" class="artist-name-link"><?= htmlspecialchars($user_name) ?></a></p>
<?php if ($track['prompt']): ?>
<p class="track-description" title="<?= htmlspecialchars($track['prompt']) ?>">
<strong><?= t('library.card.original_prompt') ?></strong> <?= htmlspecialchars($track['prompt']) ?>
</p>
<?php elseif ($track['description']): ?>
<p class="track-description"><?= htmlspecialchars($track['description']) ?></p>
<?php endif; ?>
<!-- Metadata Row - Only show values that exist in metadata -->
<div class="track-metadata-row">
<?php if (!empty($duration)): ?>
<span class="meta-item"><strong><?= t('library.card.duration') ?></strong> <?= $duration ?></span>
<?php endif; ?>
<?php if (!empty($bpm)): ?>
<span class="meta-item"><strong><?= t('library.card.bpm') ?></strong> <?= $bpm ?> BPM</span>
<?php endif; ?>
<?php if (!empty($genre)): ?>
<span class="meta-item"><strong><?= t('library.card.genre') ?></strong> <?= htmlspecialchars($genre) ?></span>
<?php endif; ?>
<?php if (!empty($mood)): ?>
<span class="meta-item"><strong><?= t('library.card.mood') ?></strong> <?= htmlspecialchars($mood) ?></span>
<?php endif; ?>
<?php if (!empty($numericalKey)): ?>
<span class="meta-item"><strong><?= t('library.card.key') ?></strong> <?= htmlspecialchars($numericalKey) ?></span>
<?php endif; ?>
<?php if (!empty($key)): ?>
<span class="meta-item"><strong><?= !empty($numericalKey) ? t('library.card.musical_key') : t('library.card.key') ?></strong> <?= htmlspecialchars($key) ?></span>
<?php endif; ?>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="track-actions-row">
<?php if ($track['status'] === 'complete'): ?>
<?php
// Get the selected variation if it exists
$selectedVariation = null;
// Extract selected_variation_index from metadata (stored as 'selected_variation' in metadata JSON)
$trackMetadata = json_decode($track['metadata'] ?? '{}', true) ?: [];
$selectedVariationIndex = $trackMetadata['selected_variation'] ?? $track['selected_variation_index'] ?? 0;
// Get the selected variation from the variations array
if (!empty($track['variations']) && is_array($track['variations'])) {
// Find variation by variation_index (not array index)
foreach ($track['variations'] as $variation) {
if (isset($variation['variation_index']) && $variation['variation_index'] == $selectedVariationIndex) {
$selectedVariation = $variation;
break;
}
}
// Fallback: use array index if variation_index doesn't match
if (!$selectedVariation && isset($track['variations'][$selectedVariationIndex])) {
$selectedVariation = $track['variations'][$selectedVariationIndex];
}
}
// Use selected variation's audio URL if available, otherwise use track's audio URL
$audioUrl = $track['audio_url'];
if ($selectedVariation && !empty($selectedVariation['audio_url'])) {
$audioUrl = $selectedVariation['audio_url'];
}
// Ensure audio URL is not empty
if (empty($audioUrl)) {
$audioUrl = $track['audio_url'] ?? '';
}
?>
<button class="play-btn-large" onclick="playTrackFromButton('<?= htmlspecialchars($audioUrl) ?>', '<?= htmlspecialchars($displayTitle) ?>', '<?= htmlspecialchars($user_name) ?>')"
data-audio-url="<?= htmlspecialchars($audioUrl) ?>"
data-title="<?= htmlspecialchars($displayTitle) ?>"
data-artist="<?= htmlspecialchars($user_name) ?>"
data-track-id="<?= $track['id'] ?>"
title="<?= t('library.card.play_track') ?>">
<i class="fas fa-play"></i>
</button>
<?php if ($track['variation_count'] > 0): ?>
<button class="action-icon-btn" onclick="showVariations(<?= $track['id'] ?>)" title="<?= t('library.card.view_variations') ?> (<?= $track['variation_count'] ?>)">
<i class="fas fa-layer-group"></i>
</button>
<?php endif; ?>
<button class="action-icon-btn" onclick="shareTrack(<?= $track['id'] ?>)" title="<?= t('library.card.share') ?>">
<i class="fas fa-share-alt"></i>
</button>
<button class="action-icon-btn <?= (isset($track['user_liked']) && (int)$track['user_liked'] > 0) ? 'liked' : '' ?>"
data-liked="<?= (isset($track['user_liked']) && (int)$track['user_liked'] > 0) ? '1' : '0' ?>"
onclick="toggleLike(<?= $track['id'] ?>, this)" title="<?= t('library.card.like') ?>">
<i class="fas fa-heart"></i>
</button>
<button class="action-icon-btn edit-track-btn"
data-track-id="<?= $track['id'] ?>"
data-track-title="<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>"
data-track-prompt="<?= htmlspecialchars($track['prompt'] ?? '', ENT_QUOTES) ?>"
data-track-price="<?= $track['price'] ?? 0 ?>"
data-track-public="<?= $track['is_public'] ?? 0 ?>"
title="<?= t('library.card.edit_track') ?>">
<i class="fas fa-edit"></i>
</button>
<button class="action-icon-btn" onclick="showTrackMenu(<?= $track['id'] ?>)" title="<?= t('library.card.more_options') ?>">
<i class="fas fa-ellipsis-v"></i>
</button>
<?php elseif ($track['status'] === 'processing'): ?>
<div class="processing-indicator">
<i class="fas fa-spinner fa-spin"></i>
<span><?= t('library.card.processing') ?></span>
</div>
<?php elseif ($track['status'] === 'failed'): ?>
<div class="failed-indicator">
<i class="fas fa-exclamation-triangle"></i>
<span><?= t('library.card.failed') ?></span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Error Message for Failed Tracks -->
<?php if ($track['status'] === 'failed' && $error_message): ?>
<div class="error-message-modern">
<div class="error-header">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error Details:</strong>
</div>
<div class="error-content">
<?= htmlspecialchars($error_message) ?>
</div>
</div>
<?php endif; ?>
</div>
<?php $track_number--; // Decrement track number for next track (oldest to newest) ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
</section>
</div>
<script>
console.log('π΅ Community Fixed - JavaScript loading');
// Enhanced filter function with genre support
function filterTracks() {
const sortFilter = document.getElementById('sort-filter').value;
const timeFilter = document.getElementById('time-filter').value;
const genreFilter = document.getElementById('genre-filter').value;
const url = new URL(window.location);
url.searchParams.set('sort', sortFilter);
url.searchParams.set('time', timeFilter);
if (genreFilter) {
url.searchParams.set('genre', genreFilter);
} else {
url.searchParams.delete('genre');
}
// Reload page with new filters
window.location.search = url.search;
}
function filterByGenre(genre) {
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('genre', genre);
currentUrl.searchParams.delete('page'); // Reset to page 1 when filtering
// Reload page with new filters
window.location.search = currentUrl.search;
}
// Working like function with API integration (matching community_fixed.php)
function toggleLike(trackId, button) {
console.log('β€οΈ Toggling like for track:', trackId);
// Prevent double-clicks
if (button.disabled) return;
button.disabled = true;
fetch('/api/toggle_like.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({track_id: trackId})
})
.then(r => {
if (!r.ok) {
throw new Error(`HTTP error! status: ${r.status}`);
}
return r.json();
})
.then(data => {
console.log('β€οΈ Like API response:', data);
if (data.success) {
// Update button state based on API response
if (data.liked) {
button.classList.add('liked');
} else {
button.classList.remove('liked');
}
// Update like count if displayed
const countSpan = button.querySelector('span');
if (countSpan) {
countSpan.textContent = data.like_count || 0;
}
console.log('β€οΈ Like toggled successfully. Liked:', data.liked);
} else {
console.error('β€οΈ Like failed:', data.error);
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to like track', 'error');
}
}
button.disabled = false;
})
.catch(error => {
console.error('β€οΈ Like error:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to like track. Please try again.', 'error');
}
button.disabled = false;
});
}
// Share track function
function shareTrack(trackId) {
console.log('π Sharing track:', trackId);
// Get track details from the page
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.error('π Track card not found for track ID:', trackId);
if (typeof showNotification === 'function') {
showNotification('Track not found', 'error');
}
return;
}
// Get track title and artist name from the card
const trackNameElement = trackCard.querySelector('.track-name');
const trackTitle = trackNameElement?.textContent?.trim() || trackNameElement?.querySelector('a')?.textContent?.trim() || 'Track';
const artistNameElement = trackCard.querySelector('.track-artist-name');
const artistName = artistNameElement?.textContent?.replace(/^By\s+/i, '').trim() || artistNameElement?.querySelector('a')?.textContent?.trim() || 'Artist';
const trackUrl = `${window.location.origin}/track.php?id=${trackId}`;
const shareText = `π΅ Check out "${trackTitle}" by ${artistName} on SoundStudioPro!`;
// Record the share in the database
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: 'web'
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('π Share recorded for track:', trackId, 'New count:', data.share_count);
} else {
console.warn('π Share recording failed:', data.message || 'Unknown error');
}
})
.catch(error => {
console.error('π Share recording error:', error);
});
// Perform the actual share action
if (navigator.share) {
// Use Web Share API if available (mobile)
navigator.share({
title: `${trackTitle} by ${artistName}`,
text: shareText,
url: trackUrl
})
.then(() => {
console.log('π Share successful via Web Share API');
if (typeof showNotification === 'function') {
showNotification('Track shared successfully!', 'success');
}
})
.catch(error => {
console.log('π Web Share API cancelled or failed, using fallback:', error);
// Fallback to clipboard
copyToClipboard(shareText, trackUrl);
});
} else {
// Fallback: copy to clipboard
copyToClipboard(shareText, trackUrl);
}
}
// Helper function to copy to clipboard
function copyToClipboard(text, url) {
const shareData = `${text}\n${url}`;
navigator.clipboard.writeText(shareData).then(() => {
console.log('π Link copied to clipboard');
if (typeof showNotification === 'function') {
showNotification('Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!\n\n' + shareData);
}
}).catch(error => {
console.error('π Clipboard error:', error);
// Final fallback: show share data in prompt
const userInput = prompt('Share this track:\n\n', shareData);
if (userInput) {
console.log('π User copied manually');
}
});
}
// Working comments function with modal
function showComments(trackId) {
// Create and show comments modal
const modal = document.createElement('div');
modal.className = 'comments-modal';
modal.innerHTML = `
<div class="comments-overlay">
<div class="comments-container">
<div class="comments-header">
<h3>Comments</h3>
<button class="close-btn" onclick="closeComments()">Γ</button>
</div>
<div class="comments-list" id="comments-list-${trackId}">
<div class="loading">Loading comments...</div>
</div>
<div class="comment-form">
<textarea id="comment-text-${trackId}" placeholder="Write a comment..." maxlength="500"></textarea>
<button onclick="addComment(${trackId})" class="btn btn-primary">Post Comment</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Load comments
loadComments(trackId);
}
function closeComments() {
const modal = document.querySelector('.comments-modal');
if (modal) {
modal.remove();
}
}
function loadComments(trackId) {
console.log('π΅ Loading comments for track:', trackId);
fetch(`/api_social.php?action=get_comments&track_id=${trackId}`)
.then(response => {
console.log('π΅ Load comments API response status:', response.status);
return response.json();
})
.then(data => {
console.log('π΅ Load comments API response data:', data);
const commentsList = document.getElementById(`comments-list-${trackId}`);
if (data.success && data.comments && data.comments.length > 0) {
commentsList.innerHTML = data.comments.map(comment => `
<div class="comment-item">
<div class="comment-avatar">
${comment.profile_image ?
`<img src="${comment.profile_image}" alt="${comment.user_name}" onerror="this.parentElement.innerHTML='<div class=\'default-avatar-small\'>${comment.user_name.charAt(0)}</div>'">` :
`<div class="default-avatar-small">${comment.user_name.charAt(0)}</div>`
}
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-author">${comment.user_name}</span>
<span class="comment-time">${comment.created_at}</span>
</div>
<div class="comment-text">${comment.comment}</div>
</div>
</div>
`).join('');
} else {
commentsList.innerHTML = '<div class="no-comments">No comments yet. Be the first to comment!</div>';
}
})
.catch(error => {
console.warn('π΅ Load comments error:', error);
document.getElementById(`comments-list-${trackId}`).innerHTML = '<div class="error">Failed to load comments</div>';
});
}
function addComment(trackId) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to comment', 'warning');
return;
}
const textarea = document.getElementById(`comment-text-${trackId}`);
const comment = textarea.value.trim();
if (!comment) {
showNotification('Please enter a comment', 'warning');
return;
}
// Show loading state
const submitBtn = textarea.nextElementSibling;
const originalText = submitBtn.textContent;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Posting...';
submitBtn.disabled = true;
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'comment',
track_id: trackId,
comment: comment
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
textarea.value = '';
loadComments(trackId); // Reload comments
showNotification('Comment posted!', 'success');
} else {
showNotification(data.message || 'Failed to post comment', 'error');
}
})
.catch(error => {
console.warn('π΅ Comment error:', error);
showNotification('Failed to post comment. Please try again.', 'error');
})
.finally(() => {
// Restore button
submitBtn.textContent = originalText;
submitBtn.disabled = false;
});
}
// Notification system
function showNotification(message, type = 'info') {
// Remove existing notifications
const existingNotifications = document.querySelectorAll('.notification');
existingNotifications.forEach(notification => notification.remove());
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<i class="notification-icon ${getNotificationIcon(type)}"></i>
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
</div>
`;
// Add to page
document.body.appendChild(notification);
// Show animation
setTimeout(() => {
notification.classList.add('show');
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}, 5000);
}
function getNotificationIcon(type) {
switch (type) {
case 'success': return 'fas fa-check-circle';
case 'error': return 'fas fa-exclamation-circle';
case 'warning': return 'fas fa-exclamation-triangle';
default: return 'fas fa-info-circle';
}
}
// Enhanced global player initialization check
function waitForEnhancedPlayerCallback(callback, maxAttempts = 20) {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
callback();
return;
}
if (maxAttempts > 0) {
setTimeout(() => waitForEnhancedPlayerCallback(callback, maxAttempts - 1), 250);
} else {
console.warn('β οΈ Enhanced global player not available, attempting to initialize...');
showNotification('Audio player not available. Please refresh the page.', 'error');
}
}
// Enable play buttons when global player is ready
function enablePlayButtons() {
console.log('π΅ Enabling play buttons - global player ready');
// Enable all play buttons
document.querySelectorAll('.action-btn.play-btn, .play-track-btn').forEach(btn => {
btn.classList.add('ready');
btn.disabled = false;
// Remove loading state if present
const icon = btn.querySelector('i');
if (icon && icon.className.includes('fa-spinner')) {
icon.className = 'fas fa-play';
}
});
// Show ready notification
showNotification('π΅ Audio player ready!', 'success');
}
// Professional music feed functions
function playTrack(trackId, audioUrl, title, artist) {
console.log('π΅ Playing track:', { trackId, audioUrl, title, artist });
// Ensure global player is ready before attempting playback
if (!window.enhancedGlobalPlayer || typeof window.enhancedGlobalPlayer.playTrack !== 'function') {
console.warn('β Global player not available');
showNotification('Audio player not ready. Please refresh the page.', 'error');
return;
}
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('π΅ Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('β Audio URL is empty!');
showNotification('Audio file not available.', 'error');
return;
}
try {
// Call the global player's playTrack function
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist);
if (success) {
// Update UI - Mark this track as currently playing
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing', 'playing');
});
// Reset all play buttons
document.querySelectorAll('.action-btn.play-btn, .play-track-btn, .action-btn-compact.primary').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
});
const currentCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (currentCard) {
currentCard.classList.add('currently-playing', 'playing');
// Update compact play button
const compactPlayBtn = currentCard.querySelector('.action-btn.play-btn, .action-btn-compact.primary');
if (compactPlayBtn) {
compactPlayBtn.classList.add('playing');
const icon = compactPlayBtn.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
}
// Update full play button if exists
const fullPlayBtn = currentCard.querySelector('.play-track-btn');
if (fullPlayBtn) {
fullPlayBtn.classList.add('playing');
fullPlayBtn.innerHTML = '<i class="fas fa-pause"></i> <span>Playing</span>';
}
}
// Record play analytics
recordTrackPlay(trackId);
// Show notification
showNotification('π΅ Now playing: ' + title, 'success');
console.log('β
Track playing successfully');
} else {
console.warn('β Global player returned false');
showNotification('Failed to start playback. Please try again.', 'error');
}
} catch (error) {
console.warn('β Error during playback:', error);
showNotification('Playback error: ' + error.message, 'error');
}
}
function playTrackFromWaveform(trackId, audioUrl, title, artist) {
playTrack(trackId, audioUrl, title, artist);
}
function addToCart(trackId, title, price, artistPlan = 'free') {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to add tracks to cart', 'warning');
return;
}
console.log('π Adding to cart:', { trackId, title, price, artistPlan });
// Add loading state to button
const button = event.target.closest('.btn-cart');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
button.disabled = true;
// Send to cart.php via POST with artist plan info
const formData = new FormData();
formData.append('track_id', trackId);
formData.append('action', 'add');
formData.append('artist_plan', artistPlan);
fetch('cart.php', {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(responseText => {
console.log('π Raw cart response:', responseText);
let data;
try {
data = JSON.parse(responseText);
console.log('π Parsed cart response:', data);
} catch (e) {
console.warn('π Failed to parse JSON response:', e);
console.warn('π Raw response was:', responseText);
throw new Error('Invalid JSON response from cart');
}
if (!data.success) {
throw new Error(data.message || 'Failed to add to cart');
}
if (price == 0) {
// Free track - make it feel like a premium purchase experience!
showNotification(`π΅ "${title}" added to cart for FREE! π Ready to purchase and own!`, 'success');
} else {
// Paid track added to cart
const revenueInfo = (artistPlan === 'free') ? ' (Platform Revenue)' : '';
showNotification(`"${title}" added to cart! ($${price})${revenueInfo}`, 'success');
}
// Update cart UI if there's a cart counter
const cartCounter = document.querySelector('.cart-count, .cart-counter');
if (cartCounter && data.cart_count) {
cartCounter.textContent = data.cart_count;
}
// Log debug info if available
if (data.debug) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
console.log('π Debug info:', data.debug);
}
}
// Change button to "Added" state temporarily
button.innerHTML = '<i class="fas fa-check"></i> Added!';
button.classList.add('added');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('added');
button.disabled = false;
}, 2000);
})
.catch(error => {
console.warn('π Cart error:', error);
showNotification('Failed to add to cart: ' + error.message, 'error');
// Restore button
button.innerHTML = originalText;
button.disabled = false;
});
}
function toggleFollow(userId, button) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to follow artists', 'warning');
return;
}
console.log('π€ Toggling follow for user:', userId);
// Add loading state
button.style.pointerEvents = 'none';
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'follow', user_id: userId })
})
.then(response => {
console.log('π€ Follow API response status:', response.status);
return response.json();
})
.then(data => {
console.log('π€ Follow API response data:', data);
if (data.success) {
button.classList.toggle('following');
const isFollowing = button.classList.contains('following');
button.innerHTML = `<i class="fas fa-user-${isFollowing ? 'check' : 'plus'}"></i> ${isFollowing ? 'Following' : 'Follow'}`;
// Show success notification
const action = isFollowing ? 'followed' : 'unfollowed';
showNotification(`Artist ${action}!`, 'success');
} else {
showNotification(data.message || 'Failed to follow artist', 'error');
}
})
.catch(error => {
console.warn('π€ Follow error:', error);
showNotification('Failed to follow artist. Please try again.', 'error');
})
.finally(() => {
// Restore button
button.style.pointerEvents = 'auto';
if (!button.innerHTML.includes('Following') && !button.innerHTML.includes('Follow')) {
button.innerHTML = originalText;
}
});
}
// Track play count functionality
function recordTrackPlay(trackId) {
// Only record if not already recorded recently
const lastPlayed = sessionStorage.getItem(`played_${trackId}`);
const now = Date.now();
if (!lastPlayed || (now - parseInt(lastPlayed)) > 30000) { // 30 seconds minimum between plays
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'play', track_id: trackId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
sessionStorage.setItem(`played_${trackId}`, now.toString());
console.log('π΅ Play count recorded for track:', trackId);
}
})
.catch(error => {
console.warn('π΅ Play count error:', error);
});
}
}
// Play button functionality is now handled by inline onclick="togglePlayPause()" calls
// This prevents conflicts and ensures proper parameter passing
document.addEventListener('DOMContentLoaded', function() {
console.log('π΅ Community Fixed - Initialized (play buttons use inline handlers)');
// Initialize like button states from server data
// The buttons are already set with the 'liked' class from PHP, but we verify here
document.querySelectorAll('.action-icon-btn[onclick*="toggleLike"]').forEach(button => {
const onclickAttr = button.getAttribute('onclick');
const trackIdMatch = onclickAttr.match(/toggleLike\((\d+)/);
if (trackIdMatch) {
const trackId = parseInt(trackIdMatch[1]);
// Verify the like state matches the server state
// The button should already have the 'liked' class if user_liked > 0
if (button.classList.contains('liked')) {
console.log('β€οΈ Like button initialized as liked for track:', trackId);
}
}
});
});
// π VIRAL TRACK SHARING SYSTEM
function showShareModal(trackId, title, artist) {
const trackUrl = `https://soundstudiopro.com/track/${trackId}`;
const shareText = `π΅ Check out "${title}" by ${artist}`;
// Update share URL input
document.getElementById('shareUrl').value = trackUrl;
// Update social share links
const platforms = {
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(trackUrl)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(trackUrl)}`,
whatsapp: `https://wa.me/?text=${encodeURIComponent(shareText + ' ' + trackUrl)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(trackUrl)}`,
discord: `https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(trackUrl)}&response_type=code&scope=webhook.incoming`
};
// Update platform buttons
document.querySelector('[data-platform="twitter"]').onclick = () => openShare(platforms.twitter, 'twitter', trackId);
document.querySelector('[data-platform="facebook"]').onclick = () => openShare(platforms.facebook, 'facebook', trackId);
document.querySelector('[data-platform="whatsapp"]').onclick = () => openShare(platforms.whatsapp, 'whatsapp', trackId);
document.querySelector('[data-platform="linkedin"]').onclick = () => openShare(platforms.linkedin, 'linkedin', trackId);
document.querySelector('[data-platform="discord"]').onclick = () => copyDiscordLink(trackUrl, trackId);
document.getElementById('shareModal').style.display = 'block';
document.body.style.overflow = 'hidden';
}
function openShare(url, platform, trackId) {
window.open(url, '_blank', 'width=600,height=400');
recordShare(trackId, platform);
closeShareModal();
}
function copyShareUrl() {
const shareUrl = document.getElementById('shareUrl');
shareUrl.select();
shareUrl.setSelectionRange(0, 99999);
navigator.clipboard.writeText(shareUrl.value);
const copyBtn = document.querySelector('.copy-btn');
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.classList.remove('copied');
}, 2000);
// Record share
const trackId = shareUrl.value.split('/track/')[1];
recordShare(trackId, 'copy-link');
}
function copyDiscordLink(url, text, trackId) {
const discordText = `π΅ ${text}\\n${url}`;
navigator.clipboard.writeText(discordText).then(() => {
recordShare(trackId, 'discord');
showNotification('Discord message copied! Paste it in your server π', 'success');
});
}
function copyInstagramLink(url, text, trackId) {
navigator.clipboard.writeText(url).then(() => {
recordShare(trackId, 'instagram');
showNotification('Link copied! Add it to your Instagram story πΈ', 'success');
});
}
function recordShare(trackId, platform) {
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: platform
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log(`π Share recorded: ${platform} for track ${trackId}`);
// Update share count in UI
const shareButton = document.querySelector(`[onclick*="${trackId}"] .social-count`);
if (shareButton) {
const currentCount = parseInt(shareButton.textContent);
shareButton.textContent = currentCount + 1;
}
}
})
.catch(error => {
console.warn('π Share recording error:', error);
});
}
// Handle shared track highlighting from URL
function handleSharedTrack() {
const urlParams = new URLSearchParams(window.location.search);
const trackId = urlParams.get('track');
if (trackId) {
// Find the track card
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`)?.closest('.track-card');
if (trackCard) {
// Scroll to the track
setTimeout(() => {
trackCard.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// Add highlight effect
trackCard.classList.add('highlighted');
// Optional: Auto-play the track
const playButton = trackCard.querySelector('.play-track-btn');
if (playButton) {
setTimeout(() => {
playButton.click();
}, 1000);
}
// Remove highlight after animation
setTimeout(() => {
trackCard.classList.remove('highlighted');
}, 3000);
}, 500);
showNotification('π΅ Shared track found! Playing now...', 'success');
} else {
showNotification('Track not found on this page', 'warning');
}
}
}
// Initialize shared track handling
document.addEventListener('DOMContentLoaded', function() {
handleSharedTrack();
});
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('share-modal')) {
closeShareModal();
}
});
// DJ Mixing Features
function highlightCompatibleTracks(currentBpm, currentKey) {
const tracks = document.querySelectorAll('.track-card');
tracks.forEach(track => {
const bpmElement = track.querySelector('.bpm-value');
const keyElement = track.querySelector('.key-value');
if (bpmElement && keyElement) {
const trackBpm = parseInt(bpmElement.textContent);
const trackKey = keyElement.textContent;
// BPM compatibility (within 6% for mixing)
const bpmDiff = Math.abs(trackBpm - currentBpm);
const bpmCompatible = bpmDiff <= (currentBpm * 0.06);
// Key compatibility (same key, relative major/minor, perfect 5th)
const keyCompatible = isKeyCompatible(currentKey, trackKey);
// Add visual indicators
if (bpmCompatible) {
bpmElement.parentElement.classList.add('bpm-compatible');
}
if (keyCompatible) {
keyElement.parentElement.classList.add('key-compatible');
}
if (bpmCompatible && keyCompatible) {
track.classList.add('perfect-mix-match');
}
}
});
}
function isKeyCompatible(key1, key2) {
// Simplified key compatibility - same key, relative major/minor
if (key1 === key2) return true;
// Basic major/minor relative matching
const keyMap = {
'C major': 'A minor',
'A minor': 'C major',
'G major': 'E minor',
'E minor': 'G major',
'D major': 'B minor',
'B minor': 'D major',
'A major': 'F# minor',
'F# minor': 'A major',
'E major': 'C# minor',
'C# minor': 'E major',
'F major': 'D minor',
'D minor': 'F major',
'Bb major': 'G minor',
'G minor': 'Bb major'
};
return keyMap[key1] === key2;
}
function calculateBpmRange(bpm) {
const range = Math.round(bpm * 0.06);
return {
min: bpm - range,
max: bpm + range,
half: Math.round(bpm / 2),
double: bpm * 2
};
}
// Enhanced track clicking for DJ mode
function selectTrackForDjMode(trackElement) {
// Clear previous selections
document.querySelectorAll('.track-card.dj-selected').forEach(card => {
card.classList.remove('dj-selected');
});
// Mark as selected
trackElement.classList.add('dj-selected');
// Get technical info
const bpmElement = trackElement.querySelector('.bpm-value');
const keyElement = trackElement.querySelector('.key-value');
if (bpmElement && keyElement) {
const bpm = parseInt(bpmElement.textContent);
const key = keyElement.textContent;
// Highlight compatible tracks
highlightCompatibleTracks(bpm, key);
// Show DJ info panel
showDjMixingPanel(bpm, key);
}
}
function showDjMixingPanel(bpm, key) {
const bpmRange = calculateBpmRange(bpm);
// Create or update DJ panel
let djPanel = document.getElementById('dj-mixing-panel');
if (!djPanel) {
djPanel = document.createElement('div');
djPanel.id = 'dj-mixing-panel';
djPanel.className = 'dj-mixing-panel';
document.body.appendChild(djPanel);
}
djPanel.innerHTML = `
<div class="dj-panel-header">
<h3>π§ DJ Mixing Info</h3>
<button onclick="closeDjPanel()" class="close-dj-panel">Γ</button>
</div>
<div class="dj-panel-content">
<div class="current-track-info">
<div class="dj-info-item">
<label>Current BPM:</label>
<span class="dj-bpm">${bpm}</span>
</div>
<div class="dj-info-item">
<label>Current Key:</label>
<span class="dj-key">${key}</span>
</div>
</div>
<div class="mixing-suggestions">
<h4>π― Mixing Compatibility</h4>
<div class="bpm-suggestions">
<p><strong>BPM Range:</strong> ${bpmRange.min} - ${bpmRange.max}</p>
<p><strong>Half Time:</strong> ${bpmRange.half} BPM</p>
<p><strong>Double Time:</strong> ${bpmRange.double} BPM</p>
</div>
<div class="key-suggestions">
<p><strong>Compatible Keys:</strong> Same key, relative major/minor</p>
</div>
</div>
</div>
`;
djPanel.style.display = 'block';
}
function closeDjPanel() {
const djPanel = document.getElementById('dj-mixing-panel');
if (djPanel) {
djPanel.style.display = 'none';
}
// Clear all highlighting
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('dj-selected', 'perfect-mix-match');
});
document.querySelectorAll('.bpm-compatible').forEach(el => {
el.classList.remove('bpm-compatible');
});
document.querySelectorAll('.key-compatible').forEach(el => {
el.classList.remove('key-compatible');
});
}
// Add click handlers for DJ mode
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.dj-tech-grid').forEach(grid => {
grid.addEventListener('click', function(e) {
e.preventDefault();
const trackCard = this.closest('.track-card');
selectTrackForDjMode(trackCard);
});
});
});
// Premium Rating System
function rateTrack(trackId, rating, starElement) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to rate tracks', 'warning');
return;
}
console.log('β Rating track:', trackId, 'with', rating, 'stars');
// Add loading state
const ratingContainer = starElement.closest('.star-rating');
ratingContainer.style.pointerEvents = 'none';
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'rate',
track_id: trackId,
rating: rating
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update visual rating
const stars = ratingContainer.querySelectorAll('.star');
stars.forEach((star, index) => {
star.classList.remove('filled', 'user-rated');
if (index < data.average_rating) {
star.classList.add('filled');
}
if (index < rating) {
star.classList.add('user-rated');
}
});
// Update stats
const avgElement = ratingContainer.closest('.track-rating-section').querySelector('.avg-rating');
const countElement = ratingContainer.closest('.track-rating-section').querySelector('.rating-count');
if (avgElement) avgElement.textContent = data.average_rating.toFixed(1) + '/10';
if (countElement) countElement.textContent = `(${data.rating_count} ratings)`;
showNotification(`β Rated ${rating}/10 stars!`, 'success');
} else {
showNotification(data.message || 'Failed to rate track', 'error');
}
})
.catch(error => {
console.warn('β Rating error:', error);
showNotification('Failed to rate track. Please try again.', 'error');
})
.finally(() => {
ratingContainer.style.pointerEvents = 'auto';
});
}
// Enhanced Play/Pause Logic
// Track currently playing track ID for state management
let currentlyPlayingTrackId = null;
function togglePlayPause(button, trackId, audioUrl, title, artist) {
const isCurrentlyPlaying = currentlyPlayingTrackId === trackId;
const globalPlayer = window.enhancedGlobalPlayer;
if (isCurrentlyPlaying && globalPlayer && globalPlayer.isPlaying) {
// Pause current track
globalPlayer.pause();
button.innerHTML = '<i class="fas fa-play"></i><span>Play</span>';
button.classList.remove('playing');
// Remove playing states
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing');
});
currentlyPlayingTrackId = null;
showNotification('βΈοΈ Paused', 'success');
} else {
// Play new track or resume
if (globalPlayer && typeof globalPlayer.playTrack === 'function') {
globalPlayer.playTrack(audioUrl, title, artist);
// Update all play buttons
document.querySelectorAll('.play-track-btn').forEach(btn => {
btn.classList.remove('playing');
btn.innerHTML = '<i class="fas fa-play"></i><span>Play</span>';
});
// Update current button
button.classList.add('playing');
button.innerHTML = '<i class="fas fa-pause"></i><span>Playing</span>';
// Update track cards
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing');
});
const currentCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (currentCard) {
currentCard.classList.add('currently-playing');
}
currentlyPlayingTrackId = trackId;
recordTrackPlay(trackId);
showNotification('π΅ Now playing: ' + title, 'success');
} else {
showNotification('Player not ready, please try again', 'error');
}
}
}
// View Track Charts Function
function viewTrackCharts(trackId, genre) {
// Build chart URL with track context
const chartUrl = `/charts.php?track=${trackId}&genre=${encodeURIComponent(genre)}&highlight=true`;
// Open charts in new tab or redirect
if (confirm('View this track in the global charts?')) {
window.open(chartUrl, '_blank');
}
}
// Charts Modal Functions
function showChartsModal(trackId, position, title, genre) {
document.getElementById('currentPosition').textContent = '#' + position;
document.getElementById('trackTitle').textContent = title;
document.getElementById('genreRanking').textContent = '#' + Math.floor(Math.random() * 20 + 1) + ' in ' + genre;
document.getElementById('weeklyPlays').textContent = (Math.floor(Math.random() * 1000) + 100).toLocaleString();
document.getElementById('totalRating').textContent = (Math.random() * 3 + 7).toFixed(1) + '/10';
// Random position change
const change = Math.floor(Math.random() * 20) - 10;
const changeElement = document.getElementById('positionChange');
if (change > 0) {
changeElement.textContent = 'β +' + change + ' this week';
changeElement.className = 'position-change';
} else if (change < 0) {
changeElement.textContent = 'β ' + change + ' this week';
changeElement.className = 'position-change down';
} else {
changeElement.textContent = 'β No change';
changeElement.className = 'position-change';
}
document.getElementById('chartsModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeChartsModal() {
document.getElementById('chartsModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
function openFullCharts() {
closeChartsModal();
window.open('/charts.php', '_blank');
}
// Premium Modal Functions
function showPremiumModal() {
document.getElementById('premiumModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closePremiumModal() {
document.getElementById('premiumModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
function upgradeToPremium() {
closePremiumModal();
window.location.href = '/upgrade.php';
}
// Close modals when clicking outside
window.onclick = function(event) {
const chartsModal = document.getElementById('chartsModal');
const premiumModal = document.getElementById('premiumModal');
if (event.target === chartsModal) {
closeChartsModal();
}
if (event.target === premiumModal) {
closePremiumModal();
}
}
// Toggle Lyrics Function
function toggleLyrics(trackId) {
const lyricsContent = document.getElementById('lyrics-' + trackId);
const toggleButton = lyricsContent.previousElementSibling;
const toggleSpan = toggleButton.querySelector('span');
const toggleIcon = toggleButton.querySelector('.toggle-icon');
if (lyricsContent.style.display === 'none') {
lyricsContent.style.display = 'block';
lyricsContent.classList.add('expanded');
toggleButton.classList.add('active');
toggleSpan.textContent = 'Hide Lyrics';
} else {
lyricsContent.classList.remove('expanded');
toggleButton.classList.remove('active');
toggleSpan.textContent = 'Show Lyrics';
setTimeout(() => {
lyricsContent.style.display = 'none';
}, 400);
}
}
// Add to Playlist Function
function addToPlaylist(trackId) {
// TODO: Implement playlist functionality
showNotification('π΅ Playlist feature coming soon!', 'info');
}
// Add to Cart Function
function addToCart(trackId, title, price, button) {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
fetch('/cart.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=add&track_id=${trackId}&title=${encodeURIComponent(title)}&price=${price}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(`π "${title}" added to cart!`, 'success');
// Update cart counter if it exists - use the actual count from the API response
const cartCounts = document.querySelectorAll('.cart-count, .cart-counter');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
// Show the badge if count > 0
if (data.cart_count > 0) {
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
} else {
count.style.display = 'none';
}
});
} else if (cartCounts.length > 0) {
// Fallback: manually increment if API didn't return count
cartCounts.forEach(count => {
const currentCount = parseInt(count.textContent) || 0;
count.textContent = currentCount + 1;
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
});
}
// Refresh cart modal if it's currently open
const cartModal = document.getElementById('cartModal');
if (cartModal && cartModal.style.display === 'flex') {
if (typeof refreshCartModal === 'function') {
refreshCartModal();
}
}
} else {
showNotification(data.message || 'Failed to add to cart', 'error');
}
})
.catch(error => {
console.warn('Cart error:', error);
showNotification('Network error', 'error');
})
.finally(() => {
button.disabled = false;
button.innerHTML = '<i class="fas fa-shopping-cart"></i>';
});
}
// Record Track Play
function recordTrackPlay(trackId) {
fetch('/api_social.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=play&track_id=${trackId}`
})
.catch(error => console.warn('Play tracking error:', error));
}
// Toggle Follow Function
function toggleFollow(userId, button) {
const isFollowing = button.classList.contains('following');
const action = isFollowing ? 'unfollow' : 'follow';
fetch('/api_social.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=${action}&user_id=${userId}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.classList.toggle('following');
const icon = button.querySelector('i');
const text = button.querySelector('span') || button.childNodes[1];
if (isFollowing) {
icon.className = 'fas fa-user-plus';
if (text) text.textContent = ' Follow';
showNotification('Unfollowed artist', 'info');
} else {
icon.className = 'fas fa-user-check';
if (text) text.textContent = ' Following';
showNotification('Following artist!', 'success');
}
} else {
showNotification('Failed to update follow status', 'error');
}
})
.catch(error => {
console.warn('Follow error:', error);
showNotification('Network error', 'error');
});
}
</script>
<script>
// Initialize enhanced global player integration
document.addEventListener('DOMContentLoaded', function() {
console.log('π΅ Community page loaded, waiting for enhanced global player...');
// Wait for enhanced global player to be ready, then enable play buttons
waitForEnhancedPlayerCallback(() => {
enablePlayButtons();
});
});
</script>
<!-- Include the existing modals and JavaScript functions -->
<!-- This ensures all functionality is preserved -->
<!-- Variations Modal -->
<div id="variationsModal" class="variations-modal">
<div class="variations-content">
<div class="variations-header">
<h2 class="variations-title"><?= t('library.variations.title') ?></h2>
<button class="close-variations" onclick="closeVariations()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="variationsGrid" class="variations-grid">
<!-- Variations will be loaded here -->
</div>
<div class="variations-footer">
<div class="variations-info">
<i class="fas fa-info-circle"></i>
<?= t('library.variations.info') ?>
</div>
<div class="variations-actions">
<button class="variations-btn cancel" onclick="closeVariations()">
<?= t('library.variations.cancel') ?>
</button>
<button id="saveVariationBtn" class="variations-btn save" onclick="saveVariationSelection()" disabled>
<?= t('library.variations.save_selection') ?>
</button>
</div>
</div>
</div>
</div>
<!-- Lyrics Modal -->
<div id="lyricsModal" class="lyrics-modal">
<div class="lyrics-content">
<div class="lyrics-header">
<h2 class="lyrics-title">Track Lyrics</h2>
<button class="close-lyrics" onclick="closeLyrics()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="lyricsContent" class="lyrics-body">
<!-- Lyrics will be loaded here -->
</div>
<div class="lyrics-footer">
<div class="lyrics-actions">
<button class="lyrics-btn cancel" onclick="closeLyrics()">
Close
</button>
<button class="lyrics-btn copy" onclick="copyLyrics()">
<i class="fas fa-copy"></i> Copy Lyrics
</button>
</div>
</div>
</div>
</div>
<!-- Download Modal -->
<div id="downloadModal" class="download-modal">
<div class="download-content">
<div class="download-header">
<h2 class="download-title">Download Track</h2>
<button class="close-download" onclick="closeDownload()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="downloadContent" class="download-body">
<!-- Download options will be loaded here -->
</div>
<div class="download-footer">
<div class="download-actions">
<button class="download-btn cancel" onclick="closeDownload()">
Cancel
</button>
</div>
</div>
</div>
</div>
<script>
// Translation strings for JavaScript
const libraryTranslations = {
play: '<?= addslashes(t('library.variations.play')) ?>',
download: '<?= addslashes(t('library.variations.download')) ?>',
select: '<?= addslashes(t('library.variations.select')) ?>',
selected: '<?= addslashes(t('library.variations.selected')) ?>',
variation: '<?= addslashes(t('library.variations.variation')) ?>'
};
// Global variables for variations functionality
window.trackVariations = [];
window.currentTrackId = null;
window.selectedVariationIndex = null;
// Show variations modal
function showVariations(trackId) {
console.log('π΅ Showing variations for track:', trackId);
// Get track data from PHP
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.warn('π΅ Track card not found for track ID:', trackId);
alert('Track not found. Please refresh the page and try again.');
return;
}
console.log('π΅ Track card found:', trackCard);
// Get variations data from PHP
const variationsData = trackCard.getAttribute('data-variations');
console.log('π΅ Raw variations data:', variationsData);
// Debug: Check if variations data exists
if (!variationsData) {
console.warn('π΅ No variations data attribute found');
alert('No variations data found for this track.');
return;
}
if (variationsData === '[]' || variationsData === 'null') {
console.warn('π΅ Variations data is empty');
alert('No variations available for this track.');
return;
}
try {
if (typeof window.trackVariations === 'undefined') {
window.trackVariations = [];
}
window.trackVariations = JSON.parse(variationsData);
console.log('π΅ Parsed variations:', window.trackVariations);
if (!Array.isArray(window.trackVariations) || window.trackVariations.length === 0) {
console.warn('π΅ No variations in array');
alert('No variations available for this track.');
return;
}
window.currentTrackId = trackId;
// Get main track title from the track card
const trackTitleElement = trackCard.querySelector('.track-name');
window.mainTrackTitle = trackTitleElement ? trackTitleElement.textContent.trim() : 'Untitled Track';
// Get current selection
const currentSelection = trackCard.getAttribute('data-selected-variation') || '0';
window.selectedVariationIndex = parseInt(currentSelection);
console.log('π΅ About to populate variations grid');
// Populate variations grid
populateVariationsGrid();
console.log('π΅ About to show modal');
// Show modal
const modal = document.getElementById('variationsModal');
if (!modal) {
console.warn('π΅ Variations modal not found');
alert('Modal not found. Please refresh the page and try again.');
return;
}
modal.classList.add('active');
console.log('π΅ Modal should now be visible');
// Force modal to be visible and on top
modal.style.zIndex = '999999';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
console.log('π΅ Modal styles applied:', {
zIndex: modal.style.zIndex,
position: modal.style.position,
display: modal.style.display
});
// Close on outside click (only if clicking the backdrop, not the content)
const existingHandler = modal._closeHandler;
if (existingHandler) {
modal.removeEventListener('click', existingHandler);
}
modal._closeHandler = function(e) {
// Only close if clicking the backdrop (the modal itself), not the content
if (e.target === modal) {
closeVariations();
}
};
modal.addEventListener('click', modal._closeHandler);
// Prevent content clicks from closing
const content = modal.querySelector('.variations-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
} catch (error) {
console.warn('π΅ Error parsing variations data:', error);
alert('Error loading variations. Please refresh the page and try again.');
}
}
// Populate variations grid
function populateVariationsGrid() {
const grid = document.getElementById('variationsGrid');
grid.innerHTML = '';
window.trackVariations.forEach((variation, index) => {
const card = document.createElement('div');
card.className = `variation-card ${index === window.selectedVariationIndex ? 'selected' : ''}`;
card.onclick = () => selectVariation(index);
const duration = Math.floor(variation.duration / 60) + 'm ' + Math.floor(variation.duration % 60) + 's';
const tags = variation.tags ? variation.tags.split(',').slice(0, 3) : [];
card.innerHTML = `
<div class="variation-header">
<div class="variation-title">${window.mainTrackTitle || 'Variation ' + (index + 1)}</div>
<div class="variation-index">${index + 1}</div>
</div>
<div class="variation-duration">
<i class="fas fa-clock"></i> ${duration}
</div>
${tags.length > 0 ? `
<div class="variation-tags">
${tags.map(tag => `<span class="variation-tag">${tag.trim()}</span>`).join('')}
</div>
` : ''}
<div class="variation-actions">
<button class="variation-btn play" onclick="playVariation(${index})">
<i class="fas fa-play"></i> ${libraryTranslations.play}
</button>
<button class="variation-btn download" onclick="downloadVariation(${index})">
<i class="fas fa-download"></i> ${libraryTranslations.download}
</button>
<button class="variation-btn select ${index === window.selectedVariationIndex ? 'selected' : ''}" onclick="selectVariation(${index})">
<i class="fas fa-check"></i> ${index === window.selectedVariationIndex ? libraryTranslations.selected : libraryTranslations.select}
</button>
</div>
`;
grid.appendChild(card);
});
// Update save button state
updateSaveButton();
}
// Select variation
function selectVariation(index) {
window.selectedVariationIndex = index;
// Update visual selection
document.querySelectorAll('.variation-card').forEach((card, i) => {
card.classList.toggle('selected', i === index);
});
document.querySelectorAll('.variation-btn.select').forEach((btn, i) => {
btn.classList.toggle('selected', i === index);
btn.innerHTML = `<i class="fas fa-check"></i> ${i === index ? libraryTranslations.selected : libraryTranslations.select}`;
});
updateSaveButton();
}
// Play variation
async function playVariation(index) {
const variation = window.trackVariations[index];
if (!variation) return;
console.log('π΅ Playing variation:', variation);
// Wait for enhanced global player to be ready
await waitForEnhancedPlayer();
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = variation.audio_url;
if (variation.audio_url && !variation.audio_url.startsWith('http') && !variation.audio_url.startsWith('//')) {
if (variation.audio_url.startsWith('/')) {
finalAudioUrl = window.location.origin + variation.audio_url;
} else {
finalAudioUrl = window.location.origin + '/' + variation.audio_url;
}
console.log('π΅ Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('β Variation audio URL is empty!');
showNotification('Variation audio file not available.', 'error');
return;
}
// Use the enhanced global player (same as community.php)
if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
console.log('π΅ Using enhanced global player for variation');
const trackTitle = window.mainTrackTitle || variation.title || 'Variation ' + (index + 1);
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, trackTitle, '<?= htmlspecialchars($user_name) ?>');
} else {
console.warn('π΅ Enhanced global player not available for variation playback');
alert('Audio player not available. Please refresh the page and try again.');
}
}
// Download variation
function downloadVariation(index) {
const variation = window.trackVariations[index];
if (!variation) return;
console.log('π΅ Downloading variation:', variation);
// Get track information from modal or track card
let trackTitle = 'Untitled Track';
let artistName = '<?= htmlspecialchars($user_name) ?>';
let genre = '';
let bpm = '';
let key = '';
let numericalKey = '';
let mood = '';
// Try to get track info from the track card
if (window.currentTrackId) {
const trackCard = document.querySelector(`[data-track-id="${window.currentTrackId}"]`);
if (trackCard) {
const titleEl = trackCard.querySelector('.track-name');
if (titleEl) trackTitle = titleEl.textContent.trim();
const artistEl = trackCard.querySelector('.track-artist-name');
if (artistEl) artistName = artistEl.textContent.replace('By ', '').trim();
// Get metadata from track card data attribute if available
const trackPrompt = trackCard.getAttribute('data-prompt') || '';
// Try to extract numerical key from prompt (e.g., "Key: 6B β D Major")
if (trackPrompt) {
const keyMatch = trackPrompt.match(/key[:\s]+([0-9]+[A-G]?)\s*[β\-]?\s*/i);
if (keyMatch && keyMatch[1]) {
numericalKey = keyMatch[1].trim();
}
}
// Get metadata from displayed elements
const metadataRow = trackCard.querySelector('.track-metadata-row');
if (metadataRow) {
const metaItems = metadataRow.querySelectorAll('.meta-item');
metaItems.forEach(item => {
const text = item.textContent.trim();
if (text.includes('Genre:')) {
genre = text.replace('Genre:', '').trim();
} else if (text.includes('BPM:')) {
bpm = text.replace('BPM:', '').trim();
} else if (text.includes('Key:')) {
key = text.replace('Key:', '').trim();
} else if (text.includes('Mood:')) {
mood = text.replace('Mood:', '').trim();
}
});
}
}
}
// Sanitize filename
function sanitizeFilename(str) {
return str.replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ').trim();
}
// Create comprehensive filename
const cleanTitle = sanitizeFilename(trackTitle);
const cleanArtist = sanitizeFilename(artistName);
const cleanGenre = sanitizeFilename(genre);
const cleanKey = sanitizeFilename(key);
const cleanMood = sanitizeFilename(mood);
const cleanNumericalKey = sanitizeFilename(numericalKey);
let filename = `${cleanArtist} - ${cleanTitle} (Variation ${index + 1})`;
// Add metadata to filename
const metadataParts = [];
if (cleanGenre) metadataParts.push(cleanGenre);
if (bpm) metadataParts.push(`${bpm} BPM`);
// Add numerical key first if available, then the musical key
if (cleanNumericalKey) metadataParts.push(cleanNumericalKey);
if (cleanKey) metadataParts.push(cleanKey);
if (cleanMood) metadataParts.push(cleanMood);
if (metadataParts.length > 0) {
filename += ` [${metadataParts.join(', ')}]`;
}
filename += '.mp3';
// Create a temporary link element to trigger download
const link = document.createElement('a');
link.href = variation.audio_url;
link.download = filename;
link.target = '_blank';
// Add to DOM, click, and remove
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success message
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(72, 187, 120, 0.9);
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
font-weight: 600;
z-index: 10000;
backdrop-filter: blur(10px);
`;
toast.textContent = `β
Downloading: ${filename}`;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 3000);
}
// Update save button state
function updateSaveButton() {
const saveBtn = document.getElementById('saveVariationBtn');
if (!saveBtn) {
console.warn('π΅ Save button not found');
return;
}
if (window.currentTrackId === null) {
saveBtn.disabled = true;
saveBtn.textContent = 'No Track Selected';
return;
}
const trackCard = document.querySelector(`[data-track-id="${window.currentTrackId}"]`);
if (!trackCard) {
saveBtn.disabled = true;
saveBtn.textContent = 'Track Not Found';
return;
}
const currentSelection = trackCard.getAttribute('data-selected-variation') || '0';
if (window.selectedVariationIndex !== parseInt(currentSelection)) {
saveBtn.disabled = false;
saveBtn.textContent = 'Save Selection';
} else {
saveBtn.disabled = true;
saveBtn.textContent = 'No Changes';
}
}
// Save variation selection
function saveVariationSelection() {
if (window.selectedVariationIndex === null || window.currentTrackId === null) {
console.warn('π΅ No variation or track selected');
return;
}
console.log('π΅ Saving variation selection:', { trackId: window.currentTrackId, variationIndex: window.selectedVariationIndex });
// Disable save button during request
const saveBtn = document.getElementById('saveVariationBtn');
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
fetch('/api_select_variation.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
track_id: window.currentTrackId,
variation_index: window.selectedVariationIndex
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('π΅ Variation selection saved:', data);
// Update the track card
const trackCard = document.querySelector(`[data-track-id="${window.currentTrackId}"]`);
if (trackCard) {
trackCard.setAttribute('data-selected-variation', window.selectedVariationIndex);
// Update the main audio URL and duration
const variation = window.trackVariations[window.selectedVariationIndex];
if (variation) {
// Update play button
const playBtn = trackCard.querySelector('.play-track-btn');
if (playBtn) {
playBtn.setAttribute('data-audio-url', variation.audio_url);
}
// Update duration display
const durationSpan = trackCard.querySelector('.track-details span:first-child');
if (durationSpan) {
const duration = Math.floor(variation.duration / 60) + 'm ' + Math.floor(variation.duration % 60) + 's';
durationSpan.innerHTML = `<i class="fas fa-clock"></i> ${duration}`;
}
}
}
// Show success message
if (typeof window.showNotification === 'function') {
window.showNotification('Variation selection saved successfully!', 'success');
} else {
alert('β
Variation selection saved successfully!');
}
// Close modal
closeVariations();
} else {
console.warn('π΅ Failed to save variation selection:', data.error);
if (typeof window.showNotification === 'function') {
window.showNotification('Failed to save variation selection: ' + (data.error || 'Unknown error'), 'error');
} else {
alert('β Failed to save variation selection: ' + (data.error || 'Unknown error'));
}
}
})
.catch(error => {
console.warn('π΅ Error saving variation selection:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('Network error saving selection. Please check your connection and try again.', 'error');
} else {
alert('β Network error saving selection. Please check your connection and try again.');
}
})
.finally(() => {
// Re-enable save button
saveBtn.disabled = false;
updateSaveButton();
});
}
// Close variations modal
function closeVariations() {
const modal = document.getElementById('variationsModal');
if (!modal) return;
// Remove active class
modal.classList.remove('active');
// Remove inline styles that were set in showVariations
modal.style.display = 'none';
modal.style.zIndex = '';
modal.style.position = '';
modal.style.top = '';
modal.style.left = '';
modal.style.width = '';
modal.style.height = '';
modal.style.alignItems = '';
modal.style.justifyContent = '';
// Reset variables
window.trackVariations = [];
window.currentTrackId = null;
window.selectedVariationIndex = null;
console.log('π΅ Variations modal closed');
}
// Show lyrics modal
function showLyrics(trackId) {
console.log('π΅ Showing lyrics for track:', trackId);
// Get track data
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.warn('π΅ Track card not found for track ID:', trackId);
alert('Track not found. Please refresh the page and try again.');
return;
}
console.log('π΅ Track card found:', trackCard);
// Get track title
const titleElement = trackCard.querySelector('.track-title-mini');
const trackTitle = titleElement ? titleElement.textContent : 'Unknown Track';
console.log('π΅ Track title:', trackTitle);
// Show lyrics modal
const modal = document.getElementById('lyricsModal');
const lyricsContent = document.getElementById('lyricsContent');
console.log('π΅ Modal element:', modal);
console.log('π΅ Lyrics content element:', lyricsContent);
if (!modal || !lyricsContent) {
console.warn('π΅ Modal elements not found');
alert('Modal not found. Please refresh the page and try again.');
return;
}
// For now, show placeholder lyrics since lyrics functionality needs to be implemented
lyricsContent.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<i class="fas fa-music" style="font-size: 3rem; color: #667eea; margin-bottom: 1rem;"></i>
<h3 style="color: #ffffff; margin-bottom: 1rem;">${trackTitle}</h3>
<p style="color: #a0aec0; margin-bottom: 2rem;">π΅ Lyrics feature coming soon!</p>
<div style="background: rgba(102, 126, 234, 0.1); padding: 1.5rem; border-radius: 8px; border: 1px solid rgba(102, 126, 234, 0.3);">
<p style="color: #cccccc; font-style: italic;">
"This will show the AI-generated lyrics for your track.<br>
Stay tuned for this exciting feature!"
</p>
</div>
</div>
`;
// Show modal
modal.classList.add('active');
console.log('π΅ Lyrics modal activated');
// Force modal to be visible and on top
modal.style.zIndex = '999999';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
console.log('π΅ Lyrics modal styles applied:', {
zIndex: modal.style.zIndex,
position: modal.style.position,
display: modal.style.display
});
// Close on outside click
modal.addEventListener('click', function(e) {
if (e.target === this) {
closeLyrics();
}
});
}
// Close lyrics modal
function closeLyrics() {
const modal = document.getElementById('lyricsModal');
modal.classList.remove('active');
}
// Copy lyrics to clipboard
function copyLyrics() {
const lyricsContent = document.getElementById('lyricsContent');
if (lyricsContent) {
const text = lyricsContent.textContent || lyricsContent.innerText;
navigator.clipboard.writeText(text).then(() => {
alert('β
Lyrics copied to clipboard!');
}).catch(() => {
alert('β Failed to copy lyrics. Please select and copy manually.');
});
}
}
// Download track
function downloadTrack(trackId) {
console.log('π΅ Downloading track:', trackId);
// Get track data
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.warn('π΅ Track card not found for track ID:', trackId);
alert('Track not found. Please refresh the page and try again.');
return;
}
// Show download modal
const modal = document.getElementById('downloadModal');
const downloadContent = document.getElementById('downloadContent');
if (!modal || !downloadContent) {
console.warn('π΅ Download modal elements not found');
alert('Download modal not found. Please refresh the page and try again.');
return;
}
// Get track title - use the correct class name from the track card
const titleElement = trackCard.querySelector('.track-name');
const trackTitle = titleElement ? titleElement.textContent.trim() : 'Unknown Track';
// Populate download modal
downloadContent.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<i class="fas fa-download" style="font-size: 3rem; color: #667eea; margin-bottom: 1rem;"></i>
<h3 style="color: #ffffff; margin-bottom: 1rem;">${trackTitle}</h3>
<p style="color: #a0aec0; margin-bottom: 2rem;">Choose your download option:</p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<button onclick="downloadSingleTrack(${trackId})" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 8px; cursor: pointer; margin: 0.5rem;">
<i class="fas fa-music"></i> Download Track
</button>
<button onclick="downloadAllVariations(${trackId})" style="background: linear-gradient(135deg, #48bb78, #38a169); color: white; border: none; padding: 1rem 2rem; border-radius: 8px; cursor: pointer; margin: 0.5rem;">
<i class="fas fa-layer-group"></i> Download All Variations
</button>
</div>
</div>
`;
// Show modal
modal.classList.add('active');
console.log('π΅ Download modal activated');
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === this) {
closeDownload();
}
});
// Force modal to be visible and on top
modal.style.zIndex = '999999';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
}
// Close download modal
function closeDownload() {
const modal = document.getElementById('downloadModal');
if (modal) {
modal.classList.remove('active');
// Reset any forced styles
modal.style.zIndex = '';
modal.style.position = '';
modal.style.top = '';
modal.style.left = '';
modal.style.width = '';
modal.style.height = '';
modal.style.display = '';
modal.style.alignItems = '';
modal.style.justifyContent = '';
}
}
// Download single track
function downloadSingleTrack(trackId) {
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) return;
// Get the play button which has the audio data
const playBtn = trackCard.querySelector('.action-btn-compact.primary');
if (!playBtn) {
showNotification('β Play button not found. Please try again.', 'error');
return;
}
const audioUrl = playBtn.getAttribute('data-audio-url');
const title = playBtn.getAttribute('data-title');
const artist = playBtn.getAttribute('data-artist');
if (!audioUrl) {
showNotification('β Audio URL not found. Please try again.', 'error');
return;
}
// Create download link
const link = document.createElement('a');
link.href = audioUrl;
link.download = `${artist} - ${title}.mp3`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success message
showNotification('β
Track downloaded successfully!', 'success');
closeDownload();
}
// Download all variations
function downloadAllVariations(trackId) {
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
showNotification('β Track not found.', 'error');
return;
}
const variationsData = trackCard.getAttribute('data-variations');
if (!variationsData || variationsData === '[]') {
showNotification('β No variations available for this track.', 'warning');
return;
}
try {
const variations = JSON.parse(variationsData);
if (!Array.isArray(variations) || variations.length === 0) {
showNotification('β No variations available for this track.', 'warning');
return;
}
// Get track information from the card
const trackTitleEl = trackCard.querySelector('.track-name');
const trackTitle = trackTitleEl ? trackTitleEl.textContent.trim() : 'Untitled Track';
const artistEl = trackCard.querySelector('.track-artist-name');
const artistName = artistEl ? artistEl.textContent.replace('By ', '').trim() : '<?= htmlspecialchars($user_name) ?>';
// Get metadata from the card
const metadataRow = trackCard.querySelector('.track-metadata-row');
let genre = 'Unknown';
let bpm = '';
let key = '';
let numericalKey = '';
let mood = '';
// Try to get numerical key from prompt stored in data attribute
const trackPrompt = trackCard.getAttribute('data-prompt') || '';
if (trackPrompt) {
const keyMatch = trackPrompt.match(/key[:\s]+([0-9]+[A-G]?)\s*[β\-]?\s*/i);
if (keyMatch && keyMatch[1]) {
numericalKey = keyMatch[1].trim();
}
}
if (metadataRow) {
const metaItems = metadataRow.querySelectorAll('.meta-item');
metaItems.forEach(item => {
const text = item.textContent.trim();
if (text.includes('Genre:')) {
genre = text.replace('Genre:', '').trim();
} else if (text.includes('BPM:')) {
bpm = text.replace('BPM:', '').trim();
} else if (text.includes('Key:')) {
key = text.replace('Key:', '').trim();
} else if (text.includes('Mood:')) {
mood = text.replace('Mood:', '').trim();
}
});
}
// Sanitize filename (remove invalid characters)
function sanitizeFilename(str) {
return str.replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ').trim();
}
// Download each variation with descriptive filename
variations.forEach((variation, index) => {
setTimeout(() => {
const link = document.createElement('a');
link.href = variation.audio_url;
// Create comprehensive filename with all metadata
const variationNum = index + 1;
const totalVariations = variations.length;
const cleanTitle = sanitizeFilename(trackTitle);
const cleanArtist = sanitizeFilename(artistName);
const cleanGenre = sanitizeFilename(genre);
const cleanKey = sanitizeFilename(key);
const cleanMood = sanitizeFilename(mood);
// Build filename: Artist - Title (Variation X of Y) [Genre, BPM, NumericalKey, Key, Mood].mp3
let filename = `${cleanArtist} - ${cleanTitle}`;
if (totalVariations > 1) {
filename += ` (Variation ${variationNum} of ${totalVariations})`;
}
// Add metadata to filename
const metadataParts = [];
if (cleanGenre && cleanGenre !== 'Unknown') metadataParts.push(cleanGenre);
if (bpm) metadataParts.push(`${bpm} BPM`);
// Add numerical key first if available, then the musical key
if (cleanNumericalKey) metadataParts.push(cleanNumericalKey);
if (cleanKey) metadataParts.push(cleanKey);
if (cleanMood) metadataParts.push(cleanMood);
if (metadataParts.length > 0) {
filename += ` [${metadataParts.join(', ')}]`;
}
filename += '.mp3';
link.download = filename;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, index * 500); // Stagger downloads
});
showNotification(`β
Downloading ${variations.length} variation(s) with full metadata!`, 'success');
closeDownload();
} catch (error) {
console.warn('Error downloading variations:', error);
showNotification('β Error downloading variations. Please try again.', 'error');
}
}
// Genre expansion toggle function
function toggleGenreExpansion(button, hiddenGenres) {
const trackCard = button.closest('.track-card');
const expandedGenres = trackCard.querySelector('.expanded-genres');
if (expandedGenres.style.display === 'none') {
// Show expanded genres
expandedGenres.style.display = 'flex';
button.textContent = 'Show less';
button.classList.add('expanded');
} else {
// Hide expanded genres
expandedGenres.style.display = 'none';
button.textContent = `+${hiddenGenres.length} more`;
button.classList.remove('expanded');
}
}
// Update currently playing track
function updateCurrentlyPlaying(trackId) {
// Remove currently playing class from all tracks
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing', 'playing');
});
// Reset all play buttons to play state
document.querySelectorAll('.play-btn-large, .play-track-btn, .action-btn.play-btn, .action-btn-compact.primary').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
// Update button text if it has a span
const span = btn.querySelector('span');
if (span && span.textContent.includes('Playing')) {
span.textContent = 'Play';
}
});
// Add currently playing class to the current track and update its button
if (trackId) {
const currentCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (currentCard) {
currentCard.classList.add('currently-playing', 'playing');
// Update play-btn-large button
const playBtnLarge = currentCard.querySelector('.play-btn-large');
if (playBtnLarge) {
playBtnLarge.classList.add('playing');
const icon = playBtnLarge.querySelector('i');
if (icon) {
icon.className = 'fas fa-pause';
}
}
// Update play-track-btn button
const playTrackBtn = currentCard.querySelector('.play-track-btn');
if (playTrackBtn) {
playTrackBtn.classList.add('playing');
const icon = playTrackBtn.querySelector('i');
if (icon) {
icon.className = 'fas fa-pause';
}
const span = playTrackBtn.querySelector('span');
if (span) {
span.textContent = 'Playing';
} else if (playTrackBtn.innerHTML.includes('<i')) {
playTrackBtn.innerHTML = '<i class="fas fa-pause"></i> <span>Playing</span>';
}
}
// Update compact play button
const compactPlayBtn = currentCard.querySelector('.action-btn.play-btn, .action-btn-compact.primary');
if (compactPlayBtn) {
compactPlayBtn.classList.add('playing');
const icon = compactPlayBtn.querySelector('i');
if (icon) {
icon.className = 'fas fa-pause';
}
}
}
}
}
// Enhanced play track function with playing state
async function playTrackFromButton(audioUrl, title, artist) {
console.log('π΅ Library: playTrackFromButton called with:', { audioUrl, title, artist });
if (!audioUrl) {
console.warn('No audio URL provided');
return;
}
// Wait for enhanced global player to be ready
await waitForEnhancedPlayer();
// Get track ID and button element
const trackId = getTrackIdFromAudioUrl(audioUrl);
const button = document.querySelector(`[data-audio-url="${audioUrl}"]`);
const isCurrentlyPlaying = button && button.classList.contains('playing');
// Check if this track is currently playing - if so, pause it
if (isCurrentlyPlaying) {
// Check if global player is actually playing by checking the audio element
const globalPlayer = window.enhancedGlobalPlayer;
if (globalPlayer && typeof globalPlayer.togglePlayPause === 'function') {
// Get the audio element from the global player
const audioElement = document.getElementById('globalAudioElement');
if (audioElement && !audioElement.paused && audioElement.src) {
// Check if it's the same track by comparing URLs
const currentSrc = audioElement.src;
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
}
// Normalize URLs for comparison
const normalizeUrl = (url) => url.replace(/\/$/, '').split('?')[0];
if (normalizeUrl(currentSrc) === normalizeUrl(finalAudioUrl)) {
// Same track is playing - pause it
console.log('π΅ Library: Pausing current track');
globalPlayer.togglePlayPause();
// Update button to play state
if (button) {
button.classList.remove('playing');
const icon = button.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
}
// Remove playing states from all tracks
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing', 'playing');
});
showNotification('βΈοΈ Paused', 'success');
return;
}
}
}
}
// Use the enhanced global player (same as community.php)
if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(audioUrl, title, artist);
// Update currently playing state
console.log('π΅ Library: Track ID for playing state:', trackId);
updateCurrentlyPlaying(trackId);
console.log('π΅ Library: Track sent via enhancedGlobalPlayer -', title);
} else {
console.error('Enhanced global player not available');
alert('Audio player not available. Please refresh the page and try again.');
}
}
// Get track ID from audio URL
function getTrackIdFromAudioUrl(audioUrl) {
// Find the button with this audio URL and get its track ID directly
const button = document.querySelector(`[data-audio-url="${audioUrl}"]`);
if (button) {
return button.getAttribute('data-track-id');
}
return null;
}
// Toggle generation parameters visibility
function toggleGenerationParams(header) {
const paramsGrid = header.nextElementSibling;
const toggleIcon = header.querySelector('.toggle-icon');
if (paramsGrid.style.display === 'none') {
paramsGrid.style.display = 'grid';
toggleIcon.classList.remove('fa-chevron-down');
toggleIcon.classList.add('fa-chevron-up');
} else {
paramsGrid.style.display = 'none';
toggleIcon.classList.remove('fa-chevron-up');
toggleIcon.classList.add('fa-chevron-down');
}
}
// Play track from waveform (like community_fixed.php)
function playTrackFromWaveform(trackId, audioUrl, title, artist) {
console.log('π΅ Library: playTrackFromWaveform called with:', { trackId, audioUrl, title, artist });
playTrackFromButton(audioUrl, title, artist);
}
// Wait for enhanced global player to be ready
function waitForEnhancedPlayer() {
return new Promise((resolve) => {
if (window.enhancedGlobalPlayer) {
resolve();
} else {
const checkPlayer = () => {
if (window.enhancedGlobalPlayer) {
resolve();
} else {
setTimeout(checkPlayer, 100);
}
};
checkPlayer();
}
});
}
// Other library functions
async function playTrackFromButtonElement(button) {
const audioUrl = button.getAttribute('data-audio-url');
const title = button.getAttribute('data-title');
const artist = button.getAttribute('data-artist');
if (!audioUrl) {
showNotification('β Audio URL not found. Please try again.', 'error');
return;
}
// Wait for enhanced global player to be ready
await waitForEnhancedPlayer();
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('π΅ Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('β Audio URL is empty!');
showNotification('Audio file not available.', 'error');
return;
}
// Use the enhanced global player (same as community.php)
if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist);
console.log('π΅ Library: Track sent via enhancedGlobalPlayer -', title);
} else {
console.warn('Enhanced global player not available');
alert('Audio player not available. Please refresh the page and try again.');
}
}
// Track status monitoring system
let statusCheckInterval = null;
let processingTrackIds = [];
// Initialize status checking for processing tracks
function initStatusChecking() {
// Find all processing tracks on the page
const processingTracks = document.querySelectorAll('.track-card-modern[data-status="processing"]');
processingTrackIds = Array.from(processingTracks).map(track => track.getAttribute('data-track-id'));
if (processingTrackIds.length > 0) {
console.log('π΅ Found', processingTrackIds.length, 'processing tracks, starting status monitoring...');
// Check immediately
checkAllTrackStatuses();
// Then check every 15 seconds
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
statusCheckInterval = setInterval(checkAllTrackStatuses, 15000);
} else {
// No processing tracks, stop checking
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
statusCheckInterval = null;
}
}
}
// Check all processing tracks
async function checkAllTrackStatuses() {
if (processingTrackIds.length === 0) {
initStatusChecking(); // Refresh the list
return;
}
console.log('π Checking status for', processingTrackIds.length, 'tracks...');
// Check each processing track
for (const trackId of processingTrackIds) {
await checkTrackStatus(trackId);
// Small delay to avoid overwhelming the server
await new Promise(resolve => setTimeout(resolve, 500));
}
// Update stats after checking
updateStatsBar();
}
// Check individual track status
async function checkTrackStatus(trackId) {
try {
const response = await fetch(`/api/check_track_status.php?track_id=${trackId}`);
const data = await response.json();
if (data.success && data.status) {
const trackCard = document.querySelector(`.track-card-modern[data-track-id="${trackId}"]`);
if (!trackCard) return;
const currentStatus = trackCard.getAttribute('data-status');
// If status changed, update the UI
if (data.status !== currentStatus) {
console.log(`β
Track ${trackId} status changed: ${currentStatus} β ${data.status}`);
updateTrackCardStatus(trackCard, data.status, data.data);
// Remove from processing list if no longer processing
if (data.status !== 'processing') {
processingTrackIds = processingTrackIds.filter(id => id !== trackId);
}
}
}
} catch (error) {
console.error('Error checking track status:', error);
}
}
// Update track card UI when status changes
function updateTrackCardStatus(trackCard, newStatus, trackData) {
// Update data attribute
trackCard.setAttribute('data-status', newStatus);
// Get the actions row
const actionsRow = trackCard.querySelector('.track-actions-row');
if (!actionsRow) return;
// Clear current content
actionsRow.innerHTML = '';
if (newStatus === 'complete') {
// Show play button and other actions
const audioUrl = trackData.audio_url || '';
const title = trackData.title || 'Untitled Track';
const artist = trackCard.querySelector('.track-artist-name')?.textContent.replace('By ', '') || 'Unknown';
actionsRow.innerHTML = `
<button class="play-btn-large" onclick="playTrackFromButton('${audioUrl}', '${title.replace(/'/g, "\\'")}', '${artist.replace(/'/g, "\\'")}')"
data-audio-url="${audioUrl}"
data-title="${title.replace(/'/g, "\\'")}"
data-artist="${artist.replace(/'/g, "\\'")}"
data-track-id="${trackData.id}"
title="Play Track">
<i class="fas fa-play"></i>
</button>
<button class="action-icon-btn" onclick="shareTrack(${trackData.id})" title="Share">
<i class="fas fa-share-alt"></i>
</button>
<button class="action-icon-btn ${trackData.user_liked ? 'liked' : ''}" onclick="toggleLike(${trackData.id}, this)" title="Like">
<i class="fas fa-heart"></i>
</button>
<button class="action-icon-btn edit-track-btn"
data-track-id="${trackData.id}"
data-track-title="${(trackData.title || 'Untitled Track').replace(/"/g, '"')}"
data-track-prompt="${(trackData.prompt || '').replace(/"/g, '"')}"
data-track-price="${trackData.price || 0}"
data-track-public="${trackData.is_public || 0}"
title="Edit Track">
<i class="fas fa-edit"></i>
</button>
<button class="action-icon-btn" onclick="showTrackMenu(${trackData.id})" title="More options">
<i class="fas fa-ellipsis-v"></i>
</button>
`;
// Show notification
showNotification(`π΅ "${title}" is ready to play!`, 'success');
} else if (newStatus === 'failed') {
// Show failed indicator
actionsRow.innerHTML = `
<div class="failed-indicator">
<i class="fas fa-exclamation-triangle"></i>
<span>Generation Failed</span>
</div>
`;
showNotification(`β Track generation failed`, 'error');
}
// Update stats
updateStatsBar();
}
// Update stats bar with current counts
async function updateStatsBar() {
try {
const response = await fetch('/api/get_user_stats.php');
const data = await response.json();
if (data.success && data.data) {
const stats = data.data;
// Update stat numbers - be careful with selectors!
// Stat card order: 1=Total Tracks, 2=Total Minutes, 3=Credits, 4=Creator Level
const totalTracksEl = document.querySelector('.stat-card:nth-child(1) .stat-number');
// Don't update total minutes (2nd card) - it's calculated from duration
const creditsEl = document.querySelector('.stat-card:nth-child(3) .stat-number');
if (totalTracksEl) totalTracksEl.textContent = stats.total_tracks || 0;
// Update credits from API (which gets fresh from database)
if (creditsEl && stats.credits !== undefined) {
creditsEl.textContent = stats.credits;
}
// Don't update processing_tracks - that was overwriting credits before!
}
} catch (error) {
console.error('Error updating stats bar:', error);
}
}
function retryTrack(trackId) {
console.log('Retrying track:', trackId);
// Implementation for retrying failed tracks
showNotification('Retry feature coming soon!', 'info');
}
function showFailureHelp(trackId) {
console.log('Showing failure help for track:', trackId);
// Implementation for showing failure help
showNotification('Failure help feature coming soon!', 'info');
}
function deleteFailedTrack(trackId) {
if (confirm('Are you sure you want to delete this failed track?')) {
console.log('Deleting failed track:', trackId);
// Implementation for deleting failed tracks
showNotification('Delete feature coming soon!', 'info');
}
}
// Initialize enhanced global player integration
document.addEventListener('DOMContentLoaded', function() {
console.log('π΅ Library page loaded, initializing enhanced global player...');
// Wait for enhanced global player to be ready
waitForEnhancedPlayer().then(() => {
console.log('π΅ Enhanced global player ready for library page');
// Test enhanced global player functions
console.log('π΅ Available enhanced global player functions:', {
enhancedGlobalPlayer: typeof window.enhancedGlobalPlayer,
playTrack: typeof window.enhancedGlobalPlayer?.playTrack
});
}).catch(error => {
console.warn('π΅ Enhanced global player initialization error:', error);
});
// Initialize status checking for processing tracks
initStatusChecking();
// Enhanced filter handling
const filterSelects = document.querySelectorAll('select[name="sort"], select[name="time"], select[name="genre"], select[name="status"]');
filterSelects.forEach(select => {
select.addEventListener('change', function() {
applyFilters();
});
});
// Search input with debounce
const searchInput = document.getElementById('trackSearch');
let searchTimeout;
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
applyFilters();
}, 500); // Wait 500ms after user stops typing
});
// Also trigger on Enter key
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
clearTimeout(searchTimeout);
applyFilters();
}
});
}
// Function to apply all filters
function applyFilters() {
const url = new URL(window.location);
const params = new URLSearchParams(url.search);
// Get all filter values
const status = document.querySelector('select[name="status"]')?.value || 'all';
const sort = document.querySelector('select[name="sort"]')?.value || 'latest';
const time = document.querySelector('select[name="time"]')?.value || 'all';
const genre = document.querySelector('select[name="genre"]')?.value || '';
const search = document.getElementById('trackSearch')?.value.trim() || '';
// Update parameters
if (status !== 'all') {
params.set('status', status);
} else {
params.delete('status');
}
if (sort !== 'latest') {
params.set('sort', sort);
} else {
params.delete('sort');
}
if (time !== 'all') {
params.set('time', time);
} else {
params.delete('time');
}
if (genre) {
params.set('genre', genre);
} else {
params.delete('genre');
}
if (search) {
params.set('search', search);
} else {
params.delete('search');
}
// Reset to page 1 when filters change
params.delete('page');
// Navigate to new URL with filters
window.location.search = params.toString();
}
// Close modal when clicking outside
const modal = document.getElementById('variationsModal');
if (modal) {
modal.addEventListener('click', function(e) {
if (e.target === this) {
closeVariations();
}
});
}
// All tracks loaded at once - no pagination needed
console.log('π΅ All tracks loaded - infinite scroll not needed');
});
</script>
<!-- Track Edit Modal -->
<div id="editTrackModal" class="modal">
<div class="modal-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2><?= t('library.edit.title') ?></h2>
<button onclick="closeEditModal()" class="btn">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" id="editTrackForm">
<input type="hidden" name="action" value="update_track">
<input type="hidden" name="track_id" id="editTrackId">
<div class="form-group">
<label class="form-label"><?= t('library.edit.track_title') ?></label>
<input type="text" name="title" id="editTitle" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label"><?= t('library.edit.original_prompt') ?></label>
<textarea name="description" id="editDescription" class="form-input" rows="5" placeholder="<?= htmlspecialchars(t('library.edit.prompt_placeholder')) ?>"></textarea>
</div>
<div class="form-group">
<label class="form-label"><?= t('library.edit.price') ?></label>
<input type="number" name="price" id="editPrice" class="form-input" min="0" step="0.01" placeholder="0.00">
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" name="is_public" id="editIsPublic">
<?= t('library.edit.make_public') ?>
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><?= t('library.edit.update_track') ?></button>
<button type="button" onclick="closeEditModal()" class="btn"><?= t('library.edit.cancel') ?></button>
</div>
</form>
</div>
</div>
<script>
function editTrack(trackId, title, description, price, isPublic) {
const modal = document.getElementById('editTrackModal');
if (!modal) {
console.error('Edit modal not found');
alert('Edit modal not found. Please refresh the page.');
return;
}
document.getElementById('editTrackId').value = trackId;
document.getElementById('editTitle').value = title;
document.getElementById('editDescription').value = description || '';
document.getElementById('editPrice').value = price || '0';
document.getElementById('editIsPublic').checked = isPublic == 1;
// Override the !important rule by setting style directly with important
modal.style.setProperty('display', 'flex', 'important');
modal.style.setProperty('z-index', '10000', 'important');
}
function closeEditModal() {
const modal = document.getElementById('editTrackModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
}
}
// Ensure modal is hidden on page load and set up click outside handler
document.addEventListener('DOMContentLoaded', function() {
const editModal = document.getElementById('editTrackModal');
if (editModal) {
// Force hide on page load
editModal.style.setProperty('display', 'none', 'important');
// Close modal when clicking outside
editModal.addEventListener('click', function(event) {
if (event.target === editModal) {
closeEditModal();
}
});
}
// Set up event listeners for edit buttons using data attributes
// This handles both existing buttons and dynamically added ones
function setupEditButtons() {
document.querySelectorAll('.edit-track-btn').forEach(button => {
// Remove existing listeners to avoid duplicates
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
newButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const trackId = this.getAttribute('data-track-id');
const title = this.getAttribute('data-track-title') || 'Untitled Track';
const prompt = this.getAttribute('data-track-prompt') || '';
const price = parseFloat(this.getAttribute('data-track-price')) || 0;
const isPublic = parseInt(this.getAttribute('data-track-public')) || 0;
if (trackId) {
editTrack(trackId, title, prompt, price, isPublic);
} else {
console.error('Edit button missing track ID');
alert('Error: Track ID not found. Please refresh the page.');
}
});
});
}
// Set up initial buttons
setupEditButtons();
// Re-setup buttons when track cards are dynamically updated
// Use MutationObserver to watch for new edit buttons
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
if (node.classList && node.classList.contains('edit-track-btn')) {
setupEditButtons();
} else if (node.querySelector && node.querySelector('.edit-track-btn')) {
setupEditButtons();
}
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
</script>
<?php include 'includes/footer.php'; ?>