Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>MusiSync - Music Lyrics & Metadata Explorer</title> | |
<style> | |
/* Reset and Base Styles */ | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
:root { | |
/* Dark Theme Colors */ | |
--bg-primary: #0a0a0a; | |
--bg-secondary: #1a1a1a; | |
--bg-tertiary: #2a2a2a; | |
--bg-card: #1e1e1e; | |
--text-primary: #ffffff; | |
--text-secondary: #b3b3b3; | |
--text-muted: #6b7280; | |
--accent-primary: #1db954; | |
--accent-secondary: #1ed760; | |
--accent-tertiary: #ff6b35; | |
--border-color: #333333; | |
--shadow: rgba(0, 0, 0, 0.5); | |
--gradient-primary: linear-gradient(135deg, #1db954, #1ed760); | |
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff8e53); | |
--glass-bg: rgba(30, 30, 30, 0.8); | |
--glass-border: rgba(255, 255, 255, 0.1); | |
} | |
[data-theme="light"] { | |
/* Light Theme Colors */ | |
--bg-primary: #ffffff; | |
--bg-secondary: #f8fafc; | |
--bg-tertiary: #e2e8f0; | |
--bg-card: #ffffff; | |
--text-primary: #1a202c; | |
--text-secondary: #4a5568; | |
--text-muted: #718096; | |
--accent-primary: #1db954; | |
--accent-secondary: #1ed760; | |
--accent-tertiary: #ff6b35; | |
--border-color: #e2e8f0; | |
--shadow: rgba(0, 0, 0, 0.1); | |
--gradient-primary: linear-gradient(135deg, #1db954, #1ed760); | |
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff8e53); | |
--glass-bg: rgba(248, 250, 252, 0.8); | |
--glass-border: rgba(0, 0, 0, 0.1); | |
} | |
body { | |
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
background: var(--bg-primary); | |
color: var(--text-primary); | |
line-height: 1.6; | |
transition: all 0.3s ease; | |
min-height: 100vh; | |
background-image: | |
radial-gradient(circle at 20% 50%, rgba(29, 185, 84, 0.1) 0%, transparent 50%), | |
radial-gradient(circle at 80% 20%, rgba(255, 107, 53, 0.1) 0%, transparent 50%), | |
radial-gradient(circle at 40% 80%, rgba(29, 231, 96, 0.05) 0%, transparent 50%); | |
} | |
/* Header */ | |
.header { | |
background: var(--glass-bg); | |
backdrop-filter: blur(20px); | |
border-bottom: 1px solid var(--glass-border); | |
padding: 1rem 2rem; | |
position: sticky; | |
top: 0; | |
z-index: 100; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.logo { | |
font-size: 1.8rem; | |
font-weight: 700; | |
background: var(--gradient-primary); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
} | |
.theme-toggle { | |
background: var(--bg-card); | |
border: 1px solid var(--border-color); | |
border-radius: 50px; | |
padding: 0.5rem 1rem; | |
color: var(--text-primary); | |
cursor: pointer; | |
transition: all 0.3s ease; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
font-size: 0.9rem; | |
} | |
.theme-toggle:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 8px 25px var(--shadow); | |
} | |
/* Container */ | |
.container { | |
max-width: 1400px; | |
margin: 0 auto; | |
padding: 2rem; | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 2rem; | |
min-height: calc(100vh - 100px); | |
} | |
/* Search Section */ | |
.search-section { | |
background: var(--bg-card); | |
border-radius: 20px; | |
padding: 2rem; | |
border: 1px solid var(--border-color); | |
box-shadow: 0 10px 30px var(--shadow); | |
transition: all 0.3s ease; | |
} | |
.search-section:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 20px 40px var(--shadow); | |
} | |
.search-title { | |
font-size: 1.5rem; | |
font-weight: 600; | |
margin-bottom: 1.5rem; | |
background: var(--gradient-primary); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
} | |
.search-form { | |
display: flex; | |
flex-direction: column; | |
gap: 1rem; | |
} | |
.input-group { | |
position: relative; | |
} | |
.input-group label { | |
display: block; | |
margin-bottom: 0.5rem; | |
font-weight: 500; | |
color: var(--text-secondary); | |
} | |
.input-group input { | |
width: 100%; | |
padding: 1rem 1.5rem; | |
border: 2px solid var(--border-color); | |
border-radius: 12px; | |
background: var(--bg-secondary); | |
color: var(--text-primary); | |
font-size: 1rem; | |
transition: all 0.3s ease; | |
} | |
.input-group input:focus { | |
outline: none; | |
border-color: var(--accent-primary); | |
box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.1); | |
} | |
.autocomplete-dropdown { | |
position: absolute; | |
top: 100%; | |
left: 0; | |
right: 0; | |
background: var(--bg-card); | |
border: 1px solid var(--border-color); | |
border-radius: 8px; | |
max-height: 200px; | |
overflow-y: auto; | |
z-index: 10; | |
display: none; | |
} | |
.autocomplete-item { | |
padding: 0.75rem; | |
cursor: pointer; | |
border-bottom: 1px solid var(--border-color); | |
transition: background 0.2s ease; | |
} | |
.autocomplete-item:hover, | |
.autocomplete-item.selected { | |
background: var(--accent-primary); | |
color: white; | |
} | |
.search-btn { | |
background: var(--gradient-primary); | |
color: white; | |
border: none; | |
padding: 1rem 2rem; | |
border-radius: 12px; | |
font-size: 1rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin-top: 1rem; | |
} | |
.search-btn:hover:not(:disabled) { | |
transform: translateY(-2px); | |
box-shadow: 0 8px 25px rgba(29, 185, 84, 0.4); | |
} | |
.search-btn:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
} | |
/* Results Section */ | |
.results-section { | |
display: flex; | |
flex-direction: column; | |
gap: 1.5rem; | |
} | |
.result-card { | |
background: var(--bg-card); | |
border-radius: 16px; | |
padding: 1.5rem; | |
border: 1px solid var(--border-color); | |
box-shadow: 0 8px 25px var(--shadow); | |
transition: all 0.3s ease; | |
} | |
.result-card:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 15px 35px var(--shadow); | |
} | |
.result-card h3 { | |
font-size: 1.2rem; | |
font-weight: 600; | |
margin-bottom: 1rem; | |
color: var(--accent-primary); | |
} | |
/* Album Art Section */ | |
.album-art-card { | |
text-align: center; | |
} | |
.album-art { | |
width: 100%; | |
max-width: 300px; | |
height: auto; | |
border-radius: 12px; | |
box-shadow: 0 10px 30px var(--shadow); | |
transition: transform 0.3s ease; | |
} | |
.album-art:hover { | |
transform: scale(1.05); | |
} | |
/* Lyrics Section */ | |
.lyrics-container { | |
max-height: 400px; | |
overflow-y: auto; | |
padding: 1rem; | |
background: var(--bg-secondary); | |
border-radius: 8px; | |
white-space: pre-line; | |
line-height: 1.8; | |
} | |
.lyrics-container::-webkit-scrollbar { | |
width: 6px; | |
} | |
.lyrics-container::-webkit-scrollbar-track { | |
background: var(--bg-tertiary); | |
border-radius: 3px; | |
} | |
.lyrics-container::-webkit-scrollbar-thumb { | |
background: var(--accent-primary); | |
border-radius: 3px; | |
} | |
/* Metadata Grid */ | |
.metadata-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
gap: 1rem; | |
} | |
.metadata-item { | |
background: var(--bg-secondary); | |
padding: 1rem; | |
border-radius: 8px; | |
text-align: center; | |
} | |
.metadata-item .label { | |
font-size: 0.8rem; | |
color: var(--text-muted); | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
margin-bottom: 0.5rem; | |
} | |
.metadata-item .value { | |
font-weight: 600; | |
color: var(--text-primary); | |
} | |
/* Loading States */ | |
.loading { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 0.5rem; | |
color: var(--text-secondary); | |
} | |
.spinner { | |
width: 20px; | |
height: 20px; | |
border: 2px solid var(--border-color); | |
border-top: 2px solid var(--accent-primary); | |
border-radius: 50%; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
/* Skeleton Loading */ | |
.skeleton { | |
background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%); | |
background-size: 200% 100%; | |
animation: skeleton-loading 1.5s infinite; | |
border-radius: 4px; | |
} | |
@keyframes skeleton-loading { | |
0% { background-position: 200% 0; } | |
100% { background-position: -200% 0; } | |
} | |
.skeleton-text { | |
height: 1rem; | |
margin: 0.5rem 0; | |
} | |
.skeleton-image { | |
width: 100%; | |
height: 200px; | |
} | |
/* Action Buttons */ | |
.action-buttons { | |
display: flex; | |
gap: 0.5rem; | |
margin-top: 1rem; | |
flex-wrap: wrap; | |
} | |
.action-btn { | |
background: var(--bg-secondary); | |
border: 1px solid var(--border-color); | |
color: var(--text-primary); | |
padding: 0.5rem 1rem; | |
border-radius: 6px; | |
font-size: 0.9rem; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.action-btn:hover { | |
background: var(--accent-primary); | |
color: white; | |
transform: translateY(-1px); | |
} | |
.action-btn.favorite.active { | |
background: var(--accent-tertiary); | |
color: white; | |
} | |
/* Error States */ | |
.error { | |
color: #ef4444; | |
background: rgba(239, 68, 68, 0.1); | |
padding: 1rem; | |
border-radius: 8px; | |
border: 1px solid rgba(239, 68, 68, 0.2); | |
} | |
/* Favorites Panel */ | |
.favorites-panel { | |
background: var(--bg-card); | |
border-radius: 16px; | |
padding: 1.5rem; | |
border: 1px solid var(--border-color); | |
box-shadow: 0 8px 25px var(--shadow); | |
margin-top: 1rem; | |
} | |
.favorites-list { | |
display: flex; | |
flex-direction: column; | |
gap: 0.5rem; | |
max-height: 200px; | |
overflow-y: auto; | |
} | |
.favorite-item { | |
background: var(--bg-secondary); | |
padding: 0.75rem; | |
border-radius: 6px; | |
cursor: pointer; | |
transition: background 0.2s ease; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.favorite-item:hover { | |
background: var(--bg-tertiary); | |
} | |
.remove-favorite { | |
color: var(--accent-tertiary); | |
cursor: pointer; | |
font-size: 0.8rem; | |
} | |
/* Related Songs */ | |
.related-songs { | |
display: flex; | |
flex-direction: column; | |
gap: 0.5rem; | |
} | |
.related-song { | |
background: var(--bg-secondary); | |
padding: 1rem; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
} | |
.related-song:hover { | |
background: var(--bg-tertiary); | |
transform: translateY(-1px); | |
} | |
.related-song-title { | |
font-weight: 600; | |
margin-bottom: 0.25rem; | |
} | |
.related-song-artist { | |
color: var(--text-secondary); | |
font-size: 0.9rem; | |
} | |
/* Responsive Design */ | |
@media (max-width: 1024px) { | |
.container { | |
grid-template-columns: 1fr; | |
padding: 1rem; | |
} | |
.header { | |
padding: 1rem; | |
} | |
} | |
@media (max-width: 768px) { | |
.metadata-grid { | |
grid-template-columns: repeat(2, 1fr); | |
} | |
.action-buttons { | |
justify-content: center; | |
} | |
} | |
/* Animations */ | |
.fade-in { | |
animation: fadeIn 0.5s ease-in; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.slide-in { | |
animation: slideIn 0.3s ease-out; | |
} | |
@keyframes slideIn { | |
from { transform: translateX(-20px); opacity: 0; } | |
to { transform: translateX(0); opacity: 1; } | |
} | |
/* Custom scrollbar */ | |
::-webkit-scrollbar { | |
width: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: var(--bg-secondary); | |
} | |
::-webkit-scrollbar-thumb { | |
background: var(--accent-primary); | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: var(--accent-secondary); | |
} | |
/* Focus styles for accessibility */ | |
button:focus-visible, | |
input:focus-visible { | |
outline: 2px solid var(--accent-primary); | |
outline-offset: 2px; | |
} | |
/* Hidden state */ | |
.hidden { | |
display: none ; | |
} | |
/* Notification */ | |
.notification { | |
position: fixed; | |
top: 100px; | |
right: 20px; | |
background: var(--bg-card); | |
border: 1px solid var(--border-color); | |
border-radius: 8px; | |
padding: 1rem; | |
box-shadow: 0 8px 25px var(--shadow); | |
z-index: 1000; | |
animation: slideInRight 0.3s ease-out; | |
max-width: 300px; | |
} | |
@keyframes slideInRight { | |
from { transform: translateX(100%); } | |
to { transform: translateX(0); } | |
} | |
.notification.success { | |
border-left: 4px solid var(--accent-primary); | |
} | |
.notification.error { | |
border-left: 4px solid #ef4444; | |
} | |
</style> | |
</head> | |
<body> | |
<header class="header"> | |
<div class="logo">🎵 MusiSync</div> | |
<button class="theme-toggle" id="themeToggle"> | |
<span id="themeIcon">🌙</span> | |
<span id="themeText">Dark Mode</span> | |
</button> | |
</header> | |
<div class="container"> | |
<!-- Search Section --> | |
<div class="search-section"> | |
<h2 class="search-title">Search Music</h2> | |
<form class="search-form" id="searchForm"> | |
<div class="input-group"> | |
<label for="artistInput">Artist Name</label> | |
<input type="text" id="artistInput" placeholder="Enter artist name..." autocomplete="off"> | |
<div class="autocomplete-dropdown" id="artistDropdown"></div> | |
</div> | |
<div class="input-group"> | |
<label for="songInput">Song Title</label> | |
<input type="text" id="songInput" placeholder="Enter song title..." autocomplete="off"> | |
<div class="autocomplete-dropdown" id="songDropdown"></div> | |
</div> | |
<button type="submit" class="search-btn" id="searchBtn"> | |
<span id="searchBtnText">Search</span> | |
</button> | |
</form> | |
<!-- Favorites Panel --> | |
<div class="favorites-panel"> | |
<h3>❤️ Favorites</h3> | |
<div class="favorites-list" id="favoritesList"> | |
<div class="loading" id="favoritesEmpty">No favorites yet. Search for songs and add them to your favorites!</div> | |
</div> | |
</div> | |
</div> | |
<!-- Results Section --> | |
<div class="results-section" id="resultsSection"> | |
<!-- Album Art Card --> | |
<div class="result-card album-art-card hidden" id="albumArtCard"> | |
<h3>🎨 Album Artwork</h3> | |
<img class="album-art" id="albumArt" alt="Album artwork"> | |
<div class="action-buttons"> | |
<button class="action-btn" id="downloadArt">📥 Download</button> | |
<button class="action-btn" id="shareArt">🔗 Share</button> | |
</div> | |
</div> | |
<!-- Song Info Card --> | |
<div class="result-card hidden" id="songInfoCard"> | |
<h3>🎵 Song Information</h3> | |
<div class="metadata-grid" id="songMetadata"></div> | |
<div class="action-buttons"> | |
<button class="action-btn favorite" id="favoriteBtn">❤️ Add to Favorites</button> | |
<button class="action-btn" id="shareBtn">🔗 Share</button> | |
</div> | |
</div> | |
<!-- Artist Info Card --> | |
<div class="result-card hidden" id="artistInfoCard"> | |
<h3>👤 Artist Information</h3> | |
<div id="artistInfo"></div> | |
</div> | |
<!-- Lyrics Card --> | |
<div class="result-card hidden" id="lyricsCard"> | |
<h3>📝 Lyrics</h3> | |
<div class="lyrics-container" id="lyricsContent"></div> | |
<div class="action-buttons"> | |
<button class="action-btn" id="downloadLyrics">📥 Download Lyrics</button> | |
<button class="action-btn" id="shareLyrics">🔗 Share</button> | |
</div> | |
</div> | |
<!-- Related Songs Card --> | |
<div class="result-card hidden" id="relatedSongsCard"> | |
<h3>🎶 Related Songs</h3> | |
<div class="related-songs" id="relatedSongs"></div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Global App State | |
const AppState = { | |
currentSearch: null, | |
cache: new Map(), | |
favorites: JSON.parse(localStorage.getItem('musicapp_favorites') || '[]'), | |
theme: localStorage.getItem('musicapp_theme') || 'dark', | |
searchHistory: JSON.parse(localStorage.getItem('musicapp_history') || '[]') | |
}; | |
// API Configuration | |
const API_CONFIG = { | |
// Note: Replace with your actual API keys in production | |
lastfm: { | |
key: 'demo_key_replace_with_yours', | |
baseUrl: 'https://ws.audioscrobbler.com/2.0/' | |
}, | |
musicbrainz: { | |
baseUrl: 'https://musicbrainz.org/ws/2/', | |
userAgent: 'MusiSync/1.0 (https://your-app.com)' | |
}, | |
coverart: { | |
baseUrl: 'https://coverartarchive.org/' | |
}, | |
lyrics: { | |
baseUrl: 'https://api.lyrics.ovh/v1/' | |
}, | |
itunes: { | |
baseUrl: 'https://itunes.apple.com/' | |
} | |
}; | |
// Utility Functions | |
class Utils { | |
static debounce(func, wait) { | |
let timeout; | |
return function executedFunction(...args) { | |
const later = () => { | |
clearTimeout(timeout); | |
func(...args); | |
}; | |
clearTimeout(timeout); | |
timeout = setTimeout(later, wait); | |
}; | |
} | |
static sanitizeQuery(query) { | |
return query.trim().replace(/[^\w\s-]/g, '').substring(0, 100); | |
} | |
static formatDuration(ms) { | |
if (!ms) return 'Unknown'; | |
const minutes = Math.floor(ms / 60000); | |
const seconds = ((ms % 60000) / 1000).toFixed(0); | |
return `${minutes}:${seconds.padStart(2, '0')}`; | |
} | |
static formatDate(dateString) { | |
if (!dateString) return 'Unknown'; | |
return new Date(dateString).toLocaleDateString(); | |
} | |
static showNotification(message, type = 'success') { | |
const notification = document.createElement('div'); | |
notification.className = `notification ${type}`; | |
notification.textContent = message; | |
document.body.appendChild(notification); | |
setTimeout(() => { | |
notification.remove(); | |
}, 3000); | |
} | |
static downloadFile(content, filename, type = 'text/plain') { | |
const blob = new Blob([content], { type }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = filename; | |
a.click(); | |
URL.revokeObjectURL(url); | |
} | |
static getCacheKey(type, params) { | |
return `${type}_${Object.values(params).join('_')}`; | |
} | |
static getCachedData(key, maxAge = 24 * 60 * 60 * 1000) { // 24 hours default | |
const cached = AppState.cache.get(key); | |
if (!cached) return null; | |
if (Date.now() - cached.timestamp > maxAge) { | |
AppState.cache.delete(key); | |
return null; | |
} | |
return cached.data; | |
} | |
static setCachedData(key, data) { | |
AppState.cache.set(key, { | |
data, | |
timestamp: Date.now() | |
}); | |
} | |
} | |
// API Service Classes | |
class LyricsService { | |
static async fetchLyrics(artist, title) { | |
const cacheKey = Utils.getCacheKey('lyrics', { artist, title }); | |
const cached = Utils.getCachedData(cacheKey); | |
if (cached) return cached; | |
try { | |
// Primary: Lyrics.ovh | |
const response = await fetch(`${API_CONFIG.lyrics.baseUrl}${encodeURIComponent(artist)}/${encodeURIComponent(title)}`); | |
if (response.ok) { | |
const data = await response.json(); | |
Utils.setCachedData(cacheKey, data.lyrics); | |
return data.lyrics; | |
} | |
} catch (error) { | |
console.warn('Lyrics.ovh failed:', error); | |
} | |
// Fallback: Mock lyrics for demo | |
const mockLyrics = `Lyrics for "${title}" by ${artist} are not available. | |
This is a demo application. In a production environment, you would: | |
1. Use authenticated APIs like Genius or Musixmatch | |
2. Implement proper error handling | |
3. Add retry mechanisms | |
4. Consider legal aspects of lyrics distribution | |
For now, enjoy exploring the metadata and album artwork features!`; | |
Utils.setCachedData(cacheKey, mockLyrics); | |
return mockLyrics; | |
} | |
} | |
class MetadataService { | |
static async fetchFromMusicBrainz(artist, title) { | |
const cacheKey = Utils.getCacheKey('mb_search', { artist, title }); | |
const cached = Utils.getCachedData(cacheKey); | |
if (cached) return cached; | |
try { | |
await this.rateLimitDelay(); // Respect MusicBrainz rate limit | |
const query = encodeURIComponent(`artist:"${artist}" AND recording:"${title}"`); | |
const url = `${API_CONFIG.musicbrainz.baseUrl}recording?query=${query}&fmt=json&limit=5`; | |
const response = await fetch(url, { | |
headers: { | |
'User-Agent': API_CONFIG.musicbrainz.userAgent, | |
'Accept': 'application/json' | |
} | |
}); | |
if (response.ok) { | |
const data = await response.json(); | |
Utils.setCachedData(cacheKey, data); | |
return data; | |
} | |
} catch (error) { | |
console.warn('MusicBrainz API failed:', error); | |
} | |
return null; | |
} | |
static async fetchFromLastFm(artist, title) { | |
const cacheKey = Utils.getCacheKey('lastfm_track', { artist, title }); | |
const cached = Utils.getCachedData(cacheKey); | |
if (cached) return cached; | |
try { | |
// Using JSONP for cross-origin requests | |
const data = await this.jsonpRequest( | |
`${API_CONFIG.lastfm.baseUrl}?method=track.getInfo&api_key=${API_CONFIG.lastfm.key}&artist=${encodeURIComponent(artist)}&track=${encodeURIComponent(title)}&format=json` | |
); | |
Utils.setCachedData(cacheKey, data); | |
return data; | |
} catch (error) { | |
console.warn('Last.fm API failed:', error); | |
} | |
return null; | |
} | |
static async fetchFromItunes(artist, title) { | |
const cacheKey = Utils.getCacheKey('itunes_search', { artist, title }); | |
const cached = Utils.getCachedData(cacheKey); | |
if (cached) return cached; | |
try { | |
// iTunes Search API supports JSONP | |
const query = encodeURIComponent(`${artist} ${title}`); | |
const data = await this.jsonpRequest( | |
`${API_CONFIG.itunes.baseUrl}search?term=${query}&entity=song&limit=5` | |
); | |
Utils.setCachedData(cacheKey, data); | |
return data; | |
} catch (error) { | |
console.warn('iTunes API failed:', error); | |
} | |
return null; | |
} | |
static jsonpRequest(url) { | |
return new Promise((resolve, reject) => { | |
const callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random()); | |
const script = document.createElement('script'); | |
window[callbackName] = function(data) { | |
delete window[callbackName]; | |
document.body.removeChild(script); | |
resolve(data); | |
}; | |
script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName; | |
script.onerror = () => { | |
delete window[callbackName]; | |
document.body.removeChild(script); | |
reject(new Error('JSONP request failed')); | |
}; | |
document.body.appendChild(script); | |
// Timeout after 10 seconds | |
setTimeout(() => { | |
if (window[callbackName]) { | |
delete window[callbackName]; | |
document.body.removeChild(script); | |
reject(new Error('JSONP request timeout')); | |
} | |
}, 10000); | |
}); | |
} | |
static rateLimitDelay() { | |
// MusicBrainz requires 1 request per second | |
return new Promise(resolve => setTimeout(resolve, 1000)); | |
} | |
} | |
class ArtworkService { | |
static async fetchAlbumArt(mbid, spotifyUrl, lastfmUrl, itunesUrl) { | |
// Primary: Cover Art Archive (highest quality) | |
if (mbid) { | |
try { | |
const response = await fetch(`${API_CONFIG.coverart.baseUrl}release/${mbid}`); | |
if (response.ok) { | |
const data = await response.json(); | |
if (data.images && data.images.length > 0) { | |
return data.images[0].thumbnails?.large || data.images[0].image; | |
} | |
} | |
} catch (error) { | |
console.warn('Cover Art Archive failed:', error); | |
} | |
} | |
// Fallback order: Spotify, iTunes, Last.fm | |
const fallbacks = [spotifyUrl, itunesUrl, lastfmUrl].filter(Boolean); | |
for (const url of fallbacks) { | |
if (await this.isImageAccessible(url)) { | |
return url; | |
} | |
} | |
return this.getDefaultArtwork(); | |
} | |
static async isImageAccessible(url) { | |
try { | |
const response = await fetch(url, { method: 'HEAD' }); | |
return response.ok; | |
} catch { | |
return false; | |
} | |
} | |
static getDefaultArtwork() { | |
// Generate a beautiful default artwork using CSS gradients | |
const canvas = document.createElement('canvas'); | |
canvas.width = 300; | |
canvas.height = 300; | |
const ctx = canvas.getContext('2d'); | |
// Create gradient | |
const gradient = ctx.createLinearGradient(0, 0, 300, 300); | |
gradient.addColorStop(0, '#1db954'); | |
gradient.addColorStop(0.5, '#1ed760'); | |
gradient.addColorStop(1, '#ff6b35'); | |
ctx.fillStyle = gradient; | |
ctx.fillRect(0, 0, 300, 300); | |
// Add music note | |
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; | |
ctx.font = 'bold 80px Arial'; | |
ctx.textAlign = 'center'; | |
ctx.fillText('🎵', 150, 180); | |
return canvas.toDataURL(); | |
} | |
} | |
// UI Controller Classes | |
class SearchController { | |
constructor() { | |
this.initElements(); | |
this.bindEvents(); | |
this.setupAutocomplete(); | |
} | |
initElements() { | |
this.form = document.getElementById('searchForm'); | |
this.artistInput = document.getElementById('artistInput'); | |
this.songInput = document.getElementById('songInput'); | |
this.searchBtn = document.getElementById('searchBtn'); | |
this.searchBtnText = document.getElementById('searchBtnText'); | |
} | |
bindEvents() { | |
this.form.addEventListener('submit', this.handleSearch.bind(this)); | |
// Debounced autocomplete | |
const debouncedAutocomplete = Utils.debounce(this.handleAutocomplete.bind(this), 300); | |
this.artistInput.addEventListener('input', debouncedAutocomplete); | |
this.songInput.addEventListener('input', debouncedAutocomplete); | |
} | |
setupAutocomplete() { | |
// Implement autocomplete based on search history | |
[this.artistInput, this.songInput].forEach(input => { | |
input.addEventListener('focus', () => this.showAutocomplete(input)); | |
input.addEventListener('blur', () => { | |
// Delay hiding to allow clicking on suggestions | |
setTimeout(() => this.hideAutocomplete(input), 200); | |
}); | |
}); | |
} | |
async handleSearch(e) { | |
e.preventDefault(); | |
const artist = Utils.sanitizeQuery(this.artistInput.value); | |
const song = Utils.sanitizeQuery(this.songInput.value); | |
if (!artist || !song) { | |
Utils.showNotification('Please enter both artist and song title', 'error'); | |
return; | |
} | |
this.setLoading(true); | |
try { | |
AppState.currentSearch = { artist, song }; | |
this.addToHistory(artist, song); | |
// Start all API calls simultaneously | |
const [lyrics, metadata, artwork] = await Promise.allSettled([ | |
LyricsService.fetchLyrics(artist, song), | |
this.fetchAllMetadata(artist, song), | |
this.fetchArtworkFromMultipleSources(artist, song) | |
]); | |
// Update UI with results | |
if (lyrics.status === 'fulfilled') { | |
resultsController.displayLyrics(lyrics.value); | |
} | |
if (metadata.status === 'fulfilled') { | |
resultsController.displayMetadata(metadata.value); | |
} | |
if (artwork.status === 'fulfilled') { | |
resultsController.displayArtwork(artwork.value); | |
} | |
Utils.showNotification('Search completed successfully!'); | |
} catch (error) { | |
console.error('Search failed:', error); | |
Utils.showNotification('Search failed. Please try again.', 'error'); | |
} finally { | |
this.setLoading(false); | |
} | |
} | |
async fetchAllMetadata(artist, song) { | |
const results = await Promise.allSettled([ | |
MetadataService.fetchFromMusicBrainz(artist, song), | |
MetadataService.fetchFromLastFm(artist, song), | |
MetadataService.fetchFromItunes(artist, song) | |
]); | |
// Combine results from multiple sources | |
const metadata = { | |
musicbrainz: results[0].status === 'fulfilled' ? results[0].value : null, | |
lastfm: results[1].status === 'fulfilled' ? results[1].value : null, | |
itunes: results[2].status === 'fulfilled' ? results[2].value : null | |
}; | |
return this.normalizeMetadata(metadata, artist, song); | |
} | |
normalizeMetadata(sources, artist, song) { | |
const normalized = { | |
title: song, | |
artist: artist, | |
album: 'Unknown', | |
duration: null, | |
releaseDate: 'Unknown', | |
genre: 'Unknown', | |
popularity: null, | |
playCount: null, | |
mbid: null, | |
artworkUrls: {} | |
}; | |
// Extract data from MusicBrainz | |
if (sources.musicbrainz?.recordings?.length > 0) { | |
const record = sources.musicbrainz.recordings[0]; | |
normalized.mbid = record.id; | |
normalized.duration = record.length; | |
if (record.releases?.length > 0) { | |
normalized.album = record.releases[0].title; | |
normalized.releaseDate = record.releases[0].date; | |
} | |
if (record.tags?.length > 0) { | |
normalized.genre = record.tags[0].name; | |
} | |
} | |
// Extract data from Last.fm | |
if (sources.lastfm?.track) { | |
const track = sources.lastfm.track; | |
normalized.playCount = track.playcount; | |
normalized.album = track.album?.title || normalized.album; | |
if (track.toptags?.tag?.length > 0) { | |
normalized.genre = track.toptags.tag[0].name; | |
} | |
if (track.album?.image) { | |
normalized.artworkUrls.lastfm = track.album.image.find(img => img.size === 'large')?.['#text']; | |
} | |
} | |
// Extract data from iTunes | |
if (sources.itunes?.results?.length > 0) { | |
const track = sources.itunes.results[0]; | |
normalized.album = track.collectionName || normalized.album; | |
normalized.duration = track.trackTimeMillis; | |
normalized.releaseDate = track.releaseDate; | |
normalized.genre = track.primaryGenreName || normalized.genre; | |
normalized.artworkUrls.itunes = track.artworkUrl100?.replace('100x100', '600x600'); | |
} | |
return normalized; | |
} | |
async fetchArtworkFromMultipleSources(artist, song) { | |
// Get artwork URLs from metadata first | |
const metadata = await this.fetchAllMetadata(artist, song); | |
return await ArtworkService.fetchAlbumArt( | |
metadata.mbid, | |
null, // Spotify URL would go here | |
metadata.artworkUrls.lastfm, | |
metadata.artworkUrls.itunes | |
); | |
} | |
handleAutocomplete(e) { | |
const input = e.target; | |
const value = input.value.trim(); | |
if (value.length < 2) { | |
this.hideAutocomplete(input); | |
return; | |
} | |
const suggestions = this.getSuggestions(input.id, value); | |
this.showSuggestions(input, suggestions); | |
} | |
getSuggestions(inputType, value) { | |
const field = inputType === 'artistInput' ? 'artist' : 'song'; | |
const history = AppState.searchHistory; | |
return history | |
.map(entry => entry[field]) | |
.filter(item => item.toLowerCase().includes(value.toLowerCase())) | |
.filter((item, index, arr) => arr.indexOf(item) === index) // Remove duplicates | |
.slice(0, 5); | |
} | |
showSuggestions(input, suggestions) { | |
const dropdown = input.nextElementSibling; | |
dropdown.innerHTML = ''; | |
if (suggestions.length === 0) { | |
this.hideAutocomplete(input); | |
return; | |
} | |
suggestions.forEach(suggestion => { | |
const item = document.createElement('div'); | |
item.className = 'autocomplete-item'; | |
item.textContent = suggestion; | |
item.addEventListener('click', () => { | |
input.value = suggestion; | |
this.hideAutocomplete(input); | |
input.focus(); | |
}); | |
dropdown.appendChild(item); | |
}); | |
dropdown.style.display = 'block'; | |
} | |
showAutocomplete(input) { | |
if (input.value.length >= 2) { | |
this.handleAutocomplete({ target: input }); | |
} | |
} | |
hideAutocomplete(input) { | |
const dropdown = input.nextElementSibling; | |
dropdown.style.display = 'none'; | |
} | |
addToHistory(artist, song) { | |
const entry = { artist, song, timestamp: Date.now() }; | |
AppState.searchHistory = [entry, ...AppState.searchHistory.filter( | |
h => !(h.artist === artist && h.song === song) | |
)].slice(0, 50); // Keep last 50 searches | |
localStorage.setItem('musicapp_history', JSON.stringify(AppState.searchHistory)); | |
} | |
setLoading(isLoading) { | |
this.searchBtn.disabled = isLoading; | |
this.searchBtnText.textContent = isLoading ? 'Searching...' : 'Search'; | |
if (isLoading) { | |
this.searchBtn.innerHTML = '<div class="spinner"></div> Searching...'; | |
} else { | |
this.searchBtn.innerHTML = 'Search'; | |
} | |
} | |
} | |
class ResultsController { | |
constructor() { | |
this.initElements(); | |
this.bindEvents(); | |
} | |
initElements() { | |
this.albumArtCard = document.getElementById('albumArtCard'); | |
this.albumArt = document.getElementById('albumArt'); | |
this.songInfoCard = document.getElementById('songInfoCard'); | |
this.songMetadata = document.getElementById('songMetadata'); | |
this.artistInfoCard = document.getElementById('artistInfoCard'); | |
this.artistInfo = document.getElementById('artistInfo'); | |
this.lyricsCard = document.getElementById('lyricsCard'); | |
this.lyricsContent = document.getElementById('lyricsContent'); | |
this.relatedSongsCard = document.getElementById('relatedSongsCard'); | |
this.relatedSongs = document.getElementById('relatedSongs'); | |
} | |
bindEvents() { | |
// Favorite button | |
document.getElementById('favoriteBtn').addEventListener('click', this.handleFavorite.bind(this)); | |
// Download buttons | |
document.getElementById('downloadLyrics').addEventListener('click', this.downloadLyrics.bind(this)); | |
document.getElementById('downloadArt').addEventListener('click', this.downloadArtwork.bind(this)); | |
// Share buttons | |
document.getElementById('shareBtn').addEventListener('click', this.shareTrack.bind(this)); | |
document.getElementById('shareLyrics').addEventListener('click', this.shareLyrics.bind(this)); | |
document.getElementById('shareArt').addEventListener('click', this.shareArtwork.bind(this)); | |
} | |
displayLyrics(lyrics) { | |
this.lyricsContent.textContent = lyrics; | |
this.showCard(this.lyricsCard); | |
} | |
displayMetadata(metadata) { | |
this.songMetadata.innerHTML = ''; | |
const fields = [ | |
{ label: 'Title', value: metadata.title }, | |
{ label: 'Artist', value: metadata.artist }, | |
{ label: 'Album', value: metadata.album }, | |
{ label: 'Duration', value: Utils.formatDuration(metadata.duration) }, | |
{ label: 'Release Date', value: Utils.formatDate(metadata.releaseDate) }, | |
{ label: 'Genre', value: metadata.genre }, | |
{ label: 'Play Count', value: metadata.playCount ? metadata.playCount.toLocaleString() : 'N/A' } | |
]; | |
fields.forEach(field => { | |
const item = document.createElement('div'); | |
item.className = 'metadata-item'; | |
item.innerHTML = ` | |
<div class="label">${field.label}</div> | |
<div class="value">${field.value}</div> | |
`; | |
this.songMetadata.appendChild(item); | |
}); | |
this.showCard(this.songInfoCard); | |
// Update favorite button state | |
this.updateFavoriteButton(); | |
} | |
displayArtwork(artworkUrl) { | |
this.albumArt.src = artworkUrl; | |
this.albumArt.alt = `Album artwork for ${AppState.currentSearch?.song || 'Unknown'}`; | |
this.showCard(this.albumArtCard); | |
} | |
displayArtistInfo(artistData) { | |
// Mock artist info for demo | |
this.artistInfo.innerHTML = ` | |
<div class="metadata-grid"> | |
<div class="metadata-item"> | |
<div class="label">Artist</div> | |
<div class="value">${AppState.currentSearch?.artist || 'Unknown'}</div> | |
</div> | |
<div class="metadata-item"> | |
<div class="label">Followers</div> | |
<div class="value">-</div> | |
</div> | |
<div class="metadata-item"> | |
<div class="label">Monthly Listeners</div> | |
<div class="value">-</div> | |
</div> | |
<div class="metadata-item"> | |
<div class="label">Top Genre</div> | |
<div class="value">-</div> | |
</div> | |
</div> | |
<p style="margin-top: 1rem; color: var(--text-secondary);"> | |
Artist information would be fetched from Spotify or Last.fm APIs in a production environment. | |
</p> | |
`; | |
this.showCard(this.artistInfoCard); | |
} | |
displayRelatedSongs() { | |
// Mock related songs for demo | |
const mockSongs = [ | |
{ title: 'Related Song 1', artist: 'Similar Artist' }, | |
{ title: 'Related Song 2', artist: 'Another Artist' }, | |
{ title: 'Related Song 3', artist: 'Different Artist' } | |
]; | |
this.relatedSongs.innerHTML = ''; | |
mockSongs.forEach(song => { | |
const songElement = document.createElement('div'); | |
songElement.className = 'related-song'; | |
songElement.innerHTML = ` | |
<div class="related-song-title">${song.title}</div> | |
<div class="related-song-artist">${song.artist}</div> | |
`; | |
songElement.addEventListener('click', () => { | |
document.getElementById('artistInput').value = song.artist; | |
document.getElementById('songInput').value = song.title; | |
Utils.showNotification(`Filled in "${song.title}" by ${song.artist}`); | |
}); | |
this.relatedSongs.appendChild(songElement); | |
}); | |
this.showCard(this.relatedSongsCard); | |
} | |
showCard(card) { | |
card.classList.remove('hidden'); | |
card.classList.add('fade-in'); | |
} | |
hideAllCards() { | |
[this.albumArtCard, this.songInfoCard, this.artistInfoCard, this.lyricsCard, this.relatedSongsCard] | |
.forEach(card => card.classList.add('hidden')); | |
} | |
handleFavorite() { | |
if (!AppState.currentSearch) return; | |
const { artist, song } = AppState.currentSearch; | |
const favoriteKey = `${artist}_${song}`; | |
const favoriteBtn = document.getElementById('favoriteBtn'); | |
const existingIndex = AppState.favorites.findIndex(fav => fav.key === favoriteKey); | |
if (existingIndex >= 0) { | |
// Remove from favorites | |
AppState.favorites.splice(existingIndex, 1); | |
favoriteBtn.classList.remove('active'); | |
favoriteBtn.innerHTML = '❤️ Add to Favorites'; | |
Utils.showNotification('Removed from favorites'); | |
} else { | |
// Add to favorites | |
AppState.favorites.push({ | |
key: favoriteKey, | |
artist, | |
song, | |
timestamp: Date.now() | |
}); | |
favoriteBtn.classList.add('active'); | |
favoriteBtn.innerHTML = '💔 Remove from Favorites'; | |
Utils.showNotification('Added to favorites'); | |
} | |
localStorage.setItem('musicapp_favorites', JSON.stringify(AppState.favorites)); | |
favoritesController.updateFavoritesList(); | |
} | |
updateFavoriteButton() { | |
if (!AppState.currentSearch) return; | |
const { artist, song } = AppState.currentSearch; | |
const favoriteKey = `${artist}_${song}`; | |
const favoriteBtn = document.getElementById('favoriteBtn'); | |
const isFavorite = AppState.favorites.some(fav => fav.key === favoriteKey); | |
if (isFavorite) { | |
favoriteBtn.classList.add('active'); | |
favoriteBtn.innerHTML = '💔 Remove from Favorites'; | |
} else { | |
favoriteBtn.classList.remove('active'); | |
favoriteBtn.innerHTML = '❤️ Add to Favorites'; | |
} | |
} | |
downloadLyrics() { | |
if (!AppState.currentSearch) return; | |
const lyrics = this.lyricsContent.textContent; | |
const filename = `${AppState.currentSearch.artist} - ${AppState.currentSearch.song} - Lyrics.txt`; | |
Utils.downloadFile(lyrics, filename); | |
Utils.showNotification('Lyrics downloaded!'); | |
} | |
downloadArtwork() { | |
if (!this.albumArt.src) return; | |
const link = document.createElement('a'); | |
link.href = this.albumArt.src; | |
link.download = `${AppState.currentSearch?.artist || 'Unknown'} - ${AppState.currentSearch?.song || 'Unknown'} - Artwork.jpg`; | |
link.click(); | |
Utils.showNotification('Artwork download started!'); | |
} | |
shareTrack() { | |
if (!AppState.currentSearch) return; | |
const { artist, song } = AppState.currentSearch; | |
const shareText = `Check out "${song}" by ${artist}! 🎵`; | |
if (navigator.share) { | |
navigator.share({ | |
title: 'MusiSync', | |
text: shareText, | |
url: window.location.href | |
}); | |
} else { | |
navigator.clipboard.writeText(shareText); | |
Utils.showNotification('Track info copied to clipboard!'); | |
} | |
} | |
shareLyrics() { | |
if (!AppState.currentSearch) return; | |
const lyrics = this.lyricsContent.textContent; | |
const shareText = `🎵 "${AppState.currentSearch.song}" by ${AppState.currentSearch.artist}\n\n${lyrics}`; | |
if (navigator.share) { | |
navigator.share({ | |
title: 'Song Lyrics', | |
text: shareText | |
}); | |
} else { | |
navigator.clipboard.writeText(shareText); | |
Utils.showNotification('Lyrics copied to clipboard!'); | |
} | |
} | |
shareArtwork() { | |
if (!this.albumArt.src) return; | |
if (navigator.share) { | |
navigator.share({ | |
title: 'Album Artwork', | |
text: `Album artwork for "${AppState.currentSearch?.song}" by ${AppState.currentSearch?.artist}`, | |
url: this.albumArt.src | |
}); | |
} else { | |
navigator.clipboard.writeText(this.albumArt.src); | |
Utils.showNotification('Artwork URL copied to clipboard!'); | |
} | |
} | |
} | |
class FavoritesController { | |
constructor() { | |
this.favoritesList = document.getElementById('favoritesList'); | |
this.updateFavoritesList(); | |
} | |
updateFavoritesList() { | |
if (AppState.favorites.length === 0) { | |
this.favoritesList.innerHTML = '<div class="loading">No favorites yet. Search for songs and add them to your favorites!</div>'; | |
return; | |
} | |
this.favoritesList.innerHTML = ''; | |
AppState.favorites.slice(0, 10).forEach(favorite => { // Show last 10 favorites | |
const item = document.createElement('div'); | |
item.className = 'favorite-item'; | |
item.innerHTML = ` | |
<div> | |
<div style="font-weight: 600;">${favorite.song}</div> | |
<div style="color: var(--text-secondary); font-size: 0.9rem;">${favorite.artist}</div> | |
</div> | |
<span class="remove-favorite" title="Remove favorite">✕</span> | |
`; | |
// Click to search | |
item.querySelector('div').addEventListener('click', () => { | |
document.getElementById('artistInput').value = favorite.artist; | |
document.getElementById('songInput').value = favorite.song; | |
Utils.showNotification(`Loaded "${favorite.song}" by ${favorite.artist}`); | |
}); | |
// Remove favorite | |
item.querySelector('.remove-favorite').addEventListener('click', (e) => { | |
e.stopPropagation(); | |
this.removeFavorite(favorite.key); | |
}); | |
this.favoritesList.appendChild(item); | |
}); | |
} | |
removeFavorite(key) { | |
AppState.favorites = AppState.favorites.filter(fav => fav.key !== key); | |
localStorage.setItem('musicapp_favorites', JSON.stringify(AppState.favorites)); | |
this.updateFavoritesList(); | |
Utils.showNotification('Removed from favorites'); | |
// Update favorite button if current search matches | |
if (AppState.currentSearch) { | |
const currentKey = `${AppState.currentSearch.artist}_${AppState.currentSearch.song}`; | |
if (currentKey === key) { | |
resultsController.updateFavoriteButton(); | |
} | |
} | |
} | |
} | |
class ThemeController { | |
constructor() { | |
this.themeToggle = document.getElementById('themeToggle'); | |
this.themeIcon = document.getElementById('themeIcon'); | |
this.themeText = document.getElementById('themeText'); | |
this.initTheme(); | |
this.bindEvents(); | |
} | |
initTheme() { | |
document.documentElement.setAttribute('data-theme', AppState.theme); | |
this.updateThemeUI(); | |
} | |
bindEvents() { | |
this.themeToggle.addEventListener('click', this.toggleTheme.bind(this)); | |
} | |
toggleTheme() { | |
AppState.theme = AppState.theme === 'dark' ? 'light' : 'dark'; | |
document.documentElement.setAttribute('data-theme', AppState.theme); | |
localStorage.setItem('musicapp_theme', AppState.theme); | |
this.updateThemeUI(); | |
Utils.showNotification(`Switched to ${AppState.theme} theme`); | |
} | |
updateThemeUI() { | |
if (AppState.theme === 'dark') { | |
this.themeIcon.textContent = '🌙'; | |
this.themeText.textContent = 'Dark Mode'; | |
} else { | |
this.themeIcon.textContent = '☀️'; | |
this.themeText.textContent = 'Light Mode'; | |
} | |
} | |
} | |
// Initialize Application | |
class App { | |
constructor() { | |
this.init(); | |
} | |
init() { | |
// Initialize controllers | |
this.searchController = new SearchController(); | |
this.resultsController = new ResultsController(); | |
this.favoritesController = new FavoritesController(); | |
this.themeController = new ThemeController(); | |
// Global error handler | |
window.addEventListener('error', this.handleGlobalError.bind(this)); | |
window.addEventListener('unhandledrejection', this.handleGlobalError.bind(this)); | |
// Add accessibility features | |
this.setupAccessibility(); | |
console.log('🎵 MusiSync App initialized!'); | |
Utils.showNotification('Welcome to MusiSync! Search for your favorite songs.'); | |
} | |
handleGlobalError(event) { | |
console.error('Global error:', event.error || event.reason); | |
Utils.showNotification('An unexpected error occurred. Please try again.', 'error'); | |
} | |
setupAccessibility() { | |
// Add keyboard navigation for autocomplete | |
document.addEventListener('keydown', (e) => { | |
if (e.key === 'Escape') { | |
// Close any open dropdowns | |
document.querySelectorAll('.autocomplete-dropdown').forEach(dropdown => { | |
dropdown.style.display = 'none'; | |
}); | |
} | |
}); | |
// Add ARIA labels | |
document.querySelectorAll('input').forEach(input => { | |
if (!input.getAttribute('aria-label')) { | |
input.setAttribute('aria-label', input.placeholder); | |
} | |
}); | |
// Add focus indicators | |
document.querySelectorAll('button, input').forEach(element => { | |
element.addEventListener('focus', () => { | |
element.style.outline = '2px solid var(--accent-primary)'; | |
element.style.outlineOffset = '2px'; | |
}); | |
element.addEventListener('blur', () => { | |
element.style.outline = 'none'; | |
}); | |
}); | |
} | |
} | |
// Global references for easier access | |
let app, searchController, resultsController, favoritesController, themeController; | |
// Initialize app when DOM is ready | |
document.addEventListener('DOMContentLoaded', () => { | |
app = new App(); | |
// Make controllers globally accessible | |
searchController = app.searchController; | |
resultsController = app.resultsController; | |
favoritesController = app.favoritesController; | |
themeController = app.themeController; | |
}); | |
// Service Worker Registration (optional, for offline support) | |
if ('serviceWorker' in navigator) { | |
window.addEventListener('load', () => { | |
// You could register a service worker here for offline functionality | |
console.log('Service Worker support detected'); | |
}); | |
} | |
</script> | |
<style> | |
#minimax-floating-ball { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
padding: 10px 12px; | |
background: #222222; | |
border-radius: 12px; | |
display: flex; | |
align-items: center; | |
color: #F8F8F8; | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |
z-index: 9999; | |
transition: all 0.3s ease; | |
overflow: hidden; | |
cursor: pointer; | |
} | |
#minimax-floating-ball:hover { | |
transform: translateY(-2px); | |
background: #383838; | |
} | |
.minimax-ball-content { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.minimax-logo-wave { | |
width: 26px; | |
height: 22px; | |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='22' viewBox='0 0 26 22' fill='none'%3E%3Cg clip-path='url(%23clip0_3442_102412)'%3E%3Cpath d='M12.8405 14.6775C12.8405 14.9897 13.0932 15.2424 13.4055 15.2424C13.7178 15.2424 13.9705 14.9897 13.9705 14.6775V2.98254C13.9705 1.88957 13.0809 1 11.9879 1C10.895 1 10.0054 1.88957 10.0054 2.98254V11.566V17.1068C10.0054 17.5773 9.62327 17.9594 9.1528 17.9594C8.68233 17.9594 8.30021 17.5773 8.30021 17.1068V8.04469C8.30021 6.95172 7.41063 6.06215 6.31767 6.06215C5.22471 6.06215 4.33513 6.95172 4.33513 8.04469V11.8855C4.33513 12.3559 3.953 12.7381 3.48254 12.7381C3.01207 12.7381 2.62994 12.3559 2.62994 11.8855V10.4936C2.62994 10.1813 2.37725 9.92861 2.06497 9.92861C1.7527 9.92861 1.5 10.1813 1.5 10.4936V11.8855C1.5 12.9784 2.38957 13.868 3.48254 13.868C4.5755 13.868 5.46508 12.9784 5.46508 11.8855V8.04469C5.46508 7.57422 5.8472 7.19209 6.31767 7.19209C6.78814 7.19209 7.17026 7.57422 7.17026 8.04469V17.1068C7.17026 18.1998 8.05984 19.0894 9.1528 19.0894C10.2458 19.0894 11.1353 18.1998 11.1353 17.1068V2.98254C11.1353 2.51207 11.5175 2.12994 11.9879 2.12994C12.4584 2.12994 12.8405 2.51207 12.8405 2.98254V14.6775Z' fill='%23F8F8F8'/%3E%3Cpath d='M23.3278 6.06215C22.2348 6.06215 21.3452 6.95172 21.3452 8.04469V15.6143C21.3452 16.0847 20.9631 16.4669 20.4926 16.4669C20.0222 16.4669 19.6401 16.0847 19.6401 15.6143V2.98254C19.6401 1.88957 18.7505 1 17.6575 1C16.5645 1 15.675 1.88957 15.675 2.98254V19.0175C15.675 19.4879 15.2928 19.8701 14.8224 19.8701C14.3519 19.8701 13.9698 19.4879 13.9698 19.0175V17.0329C13.9698 16.7206 13.7171 16.4679 13.4048 16.4679C13.0925 16.4679 12.8398 16.7206 12.8398 17.0329V19.0175C12.8398 20.1104 13.7294 21 14.8224 21C15.9153 21 16.8049 20.1104 16.8049 19.0175V2.98254C16.8049 2.51207 17.187 2.12994 17.6575 2.12994C18.128 2.12994 18.5101 2.51207 18.5101 2.98254V15.6143C18.5101 16.7072 19.3997 17.5968 20.4926 17.5968C21.5856 17.5968 22.4752 16.7072 22.4752 15.6143V8.04469C22.4752 7.57422 22.8573 7.19209 23.3278 7.19209C23.7982 7.19209 24.1804 7.57422 24.1804 8.04469V14.6775C24.1804 14.9897 24.4331 15.2424 24.7453 15.2424C25.0576 15.2424 25.3103 14.9897 25.3103 14.6775V8.04469C25.3103 6.95172 24.4207 6.06215 23.3278 6.06215Z' fill='%23F8F8F8'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_3442_102412'%3E%3Crect width='25' height='22' fill='white' transform='translate(0.5)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E"); | |
background-repeat: no-repeat; | |
background-position: center; | |
} | |
.minimax-ball-text { | |
font-size: 12px; | |
font-weight: 500; | |
white-space: nowrap; | |
} | |
.minimax-close-icon { | |
margin-left: 8px; | |
font-size: 16px; | |
width: 18px; | |
height: 18px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 50%; | |
cursor: pointer; | |
opacity: 0.7; | |
transition: opacity 0.2s ease; | |
} | |
.minimax-close-icon:hover { | |
opacity: 1; | |
} | |
</style> | |
<div id="minimax-floating-ball"> | |
<div class="minimax-ball-content"> | |
<div class="minimax-logo-wave"></div> | |
<span class="minimax-ball-text">Created by MiniMax Agent</span> | |
</div> | |
<div class="minimax-close-icon">×</div> | |
</div> | |
<script> | |
// Initialize floating ball functionality | |
function initFloatingBall() { | |
const ball = document.getElementById('minimax-floating-ball'); | |
if (!ball) return; | |
// Initial animation | |
ball.style.opacity = '0'; | |
ball.style.transform = 'translateY(20px)'; | |
setTimeout(() => { | |
ball.style.opacity = '1'; | |
ball.style.transform = 'translateY(0)'; | |
}, 500); | |
// Handle logo click | |
const ballContent = ball.querySelector('.minimax-ball-content'); | |
ballContent.addEventListener('click', function (e) { | |
e.stopPropagation(); | |
window.open('https://agent.minimax.io/agent', '_blank'); | |
ball.style.transform = 'scale(0.95)'; | |
setTimeout(() => { | |
ball.style.transform = 'scale(1)'; | |
}, 100); | |
}); | |
// Handle close button click | |
const closeIcon = ball.querySelector('.minimax-close-icon'); | |
closeIcon.addEventListener('click', function (e) { | |
e.stopPropagation(); | |
ball.style.opacity = '0'; | |
ball.style.transform = 'translateY(20px)'; | |
setTimeout(() => { | |
ball.style.display = 'none'; | |
}, 300); | |
}); | |
} | |
// Initialize when DOM is ready | |
document.addEventListener('DOMContentLoaded', initFloatingBall); | |
</script> | |
</body> | |
</html> | |