Spaces:
Running
Running
ok, give me a straight forward design brief for this product, using separate html and python server, draggable mini-player, with play/pause next/previous random shuffle icon, video "x of y", main page has a title header, search bar, under the search bar is a flex-grid (variable zoom upto 8x8 cards - use "+" and "-" buttons) displaying search results using CSS cards, video thumbnail, duration, video title, video author or channel name, green background signifying it is downloaded, each card will have a remove "X" circle button on the lower right corner, when user enters text and clicks search a modal opens and displays the results in a search grid (this search grid is to be the same format as the playlist flex-grid - css cards will have a red background for 'not downloaded', a remove "x" circle button to remove it from the search results list, user can click a card to add it to the playlist when selected a card in the search results display gets a green glowing border. Search modal needs a 'back' button, returning the user to the playlist, where they can press 'download new' to start the downloads. Theuser must be able to acess the search AND playlist while a video is playing.
5f4cbfa
verified
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize animations | |
| const animatedElements = document.querySelectorAll('.fade-in, .slide-up'); | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.classList.add('animate'); | |
| observer.unobserve(entry.target); | |
| } | |
| }); | |
| }, { | |
| threshold: 0.1 | |
| }); | |
| animatedElements.forEach(el => observer.observe(el)); | |
| // Load playlist | |
| fetch('/api/playlist') | |
| .then(response => response.json()) | |
| .then(data => renderPlaylist(data)); | |
| // Search functionality | |
| document.getElementById('search-btn').addEventListener('click', () => { | |
| const query = document.getElementById('search-input').value.trim(); | |
| if (query) { | |
| fetch('/api/search', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ query }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => renderSearchResults(data)); | |
| } | |
| }); | |
| // Playlist item click handler | |
| document.getElementById('playlist-grid').addEventListener('click', (e) => { | |
| if (e.target.classList.contains('remove-btn') || e.target.closest('.remove-btn')) { | |
| const card = e.target.closest('.music-card'); | |
| const id = parseInt(card.dataset.id); | |
| removeFromPlaylist(id); | |
| } | |
| }); | |
| // Search result click handler | |
| document.getElementById('search-grid').addEventListener('click', (e) => { | |
| const card = e.target.closest('.music-card'); | |
| if (!card) return; | |
| if (e.target.classList.contains('remove-btn') || e.target.closest('.remove-btn')) { | |
| card.remove(); | |
| } else { | |
| const id = parseInt(card.dataset.id); | |
| addToPlaylist(id); | |
| card.classList.add('selected'); | |
| setTimeout(() => card.classList.remove('selected'), 1000); | |
| } | |
| }); | |
| }); | |
| function renderPlaylist(songs) { | |
| const grid = document.getElementById('playlist-grid'); | |
| grid.innerHTML = ''; | |
| songs.forEach(song => { | |
| const card = createMusicCard(song, true); | |
| grid.appendChild(card); | |
| }); | |
| } | |
| function renderSearchResults(songs) { | |
| const grid = document.getElementById('search-grid'); | |
| grid.innerHTML = ''; | |
| songs.forEach(song => { | |
| const card = createMusicCard(song, false); | |
| grid.appendChild(card); | |
| }); | |
| } | |
| function createMusicCard(song, isPlaylist) { | |
| const card = document.createElement('div'); | |
| card.className = `music-card ${song.downloaded ? 'downloaded' : 'not-downloaded'}`; | |
| card.dataset.id = song.id; | |
| card.innerHTML = ` | |
| <img src="${song.thumbnail}" class="thumbnail"> | |
| <span class="duration">${song.duration}</span> | |
| <div class="remove-btn"><i data-feather="x"></i></div> | |
| <div class="info"> | |
| <div class="title">${song.title}</div> | |
| <div class="artist">${song.artist}</div> | |
| </div> | |
| `; | |
| if (!isPlaylist) { | |
| card.addEventListener('click', () => { | |
| document.querySelectorAll('#search-grid .music-card').forEach(c => { | |
| c.classList.remove('selected'); | |
| }); | |
| card.classList.add('selected'); | |
| addToPlaylist(song.id); | |
| }); | |
| } | |
| return card; | |
| } | |
| function addToPlaylist(id) { | |
| fetch('/api/add_to_playlist', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ id }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| fetch('/api/playlist') | |
| .then(response => response.json()) | |
| .then(data => renderPlaylist(data)); | |
| } | |
| }); | |
| } | |
| function removeFromPlaylist(id) { | |
| fetch('/api/remove_from_playlist', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ id }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| fetch('/api/playlist') | |
| .then(response => response.json()) | |
| .then(data => renderPlaylist(data)); | |
| } | |
| }); | |
| } | |