| | <!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 { |
| | 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; } |
| | |
| | |
| | .app-container { |
| | display: none; |
| | flex: 1; |
| | height: 100%; |
| | overflow: hidden; |
| | } |
| | .app-container.visible { display: flex; flex-direction: column; } |
| | |
| | .main-container { display: flex; flex: 1; overflow: hidden; } |
| | |
| | |
| | .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 { |
| | 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 { |
| | 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; } |
| | |
| | |
| | .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; |
| | } |
| | |
| | |
| | .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 { |
| | 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 { |
| | position: absolute; |
| | bottom: 0; left: 0; width: 100%; height: 100%; |
| | opacity: 0.1; |
| | pointer-events: none; |
| | z-index: 0; |
| | } |
| | |
| | </style> |
| | </head> |
| | <body> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <div id="app-container" class="app-container"> |
| | <div class="main-container"> |
| | |
| | <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> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <div class="context-menu" id="context-menu"> |
| | <button class="context-item" id="download-btn">Télécharger</button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <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"> |
| | |
| | </div> |
| | </div> |
| |
|
| | </main> |
| | </div> |
| |
|
| | |
| | <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(); |
| | |
| | |
| | let tracks = []; |
| | let currentTrack = null; |
| | let isGenerating = false; |
| | let audioContext, analyser, source; |
| | let currentUser = null; |
| | |
| | |
| | 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'); |
| | |
| | |
| | |
| | const urlParams = new URLSearchParams(window.location.search); |
| | const token = urlParams.get('token'); |
| | const userParam = urlParams.get('user'); |
| | |
| | if (token && userParam) { |
| | |
| | currentUser = { name: decodeURIComponent(userParam), token: token }; |
| | showApp(); |
| | } else { |
| | |
| | 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'; |
| | |
| | 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; |
| | |
| | |
| | |
| | |
| | |
| | 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(); |
| | }; |
| | |
| | |
| | durationInput.addEventListener('input', (e) => durationVal.textContent = e.target.value + 's'); |
| | |
| | |
| | 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)); |
| | } |
| | } |
| | }; |
| | |
| | |
| | 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; |
| | } |
| | } |
| | |
| | |
| | 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}`; |
| | } |
| | |
| | |
| | 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; |
| | |
| | |
| | if (currentUser) { |
| | localStorage.setItem(`cygnis_tracks_${currentUser.token}`, JSON.stringify(tracks)); |
| | } |
| | |
| | |
| | audioPlayer.src = data.audio_url; |
| | audioPlayer.play(); |
| | |
| | |
| | trackTitle.textContent = text; |
| | playerTitle.textContent = text; |
| | playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>'; |
| | lucide.createIcons(); |
| | |
| | |
| | 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%'; |
| | } |
| | }); |
| | |
| | |
| | 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> |
| |
|