music-api / index.html
Simonc-44's picture
Update index.html
bbf14ed verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CygnisAI Music</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
:root {
--bg-dark: #030014;
--panel-bg: #0f0f1a;
--primary: #8b5cf6;
--primary-hover: #7c3aed;
--text-main: #ffffff;
--text-muted: #94a3b8;
--border: rgba(255, 255, 255, 0.08);
--player-height: 90px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: 'Inter', sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* --- LOGIN SCREEN --- */
.login-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-dark);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background-image:
radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.1), transparent 50%);
}
.login-card {
width: 400px;
padding: 2.5rem;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(20px);
border: 1px solid var(--border);
border-radius: 24px;
text-align: center;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.login-logo {
margin-bottom: 2rem;
display: flex; justify-content: center; align-items: center; gap: 0.5rem;
font-size: 1.5rem; font-weight: 700;
}
.login-logo svg { color: var(--primary); width: 32px; height: 32px; }
.login-input {
width: 100%;
padding: 0.8rem 1rem;
margin-bottom: 1rem;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
border-radius: 8px;
color: white;
font-size: 0.95rem;
}
.login-input:focus { outline: none; border-color: var(--primary); }
.login-btn {
width: 100%;
padding: 0.8rem;
background: var(--primary);
color: black;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.login-btn:hover { background: var(--primary-hover); color: white; }
.login-error { color: #ef4444; font-size: 0.85rem; margin-top: 1rem; display: none; }
/* --- MAIN LAYOUT (HIDDEN BY DEFAULT) --- */
.app-container {
display: none; /* Hidden until login */
flex: 1;
height: 100%;
overflow: hidden;
}
.app-container.visible { display: flex; flex-direction: column; }
.main-container { display: flex; flex: 1; overflow: hidden; }
/* --- SIDEBAR --- */
.sidebar {
width: 240px;
background-color: #000000;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 2rem;
border-right: 1px solid var(--border);
}
.logo {
font-size: 1.25rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
}
.logo svg { color: var(--primary); }
.nav-menu {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
color: var(--text-muted);
text-decoration: none;
border-radius: 8px;
transition: all 0.2s;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
}
.nav-item:hover, .nav-item.active {
color: white;
background-color: rgba(255, 255, 255, 0.05);
}
.nav-item.active svg { color: var(--primary); }
/* --- CONTENT AREA --- */
.content {
flex: 1;
background: linear-gradient(180deg, #1e1b4b 0%, var(--bg-dark) 40%);
padding: 2rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2rem;
position: relative;
}
.view-section { display: none; flex-direction: column; gap: 2rem; height: 100%; }
.view-section.active { display: flex; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.user-pill {
background: rgba(0, 0, 0, 0.5);
padding: 4px 12px 4px 4px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
.user-avatar {
width: 28px; height: 28px;
background: var(--primary);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
/* --- GENERATION CARD (HOME) --- */
.generation-card {
display: flex;
gap: 2rem;
align-items: flex-end;
padding-bottom: 2rem;
}
.cover-art {
width: 232px;
height: 232px;
background: linear-gradient(135deg, #4c1d95, #db2777);
box-shadow: 0 8px 40px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.cover-art img {
width: 100%; height: 100%; object-fit: cover;
opacity: 0.8;
}
.gen-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.gen-type {
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: white;
}
.gen-title {
font-size: 4rem;
font-weight: 900;
line-height: 1;
margin: 0.5rem 0;
letter-spacing: -2px;
}
.gen-desc {
color: var(--text-muted);
font-size: 1rem;
max-width: 600px;
}
.input-area {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 600px;
}
.prompt-input {
background: rgba(255,255,255,0.1);
border: 1px solid transparent;
border-radius: 4px;
padding: 1rem;
color: white;
font-family: inherit;
font-size: 1rem;
resize: none;
height: 60px;
transition: all 0.2s;
}
.prompt-input:focus {
outline: none;
background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0.2);
}
.duration-slider {
display: flex;
align-items: center;
gap: 1rem;
color: var(--text-muted);
font-size: 0.85rem;
}
input[type="range"] {
flex: 1;
height: 4px;
background: rgba(255,255,255,0.2);
border-radius: 2px;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px; height: 12px;
background: white;
border-radius: 50%;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
.duration-slider:hover input[type="range"]::-webkit-slider-thumb { opacity: 1; }
.action-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
position: relative;
}
.btn-play {
width: 56px; height: 56px;
border-radius: 50%;
background: var(--primary);
border: none;
color: black;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
}
.btn-play:hover { transform: scale(1.05); background: var(--primary-hover); }
.btn-play:disabled { background: #333; cursor: not-allowed; transform: none; }
/* --- LIBRARY & SEARCH --- */
.track-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.track-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
gap: 1rem;
}
.track-item:hover { background: rgba(255,255,255,0.1); }
.track-item-img {
width: 40px; height: 40px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.track-item-img img { width: 100%; height: 100%; object-fit: cover; }
.track-item-info { flex: 1; }
.track-item-title { font-weight: 600; font-size: 0.95rem; color: white; }
.track-item-desc { font-size: 0.8rem; color: var(--text-muted); }
.track-item-duration { font-size: 0.85rem; color: var(--text-muted); }
.search-bar {
background: rgba(255,255,255,0.1);
border: none;
border-radius: 20px;
padding: 0.75rem 1.5rem;
color: white;
width: 100%;
max-width: 400px;
font-size: 0.9rem;
margin-bottom: 2rem;
}
/* --- MENU CONTEXTUEL --- */
.context-menu {
position: absolute;
top: 100%; left: 0;
background: #282828;
border-radius: 4px;
padding: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
display: none;
flex-direction: column;
min-width: 160px;
z-index: 200;
}
.context-menu.show { display: flex; }
.context-item {
padding: 10px 12px;
font-size: 0.85rem;
color: #eaeaea;
cursor: pointer;
border-radius: 2px;
text-align: left;
background: none; border: none;
}
.context-item:hover { background: #3e3e3e; }
/* --- PLAYER BAR --- */
.player-bar {
height: var(--player-height);
background-color: #000000;
border-top: 1px solid var(--border);
padding: 0 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
}
.track-info {
display: flex;
align-items: center;
gap: 1rem;
width: 30%;
}
.track-cover {
width: 56px; height: 56px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.track-cover img { width: 100%; height: 100%; object-fit: cover; }
.track-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.track-name { font-size: 0.9rem; font-weight: 600; color: white; }
.track-artist { font-size: 0.75rem; color: var(--text-muted); }
.player-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: 40%;
}
.control-buttons {
display: flex;
align-items: center;
gap: 1.5rem;
}
.ctrl-btn {
background: none; border: none; color: var(--text-muted); cursor: pointer; transition: color 0.2s;
}
.ctrl-btn:hover { color: white; }
.ctrl-btn.active { color: var(--primary); }
.ctrl-btn.main {
width: 32px; height: 32px;
background: white; color: black;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
.ctrl-btn.main:hover { transform: scale(1.05); color: black; }
.progress-container {
width: 100%;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.7rem;
color: var(--text-muted);
}
.progress-bar {
flex: 1;
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: white;
width: 0%;
border-radius: 2px;
}
.progress-fill.generating {
background: var(--primary);
animation: loading 1.5s infinite ease-in-out;
width: 30%;
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.volume-controls {
width: 30%;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
}
/* --- VISUALIZER --- */
#visualizer {
position: absolute;
bottom: 0; left: 0; width: 100%; height: 100%;
opacity: 0.1;
pointer-events: none;
z-index: 0;
}
</style>
</head>
<body>
<!-- LOGIN SCREEN -->
<div id="login-screen" class="login-screen">
<div class="login-card">
<div class="login-logo">
<i data-lucide="music"></i> Cygnis Music
</div>
<p style="color:#94a3b8; margin-bottom:1.5rem;">Connectez-vous avec votre compte CygnisAI pour accéder au studio.</p>
<input type="email" id="login-email" class="login-input" placeholder="Email">
<input type="password" id="login-password" class="login-input" placeholder="Mot de passe">
<button id="login-btn" class="login-btn">Se connecter</button>
<div id="login-error" class="login-error">Identifiants incorrects</div>
</div>
</div>
<!-- APP CONTAINER -->
<div id="app-container" class="app-container">
<div class="main-container">
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="logo">
<i data-lucide="music"></i> CygnisAI Music
</div>
<nav class="nav-menu">
<div class="nav-item active" onclick="showView('home')"><i data-lucide="home"></i> Accueil</div>
<div class="nav-item" onclick="showView('search')"><i data-lucide="search"></i> Rechercher</div>
<div class="nav-item" onclick="showView('library')"><i data-lucide="library"></i> Bibliothèque</div>
</nav>
<div style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 1rem;">
<div class="nav-item" onclick="showView('likes')"><i data-lucide="heart"></i> Titres likés</div>
</div>
</aside>
<!-- CONTENT -->
<main class="content">
<canvas id="visualizer"></canvas>
<div class="header">
<div style="display:flex; gap:1rem;">
<button style="background:rgba(0,0,0,0.3); border:none; border-radius:50%; width:32px; height:32px; color:white; cursor:pointer;"><i data-lucide="chevron-left"></i></button>
<button style="background:rgba(0,0,0,0.3); border:none; border-radius:50%; width:32px; height:32px; color:white; cursor:pointer;"><i data-lucide="chevron-right"></i></button>
</div>
<div class="user-pill" onclick="logout()">
<div class="user-avatar"><i data-lucide="user" size="16"></i></div>
<span id="user-name">Utilisateur</span>
<i data-lucide="log-out" size="14" style="margin-left:4px; opacity:0.7;"></i>
</div>
</div>
<!-- VIEW: HOME -->
<div id="view-home" class="view-section active">
<div class="generation-card">
<div class="cover-art" id="cover-art">
<i data-lucide="music" size="64" color="white"></i>
</div>
<div class="gen-info">
<div class="gen-type">Génération IA</div>
<h1 class="gen-title" id="track-title">Nouvelle Piste</h1>
<div class="gen-desc">Créez une musique unique en décrivant l'ambiance, le style et les instruments.</div>
<div class="input-area">
<textarea id="prompt" class="prompt-input" placeholder="Ex: Piano mélancolique sous la pluie, lo-fi hip hop..."></textarea>
<div class="duration-slider">
<span>Durée</span>
<input type="range" id="duration" min="5" max="30" value="10" step="5">
<span id="duration-val">10s</span>
</div>
</div>
<div class="action-buttons">
<button id="generate-btn" class="btn-play">
<i data-lucide="play" fill="black"></i>
</button>
<button class="ctrl-btn" id="like-btn" style="font-size:2rem;"><i data-lucide="heart"></i></button>
<button class="ctrl-btn" id="more-btn" style="font-size:2rem;"><i data-lucide="more-horizontal"></i></button>
<!-- Context Menu -->
<div class="context-menu" id="context-menu">
<button class="context-item" id="download-btn">Télécharger</button>
</div>
</div>
</div>
</div>
</div>
<!-- VIEW: LIBRARY / LIKES / SEARCH -->
<div id="view-list" class="view-section">
<h1 id="list-title" style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem;">Bibliothèque</h1>
<input type="text" id="search-input" class="search-bar" placeholder="Rechercher dans vos titres..." style="display:none;">
<div class="track-list" id="track-list">
<!-- Tracks will be injected here -->
</div>
</div>
</main>
</div>
<!-- PLAYER BAR -->
<div class="player-bar">
<div class="track-info">
<div class="track-cover">
<img src="https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=100" alt="Cover" id="mini-cover">
</div>
<div class="track-details">
<div class="track-name" id="player-title">En attente...</div>
<div class="track-artist">Cygnis AI</div>
</div>
<button class="ctrl-btn" id="player-like-btn"><i data-lucide="heart" size="16"></i></button>
</div>
<div class="player-controls">
<div class="control-buttons">
<button class="ctrl-btn"><i data-lucide="shuffle" size="16"></i></button>
<button class="ctrl-btn"><i data-lucide="skip-back" size="20"></i></button>
<button class="ctrl-btn main" id="play-pause-btn"><i data-lucide="play" fill="black" size="16"></i></button>
<button class="ctrl-btn"><i data-lucide="skip-forward" size="20"></i></button>
<button class="ctrl-btn"><i data-lucide="repeat" size="16"></i></button>
</div>
<div class="progress-container">
<span id="current-time">0:00</span>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<span id="total-time">0:00</span>
</div>
</div>
<div class="volume-controls">
<i data-lucide="mic-2" size="16" class="ctrl-btn"></i>
<i data-lucide="list-music" size="16" class="ctrl-btn"></i>
<i data-lucide="speaker" size="16" class="ctrl-btn"></i>
<div style="width:100px; height:4px; background:rgba(255,255,255,0.3); border-radius:2px; position:relative;">
<div style="width:70%; height:100%; background:white; border-radius:2px;"></div>
</div>
</div>
</div>
</div>
<audio id="audio-player" crossorigin="anonymous"></audio>
<script>
lucide.createIcons();
// --- STATE ---
let tracks = [];
let currentTrack = null;
let isGenerating = false;
let audioContext, analyser, source;
let currentUser = null;
// --- DOM ELEMENTS ---
const loginScreen = document.getElementById('login-screen');
const appContainer = document.getElementById('app-container');
const loginBtn = document.getElementById('login-btn');
const loginEmail = document.getElementById('login-email');
const loginPassword = document.getElementById('login-password');
const loginError = document.getElementById('login-error');
const userNameEl = document.getElementById('user-name');
const promptInput = document.getElementById('prompt');
const durationInput = document.getElementById('duration');
const durationVal = document.getElementById('duration-val');
const generateBtn = document.getElementById('generate-btn');
const playPauseBtn = document.getElementById('play-pause-btn');
const audioPlayer = document.getElementById('audio-player');
const progressFill = document.getElementById('progress-fill');
const trackTitle = document.getElementById('track-title');
const playerTitle = document.getElementById('player-title');
const currentTimeEl = document.getElementById('current-time');
const totalTimeEl = document.getElementById('total-time');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
const likeBtn = document.getElementById('like-btn');
const playerLikeBtn = document.getElementById('player-like-btn');
const moreBtn = document.getElementById('more-btn');
const contextMenu = document.getElementById('context-menu');
const downloadBtn = document.getElementById('download-btn');
const trackListEl = document.getElementById('track-list');
const searchInput = document.getElementById('search-input');
const listTitle = document.getElementById('list-title');
// --- AUTHENTICATION ---
// Vérifier si un token est passé dans l'URL (SSO depuis CygnisAI)
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const userParam = urlParams.get('user');
if (token && userParam) {
// Connexion automatique via token
currentUser = { name: decodeURIComponent(userParam), token: token };
showApp();
} else {
// Vérifier localStorage
const savedUser = localStorage.getItem('cygnis_music_user');
if (savedUser) {
currentUser = JSON.parse(savedUser);
showApp();
}
}
function showApp() {
loginScreen.style.display = 'none';
appContainer.classList.add('visible');
userNameEl.textContent = currentUser.name || 'Utilisateur';
// Charger les tracks sauvegardées
const savedTracks = localStorage.getItem(`cygnis_tracks_${currentUser.token}`);
if (savedTracks) tracks = JSON.parse(savedTracks);
}
loginBtn.addEventListener('click', async () => {
const email = loginEmail.value;
const password = loginPassword.value;
if (!email || !password) return;
// Simulation de connexion (car on ne peut pas appeler l'API Vercel Auth directement sans CORS)
// Dans une vraie prod, il faudrait une route API dédiée sur le Space ou un proxy CORS
// Pour la démo, on accepte tout si ça ressemble à un email
if (email.includes('@')) {
currentUser = { name: email.split('@')[0], token: 'demo_token_' + Date.now() };
localStorage.setItem('cygnis_music_user', JSON.stringify(currentUser));
showApp();
} else {
loginError.style.display = 'block';
}
});
window.logout = () => {
localStorage.removeItem('cygnis_music_user');
location.reload();
};
// --- INITIALIZATION ---
durationInput.addEventListener('input', (e) => durationVal.textContent = e.target.value + 's');
// --- NAVIGATION ---
window.showView = (view) => {
document.querySelectorAll('.view-section').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
if (view === 'home') {
document.getElementById('view-home').classList.add('active');
document.querySelector('.nav-item:nth-child(1)').classList.add('active');
} else {
document.getElementById('view-list').classList.add('active');
searchInput.style.display = view === 'search' ? 'block' : 'none';
if (view === 'search') {
listTitle.textContent = "Rechercher";
document.querySelector('.nav-item:nth-child(2)').classList.add('active');
renderTracks(tracks);
} else if (view === 'library') {
listTitle.textContent = "Bibliothèque";
document.querySelector('.nav-item:nth-child(3)').classList.add('active');
renderTracks(tracks);
} else if (view === 'likes') {
listTitle.textContent = "Titres Likés";
renderTracks(tracks.filter(t => t.liked));
}
}
};
// --- AUDIO VISUALIZER ---
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
source = audioContext.createMediaElementSource(audioPlayer);
source.connect(analyser);
analyser.connect(audioContext.destination);
analyser.fftSize = 256;
drawVisualizer();
}
}
function drawVisualizer() {
requestAnimationFrame(drawVisualizer);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;
for(let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] * 2;
const gradient = ctx.createLinearGradient(0, canvas.height, 0, canvas.height - barHeight);
gradient.addColorStop(0, 'rgba(139, 92, 246, 0)');
gradient.addColorStop(1, 'rgba(139, 92, 246, 0.2)');
ctx.fillStyle = gradient;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
// --- PLAYER LOGIC ---
const togglePlay = () => {
if (audioPlayer.src) {
if (audioPlayer.paused) {
audioPlayer.play();
playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>';
lucide.createIcons();
initAudio();
} else {
audioPlayer.pause();
playPauseBtn.innerHTML = '<i data-lucide="play" fill="black" size="16"></i>';
lucide.createIcons();
}
}
};
playPauseBtn.addEventListener('click', togglePlay);
audioPlayer.ontimeupdate = () => {
const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;
progressFill.style.width = progress + '%';
currentTimeEl.textContent = formatTime(audioPlayer.currentTime);
};
audioPlayer.onloadedmetadata = () => {
totalTimeEl.textContent = formatTime(audioPlayer.duration);
};
audioPlayer.onended = () => {
playPauseBtn.innerHTML = '<i data-lucide="play" fill="black" size="16"></i>';
lucide.createIcons();
progressFill.style.width = '0%';
};
function formatTime(seconds) {
if (!seconds) return "0:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
}
// --- GENERATION LOGIC ---
generateBtn.addEventListener('click', async () => {
const text = promptInput.value;
const duration = durationInput.value;
if (!text || isGenerating) return;
isGenerating = true;
generateBtn.disabled = true;
generateBtn.innerHTML = '<div style="width:20px;height:20px;border:2px solid black;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;"></div>';
progressFill.classList.add('generating');
playerTitle.textContent = "Génération en cours...";
trackTitle.textContent = "Composition...";
try {
const response = await fetch('/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: text, duration: parseInt(duration) })
});
if (!response.ok) throw new Error("Erreur serveur");
const data = await response.json();
if (data.audio_url) {
const newTrack = {
id: Date.now(),
title: text,
url: data.audio_url,
duration: duration,
liked: false,
date: new Date().toLocaleDateString()
};
tracks.unshift(newTrack);
currentTrack = newTrack;
// Save to local storage
if (currentUser) {
localStorage.setItem(`cygnis_tracks_${currentUser.token}`, JSON.stringify(tracks));
}
// Play
audioPlayer.src = data.audio_url;
audioPlayer.play();
// Update UI
trackTitle.textContent = text;
playerTitle.textContent = text;
playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>';
lucide.createIcons();
// Send to parent
window.parent.postMessage({
type: 'MUSIC_GENERATED',
audioUrl: window.location.origin + data.audio_url,
prompt: text
}, '*');
}
} catch (error) {
console.error(error);
playerTitle.textContent = "Erreur de génération";
trackTitle.textContent = "Erreur";
} finally {
isGenerating = false;
generateBtn.disabled = false;
generateBtn.innerHTML = '<i data-lucide="play" fill="black"></i>';
lucide.createIcons();
progressFill.classList.remove('generating');
progressFill.style.width = '0%';
}
});
// --- LIBRARY LOGIC ---
function renderTracks(trackList) {
trackListEl.innerHTML = trackList.map(track => `
<div class="track-item" onclick="playTrack(${track.id})">
<div class="track-item-img">
<img src="https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=100" alt="Cover">
</div>
<div class="track-item-info">
<div class="track-item-title">${track.title}</div>
<div class="track-item-desc">Généré le ${track.date}</div>
</div>
<div class="track-item-duration">${track.duration}s</div>
<div onclick="event.stopPropagation(); toggleLike(${track.id})">
<i data-lucide="heart" class="${track.liked ? 'text-primary fill-primary' : 'text-muted'}" size="16"></i>
</div>
</div>
`).join('');
lucide.createIcons();
}
window.playTrack = (id) => {
const track = tracks.find(t => t.id === id);
if (track) {
currentTrack = track;
audioPlayer.src = track.url;
audioPlayer.play();
playerTitle.textContent = track.title;
trackTitle.textContent = track.title;
playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>';
updateLikeButtons();
lucide.createIcons();
initAudio();
}
};
window.toggleLike = (id) => {
const track = id ? tracks.find(t => t.id === id) : currentTrack;
if (track) {
track.liked = !track.liked;
updateLikeButtons();
if (currentUser) {
localStorage.setItem(`cygnis_tracks_${currentUser.token}`, JSON.stringify(tracks));
}
if (document.getElementById('view-list').classList.contains('active')) {
const currentView = listTitle.textContent === "Titres Likés" ? 'likes' : 'library';
if (currentView === 'likes') renderTracks(tracks.filter(t => t.liked));
else renderTracks(tracks);
}
}
};
function updateLikeButtons() {
if (!currentTrack) return;
const isLiked = currentTrack.liked;
const iconClass = isLiked ? 'text-primary fill-primary' : 'text-white';
likeBtn.innerHTML = `<i data-lucide="heart" class="${iconClass}"></i>`;
playerLikeBtn.innerHTML = `<i data-lucide="heart" class="${iconClass}" size="16"></i>`;
lucide.createIcons();
}
likeBtn.addEventListener('click', () => toggleLike());
playerLikeBtn.addEventListener('click', () => toggleLike());
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = tracks.filter(t => t.title.toLowerCase().includes(query));
renderTracks(filtered);
});
moreBtn.addEventListener('click', () => {
contextMenu.classList.toggle('show');
});
downloadBtn.addEventListener('click', () => {
if (currentTrack) {
const a = document.createElement('a');
a.href = currentTrack.url;
a.download = `Cygnis_Music_${currentTrack.title}.wav`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
contextMenu.classList.remove('show');
});
document.addEventListener('click', (e) => {
if (!moreBtn.contains(e.target) && !contextMenu.contains(e.target)) {
contextMenu.classList.remove('show');
}
});
function resize() {
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight;
}
window.addEventListener('resize', resize);
resize();
const style = document.createElement('style');
style.innerHTML = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .text-primary { color: #8b5cf6; } .fill-primary { fill: #8b5cf6; }';
document.head.appendChild(style);
</script>
</body>
</html>