diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..943c3b3 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,607 @@ +:root { + --primary-color: #3498db; + --accent-color: #2ecc71; + --light-bg: #ecf0f1; + --light-surface: #ffffff; + --light-text: #2c3e50; + --dark-bg: #2c3e50; + --dark-surface: #34495e; + --dark-text: #ecf0f1; + --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + font-family: var(--font-family); + margin: 0; + padding: 0; + overflow: hidden; + transition: background-color 0.3s, color 0.3s; +} + +body.light-theme { + background-color: var(--light-bg); + color: var(--light-text); + --bg-color: var(--light-bg); + --surface-color: var(--light-surface); + --text-color: var(--light-text); +} + +body.dark-theme { + background-color: var(--dark-bg); + color: var(--dark-text); + --bg-color: var(--dark-bg); + --surface-color: var(--dark-surface); + --text-color: var(--dark-text); +} + +#app-container { + display: flex; + height: 100vh; +} + +/* --- Sidebar --- */ +#sidebar { + width: 350px; + background-color: var(--surface-color); + display: flex; + flex-direction: column; + border-right: 1px solid rgba(0,0,0,0.1); + transition: background-color 0.3s; +} + +#sidebar-header { + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0,0,0,0.1); +} + +#sidebar-header h1 { + margin: 0; + font-size: 24px; + color: var(--primary-color); +} + +/* Theme Switcher */ +.theme-switcher { + display: flex; + align-items: center; + gap: 8px; +} +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 20px; +} +.switch input { display: none; } +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 20px; +} +.slider:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; +} +input:checked + .slider { + background-color: var(--primary-color); +} +input:checked + .slider:before { + transform: translateX(20px); +} + +#sidebar-tabs { + display: flex; + border-bottom: 1px solid rgba(0,0,0,0.1); +} + +.tab-link { + flex: 1; + padding: 15px; + background: none; + border: none; + font-size: 16px; + cursor: pointer; + color: var(--text-color); + opacity: 0.7; + transition: opacity 0.2s, border-bottom 0.2s; + border-bottom: 3px solid transparent; +} + +.tab-link.active { + opacity: 1; + border-bottom: 3px solid var(--primary-color); +} + +.tab-content { + display: none; + flex-grow: 1; + overflow-y: auto; + flex-direction: column; +} + +.tab-content.active { + display: flex; +} + +.toolbar { + padding: 15px; + border-bottom: 1px solid rgba(0,0,0,0.1); +} + +.search-bar { + position: relative; + margin-bottom: 10px; +} + +.search-bar input { + width: 100%; + padding: 10px 10px 10px 35px; + border-radius: 5px; + border: 1px solid #ccc; + background-color: var(--bg-color); + color: var(--text-color); + box-sizing: border-box; +} + +.search-bar .fa-search { + position: absolute; + top: 12px; + left: 12px; + color: #aaa; +} + +#genre-filter { + width: 100%; + padding: 10px; + border-radius: 5px; + border: 1px solid #ccc; + background-color: var(--bg-color); + color: var(--text-color); +} + +#station-list, #discover-list { + flex-grow: 1; + overflow-y: auto; + padding: 10px; +} + +.station-item, .discover-item { + display: flex; + align-items: center; + padding: 15px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; + margin-bottom: 5px; +} + +.station-item:hover, .discover-item:hover { + background-color: rgba(0,0,0,0.1); +} + +.station-item.playing { + background-color: var(--primary-color); + color: white; +} + +.station-item-info { + flex-grow: 1; +} + +.station-item-info h3 { + margin: 0; + font-size: 16px; +} + +.station-item-info p { + margin: 0; + font-size: 12px; + opacity: 0.7; +} + +.station-item-controls button, .discover-item-controls button { + background: none; + border: none; + color: var(--text-color); + cursor: pointer; + font-size: 16px; + margin-left: 10px; + opacity: 0.7; + transition: opacity 0.2s; +} + +.station-item.playing .station-item-controls button { + color: white; +} + +.station-item-controls button:hover, .discover-item-controls button:hover { + opacity: 1; +} + +.sidebar-btn { + padding: 15px; + background-color: var(--accent-color); + color: white; + border: none; + font-size: 16px; + cursor: pointer; + transition: background-color 0.2s; +} + +.sidebar-btn:hover { + filter: brightness(1.1); +} + +/* --- Main Content --- */ +#main-content { + flex-grow: 1; + display: flex; + flex-direction: column; + background-color: var(--bg-color); +} + +#visualizer-container { + flex-grow: 1; + position: relative; + background-color: #000; +} + +#visualizer { + width: 100%; + height: 100%; +} + +#player-container { + padding: 30px; + background-color: var(--surface-color); + border-top: 1px solid rgba(0,0,0,0.1); + text-align: center; +} + +#now-playing { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; +} + +#now-playing img { + width: 80px; + height: 80px; + border-radius: 5px; + margin-right: 20px; +} + +#now-playing h2 { + margin: 0; + font-size: 28px; +} + +#now-playing p { + margin: 0; + font-size: 16px; + opacity: 0.7; +} + +#player-controls { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin-bottom: 20px; +} + +#player-controls button { + background: none; + border: 1px solid var(--text-color); + color: var(--text-color); + border-radius: 50%; + width: 60px; + height: 60px; + font-size: 24px; + cursor: pointer; + transition: all 0.2s; +} + +#player-controls button#play-pause-btn { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: white; + width: 70px; + height: 70px; +} + +#player-controls button:hover { + transform: scale(1.1); +} + +.volume-control { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 300px; + margin: 0 auto; +} + +.volume-control input[type="range"] { + flex-grow: 1; +} + +#recommendations-container { + padding: 20px; + background-color: var(--bg-color); + border-top: 1px solid rgba(0,0,0,0.1); +} + +#recommendations-container h3 { + margin-top: 0; + text-align: center; +} + +#recommendations-list { + display: flex; + gap: 15px; + justify-content: center; +} + +.rec-item { + background-color: var(--surface-color); + padding: 15px; + border-radius: 5px; + text-align: center; + cursor: pointer; + transition: transform 0.2s; +} + +.rec-item:hover { + transform: translateY(-5px); +} + +/* --- App Controls --- */ +#app-controls { + position: absolute; + top: 20px; + right: 20px; + display: flex; + gap: 10px; +} + +#app-controls button { + background-color: var(--surface-color); + border: 1px solid #ccc; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 18px; + cursor: pointer; + color: var(--text-color); + transition: all 0.2s; +} + +#app-controls button:hover { + background-color: var(--primary-color); + color: white; +} + +/* --- Modals --- */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + justify-content: center; + align-items: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background-color: var(--surface-color); + padding: 30px; + border-radius: 10px; + width: 90%; + max-width: 500px; + position: relative; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +.close-btn { + position: absolute; + top: 15px; + right: 15px; + font-size: 24px; + font-weight: bold; + cursor: pointer; +} + +#station-form label, .color-picker-section h3, #import-export-modal h3 { + display: block; + margin-bottom: 10px; + font-weight: bold; +} + +#station-form input { + width: 100%; + padding: 10px; + margin-bottom: 15px; + border-radius: 5px; + border: 1px solid #ccc; + box-sizing: border-box; + background-color: var(--bg-color); + color: var(--text-color); +} + +#station-form button, .modal-content button { + width: 100%; + padding: 12px; + border: none; + border-radius: 5px; + background-color: var(--primary-color); + color: white; + font-size: 16px; + cursor: pointer; + transition: filter 0.2s; +} + +#station-form button:hover, .modal-content button:hover { + filter: brightness(1.1); +} + +.color-picker-section .color-input { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.color-picker-section input[type="color"] { + width: 50px; + height: 30px; + border: none; + padding: 0; + cursor: pointer; +} + +#reset-colors-btn { + background-color: #95a5a6; + margin-top: 10px; +} + +#timer-options { + display: flex; + justify-content: space-around; + margin: 20px 0; +} + +#timer-options button { + width: auto; + padding: 10px 20px; +} + +#timer-display { + text-align: center; + font-size: 18px; + margin: 20px 0; +} + +#cancel-timer-btn { + background-color: #e74c3c; +} + +#import-export-modal .import-section, #import-export-modal .export-section { + margin-bottom: 20px; +} + +#import-export-modal input[type="file"] { + display: block; + margin: 10px 0; +} + +/* --- Equalizer --- */ +#equalizer-controls { + display: flex; + flex-direction: column; + gap: 20px; +} + +.eq-presets { + display: flex; + align-items: center; + gap: 10px; +} + +#eq-presets-select { + padding: 8px; + border-radius: 5px; + border: 1px solid #ccc; + background-color: var(--bg-color); + color: var(--text-color); +} + +#eq-bands { + display: flex; + justify-content: space-around; + align-items: flex-end; + height: 200px; + gap: 10px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; +} + +.eq-band { + display: flex; + flex-direction: column; + align-items: center; + flex-grow: 1; +} + +.eq-band input[type="range"] { + -webkit-appearance: none; + appearance: none; + width: 150px; + height: 8px; + transform: rotate(-90deg); + transform-origin: 75px 75px; + background: #ddd; + border-radius: 5px; + outline: none; + opacity: 0.7; + transition: opacity .2s; +} + +.eq-band input[type="range"]:hover { + opacity: 1; +} + +.eq-band label { + font-size: 12px; + margin-top: 10px; +} + +#reset-eq-btn { + background-color: #95a5a6; +} + +#player-controls button#record-btn { + color: #e74c3c; + border-color: #e74c3c; +} + +#player-controls button#record-btn.recording { + background-color: #e74c3c; + color: white; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); + } +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..16910d1 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,720 @@ +class RadioWaveApp { + constructor() { + this.audio = document.getElementById('audio-player'); + this.stations = []; + this.currentStationIndex = -1; + this.discoverStations = this.getDiscoverStations(); + this.audioContext = null; + this.analyser = null; + this.source = null; + this.sleepTimer = null; + this.mediaRecorder = null; + this.recordedChunks = []; + this.eqBands = []; + + this.loadStations(); + this.loadTheme(); + this.loadColors(); + this.initUI(); + this.bindEvents(); + this.renderStations(); + this.renderDiscoverStations(); + } + + initUI() { + // Cache all UI elements + this.ui = { + stationList: document.getElementById('station-list'), + discoverList: document.getElementById('discover-list'), + playPauseBtn: document.getElementById('play-pause-btn'), + nextBtn: document.getElementById('next-station-btn'), + prevBtn: document.getElementById('prev-station-btn'), + recordBtn: document.getElementById('record-btn'), + volumeSlider: document.getElementById('volume-slider'), + playerStationName: document.getElementById('player-station-name'), + playerStationGenre: document.getElementById('player-station-genre'), + stationArt: document.getElementById('station-art'), + searchInput: document.getElementById('search-input'), + genreFilter: document.getElementById('genre-filter'), + themeToggle: document.getElementById('theme-toggle'), + visualizer: document.getElementById('visualizer'), + // Modals + stationModal: document.getElementById('station-modal'), + settingsModal: document.getElementById('settings-modal'), + sleepTimerModal: document.getElementById('sleep-timer-modal'), + importExportModal: document.getElementById('import-export-modal'), + equalizerModal: document.getElementById('equalizer-modal'), + // Modal Triggers + addStationBtn: document.getElementById('add-station-btn'), + settingsBtn: document.getElementById('settings-btn'), + sleepTimerBtn: document.getElementById('sleep-timer-btn'), + importExportBtn: document.getElementById('import-export-btn'), + equalizerBtn: document.getElementById('equalizer-btn'), + // Tabs + sidebarTabs: document.querySelectorAll('.tab-link'), + tabContents: document.querySelectorAll('.tab-content'), + // Recommendations + recommendationsList: document.getElementById('recommendations-list'), + // EQ + eqBandsContainer: document.getElementById('eq-bands'), + eqPresetsSelect: document.getElementById('eq-presets-select'), + resetEqBtn: document.getElementById('reset-eq-btn'), + }; + + this.canvasCtx = this.ui.visualizer.getContext('2d'); + } + + bindEvents() { + // Player controls + this.ui.playPauseBtn.addEventListener('click', () => this.togglePlayPause()); + this.ui.nextBtn.addEventListener('click', () => this.playNext()); + this.ui.prevBtn.addEventListener('click', () => this.playPrevious()); + this.ui.recordBtn.addEventListener('click', () => this.toggleRecording()); + this.ui.volumeSlider.addEventListener('input', (e) => this.setVolume(e.target.value)); + + // Station list interactions + this.ui.stationList.addEventListener('click', (e) => this.handleStationListClick(e)); + this.ui.discoverList.addEventListener('click', (e) => this.handleDiscoverListClick(e)); + + // Search and filter + this.ui.searchInput.addEventListener('input', (e) => this.renderStations(e.target.value, this.ui.genreFilter.value)); + this.ui.genreFilter.addEventListener('change', (e) => this.renderStations(this.ui.searchInput.value, e.target.value)); + + // Theme and settings + this.ui.themeToggle.addEventListener('change', () => this.toggleTheme()); + this.bindModalEvents(); + this.bindColorPickerEvents(); + this.bindTimerEvents(); + this.bindImportExportEvents(); + this.bindEqualizerEvents(); + + // Tabs + this.ui.sidebarTabs.forEach(tab => { + tab.addEventListener('click', () => this.switchTab(tab.dataset.tab)); + }); + + // Audio events + this.audio.addEventListener('play', () => this.ui.playPauseBtn.innerHTML = ''); + this.audio.addEventListener('pause', () => this.ui.playPauseBtn.innerHTML = ''); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => this.handleKeyPress(e)); + } + + bindModalEvents() { + const modals = document.querySelectorAll('.modal'); + const closeBtns = document.querySelectorAll('.close-btn'); + + this.ui.addStationBtn.addEventListener('click', () => this.openStationModal()); + this.ui.settingsBtn.addEventListener('click', () => this.ui.settingsModal.classList.add('active')); + this.ui.equalizerBtn.addEventListener('click', () => this.ui.equalizerModal.classList.add('active')); + this.ui.sleepTimerBtn.addEventListener('click', () => this.ui.sleepTimerModal.classList.add('active')); + this.ui.importExportBtn.addEventListener('click', () => this.ui.importExportModal.classList.add('active')); + + closeBtns.forEach(btn => btn.addEventListener('click', () => this.closeAllModals())); + modals.forEach(modal => modal.addEventListener('click', (e) => { + if (e.target === modal) this.closeAllModals(); + })); + + document.getElementById('station-form').addEventListener('submit', (e) => this.saveStation(e)); + } + + closeAllModals() { + document.querySelectorAll('.modal').forEach(m => m.classList.remove('active')); + } + + /* --- Station Management --- */ + + loadStations() { + const savedStations = localStorage.getItem('radioWaveStations'); + this.stations = savedStations ? JSON.parse(savedStations) : this.getDefaultStations(); + this.updateGenreFilter(); + } + + saveStations() { + localStorage.setItem('radioWaveStations', JSON.stringify(this.stations)); + this.updateGenreFilter(); + this.renderStations(); + } + + renderStations(searchTerm = '', genreTerm = 'all') { + this.ui.stationList.innerHTML = ''; + const filteredStations = this.stations.filter(station => { + const matchesSearch = station.name.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesGenre = genreTerm === 'all' || station.genre.toLowerCase() === genreTerm.toLowerCase(); + return matchesSearch && matchesGenre; + }); + + if (filteredStations.length === 0) { + this.ui.stationList.innerHTML = '

No stations found.

'; + return; + } + + filteredStations.forEach((station, index) => { + const isPlaying = this.currentStationIndex === this.stations.indexOf(station) && !this.audio.paused; + const item = document.createElement('div'); + item.className = `station-item ${isPlaying ? 'playing' : ''}`; + item.dataset.id = station.id; + + item.innerHTML = ` +
+

${station.name}

+

${station.genre || 'No Genre'}

+
+
+ + +
+ `; + this.ui.stationList.appendChild(item); + }); + } + + handleStationListClick(e) { + const stationItem = e.target.closest('.station-item'); + if (!stationItem) return; + + const stationId = stationItem.dataset.id; + const stationIndex = this.stations.findIndex(s => s.id == stationId); + + if (e.target.closest('.edit-btn')) { + this.openStationModal(stationId); + } else if (e.target.closest('.delete-btn')) { + if (confirm('Are you sure you want to delete this station?')) { + this.stations.splice(stationIndex, 1); + this.saveStations(); + } + } else { + this.playStation(stationIndex); + } + } + + openStationModal(stationId = null) { + const form = document.getElementById('station-form'); + const modalTitle = document.getElementById('modal-title'); + form.reset(); + document.getElementById('station-id').value = ''; + + if (stationId) { + const station = this.stations.find(s => s.id == stationId); + modalTitle.textContent = 'Edit Station'; + document.getElementById('station-id').value = station.id; + document.getElementById('station-name').value = station.name; + document.getElementById('station-url').value = station.url; + document.getElementById('station-genre').value = station.genre; + } else { + modalTitle.textContent = 'Add Station'; + } + this.ui.stationModal.classList.add('active'); + } + + saveStation(e) { + e.preventDefault(); + const id = document.getElementById('station-id').value; + const newStation = { + id: id || Date.now(), + name: document.getElementById('station-name').value, + url: document.getElementById('station-url').value, + genre: document.getElementById('station-genre').value, + }; + + if (id) { + const index = this.stations.findIndex(s => s.id == id); + this.stations[index] = newStation; + } else { + this.stations.push(newStation); + } + this.saveStations(); + this.closeAllModals(); + } + + updateGenreFilter() { + const genres = ['all', ...new Set(this.stations.map(s => s.genre).filter(g => g))]; + this.ui.genreFilter.innerHTML = genres.map(g => ``).join(''); + } + + /* --- Player --- */ + + playStation(index) { + if (index < 0 || index >= this.stations.length) return; + this.currentStationIndex = index; + const station = this.stations[index]; + this.audio.src = station.url; + this.audio.play().catch(e => { + alert("Failed to play station. The stream might be offline or blocked."); + console.error("Playback Error:", e); + }); + this.updatePlayerUI(station); + this.renderStations(); + this.setupAudioPipeline(); + this.updateRecommendations(); + } + + togglePlayPause() { + if (this.audio.paused) { + if (this.currentStationIndex === -1 && this.stations.length > 0) { + this.playStation(0); + } else { + this.audio.play(); + } + } else { + this.audio.pause(); + } + this.renderStations(); + } + + playNext() { + let nextIndex = this.currentStationIndex + 1; + if (nextIndex >= this.stations.length) nextIndex = 0; + this.playStation(nextIndex); + } + + playPrevious() { + let prevIndex = this.currentStationIndex - 1; + if (prevIndex < 0) prevIndex = this.stations.length - 1; + this.playStation(prevIndex); + } + + setVolume(value) { + this.audio.volume = value; + } + + updatePlayerUI(station) { + this.ui.playerStationName.textContent = station.name; + this.ui.playerStationGenre.textContent = station.genre || '---'; + this.ui.stationArt.src = 'assets/images/default-art.png'; + } + + /* --- Audio Pipeline (Visualizer & EQ) --- */ + + setupAudioPipeline() { + if (this.audioContext) return; // Already set up + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.source = this.audioContext.createMediaElementSource(this.audio); + this.analyser = this.audioContext.createAnalyser(); + this.analyser.fftSize = 256; + + this.initEqualizer(); + + // Connect nodes: source -> EQ -> analyser -> destination + let lastNode = this.source; + this.eqBands.forEach(band => { + lastNode.connect(band); + lastNode = band; + }); + lastNode.connect(this.analyser); + this.analyser.connect(this.audioContext.destination); + + this.drawVisualizer(); + } catch (e) { + console.error('Web Audio API setup failed.', e); + this.ui.visualizer.style.display = 'none'; + this.ui.equalizerBtn.style.display = 'none'; + } + } + + drawVisualizer() { + requestAnimationFrame(() => this.drawVisualizer()); + if (!this.analyser) return; + + const bufferLength = this.analyser.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + this.analyser.getByteFrequencyData(dataArray); + + this.canvasCtx.fillStyle = '#000'; + this.canvasCtx.fillRect(0, 0, this.ui.visualizer.width, this.ui.visualizer.height); + + const barWidth = (this.ui.visualizer.width / bufferLength) * 2.5; + let barHeight; + let x = 0; + + for (let i = 0; i < bufferLength; i++) { + barHeight = dataArray[i]; + const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); + const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-color').trim(); + const gradient = this.canvasCtx.createLinearGradient(0, this.ui.visualizer.height, 0, this.ui.visualizer.height - barHeight); + gradient.addColorStop(0, primaryColor); + gradient.addColorStop(1, accentColor); + this.canvasCtx.fillStyle = gradient; + this.canvasCtx.fillRect(x, this.ui.visualizer.height - barHeight / 2, barWidth, barHeight / 2); + x += barWidth + 1; + } + } + + /* --- Equalizer --- */ + + initEqualizer() { + const frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]; + this.ui.eqBandsContainer.innerHTML = ''; + frequencies.forEach((freq, i) => { + const band = this.audioContext.createBiquadFilter(); + band.type = 'peaking'; + band.frequency.value = freq; + band.Q.value = 1; + band.gain.value = 0; + this.eqBands.push(band); + + const bandElement = document.createElement('div'); + bandElement.className = 'eq-band'; + bandElement.innerHTML = ` + + + `; + this.ui.eqBandsContainer.appendChild(bandElement); + }); + } + + bindEqualizerEvents() { + this.ui.eqBandsContainer.addEventListener('input', (e) => { + if (e.target.type === 'range') { + const index = e.target.dataset.index; + const value = e.target.value; + this.eqBands[index].gain.value = value; + this.ui.eqPresetsSelect.value = 'custom'; + } + }); + this.ui.eqPresetsSelect.addEventListener('change', (e) => this.applyEQPreset(e.target.value)); + this.ui.resetEqBtn.addEventListener('click', () => this.applyEQPreset('flat')); + } + + applyEQPreset(preset) { + const presets = { + 'flat': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'bass-boost': [6, 5, 4, 2, 0, 0, 0, 0, 0, 0], + 'rock': [4, 2, -2, -3, -1, 2, 4, 5, 5, 6], + 'pop': [-1, -2, 0, 2, 4, 4, 2, 0, -1, -2], + 'vocal-booster': [0, 0, 0, -2, -2, 4, 4, 2, 0, 0], + 'custom': null + }; + const values = presets[preset]; + if (!values) return; + + this.ui.eqPresetsSelect.value = preset; + const sliders = this.ui.eqBandsContainer.querySelectorAll('input[type="range"]'); + sliders.forEach((slider, i) => { + slider.value = values[i]; + this.eqBands[i].gain.value = values[i]; + }); + } + + /* --- Recording --- */ + + toggleRecording() { + if (this.mediaRecorder && this.mediaRecorder.state === 'recording') { + this.mediaRecorder.stop(); + this.ui.recordBtn.classList.remove('recording'); + this.ui.recordBtn.title = 'Start Recording'; + } else { + if (!this.source) { + alert('Please start playing a station first.'); + return; + } + try { + const dest = this.audioContext.createMediaStreamDestination(); + this.analyser.connect(dest); // Record post-EQ and post-analyser + this.mediaRecorder = new MediaRecorder(dest.stream); + + this.recordedChunks = []; + this.mediaRecorder.ondataavailable = (e) => { + if (e.data.size > 0) this.recordedChunks.push(e.data); + }; + + this.mediaRecorder.onstop = () => { + const blob = new Blob(this.recordedChunks, { type: 'audio/webm' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `radiowave-recording-${new Date().toISOString()}.webm`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }; + + this.mediaRecorder.start(); + this.ui.recordBtn.classList.add('recording'); + this.ui.recordBtn.title = 'Stop Recording'; + } catch (e) { + alert('Recording failed to start. Your browser may not support it.'); + console.error('Recording Error:', e); + } + } + } + + /* --- Keyboard Shortcuts --- */ + handleKeyPress(e) { + // Ignore key presses in input fields + if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; + + switch (e.code) { + case 'Space': + e.preventDefault(); + this.togglePlayPause(); + break; + case 'ArrowRight': + this.playNext(); + break; + case 'ArrowLeft': + this.playPrevious(); + break; + case 'ArrowUp': + e.preventDefault(); + this.setVolume(Math.min(1, this.audio.volume + 0.05)); + this.ui.volumeSlider.value = this.audio.volume; + break; + case 'ArrowDown': + e.preventDefault(); + this.setVolume(Math.max(0, this.audio.volume - 0.05)); + this.ui.volumeSlider.value = this.audio.volume; + break; + } + } + + /* --- Theme & Colors --- */ + + loadTheme() { + const theme = localStorage.getItem('radioWaveTheme') || 'dark'; + document.body.className = theme + '-theme'; + this.ui.themeToggle.checked = theme === 'dark'; + } + + toggleTheme() { + const isDark = this.ui.themeToggle.checked; + const theme = isDark ? 'dark' : 'light'; + document.body.className = theme + '-theme'; + localStorage.setItem('radioWaveTheme', theme); + } + + bindColorPickerEvents() { + const root = document.documentElement; + const colorInputs = { + '--primary-color': document.getElementById('primary-color'), + '--accent-color': document.getElementById('accent-color'), + '--light-text': document.getElementById('text-color-light'), + '--dark-text': document.getElementById('text-color-dark'), + }; + + for (const [prop, input] of Object.entries(colorInputs)) { + input.addEventListener('input', (e) => { + root.style.setProperty(prop, e.target.value); + this.saveColors(); + }); + } + + document.getElementById('reset-colors-btn').addEventListener('click', () => this.resetColors()); + } + + saveColors() { + const rootStyles = getComputedStyle(document.documentElement); + const colors = { + primary: rootStyles.getPropertyValue('--primary-color').trim(), + accent: rootStyles.getPropertyValue('--accent-color').trim(), + textLight: rootStyles.getPropertyValue('--light-text').trim(), + textDark: rootStyles.getPropertyValue('--dark-text').trim(), + }; + localStorage.setItem('radioWaveColors', JSON.stringify(colors)); + } + + loadColors() { + const savedColors = JSON.parse(localStorage.getItem('radioWaveColors')); + if (savedColors) { + const root = document.documentElement; + root.style.setProperty('--primary-color', savedColors.primary); + root.style.setProperty('--accent-color', savedColors.accent); + root.style.setProperty('--light-text', savedColors.textLight); + root.style.setProperty('--dark-text', savedColors.textDark); + + document.getElementById('primary-color').value = savedColors.primary; + document.getElementById('accent-color').value = savedColors.accent; + document.getElementById('text-color-light').value = savedColors.textLight; + document.getElementById('text-color-dark').value = savedColors.textDark; + } + } + + resetColors() { + localStorage.removeItem('radioWaveColors'); + window.location.reload(); + } + + /* --- Advanced Features --- */ + + switchTab(tabId) { + this.ui.tabContents.forEach(content => content.classList.remove('active')); + this.ui.sidebarTabs.forEach(tab => tab.classList.remove('active')); + document.getElementById(tabId).classList.add('active'); + document.querySelector(`[data-tab='${tabId}']`).classList.add('active'); + } + + bindTimerEvents() { + const timerDisplay = document.getElementById('timer-display'); + document.querySelectorAll('.timer-option').forEach(btn => { + btn.addEventListener('click', () => { + const minutes = parseInt(btn.dataset.minutes, 10); + this.setSleepTimer(minutes); + timerDisplay.textContent = `Stops in ${minutes} minutes.`; + }); + }); + document.getElementById('cancel-timer-btn').addEventListener('click', () => { + this.cancelSleepTimer(); + timerDisplay.textContent = 'Timer not set'; + }); + } + + setSleepTimer(minutes) { + this.cancelSleepTimer(); + this.sleepTimer = setTimeout(() => { + this.audio.pause(); + document.getElementById('timer-display').textContent = 'Timer finished'; + }, minutes * 60 * 1000); + } + + cancelSleepTimer() { + if (this.sleepTimer) { + clearTimeout(this.sleepTimer); + this.sleepTimer = null; + } + } + + bindImportExportEvents() { + document.getElementById('export-btn').addEventListener('click', () => this.exportStations()); + document.getElementById('import-btn').addEventListener('click', () => document.getElementById('import-file-input').click()); + document.getElementById('import-file-input').addEventListener('change', (e) => this.importStations(e)); + } + + exportStations() { + const dataStr = JSON.stringify(this.stations, null, 2); + const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); + const exportFileDefaultName = 'radiowave_stations.json'; + let linkElement = document.createElement('a'); + linkElement.setAttribute('href', dataUri); + linkElement.setAttribute('download', exportFileDefaultName); + linkElement.click(); + } + + importStations(event) { + const file = event.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + try { + const importedStations = JSON.parse(e.target.result); + if (Array.isArray(importedStations)) { + this.stations = importedStations; + this.saveStations(); + this.closeAllModals(); + alert('Stations imported successfully!'); + } else { + alert('Invalid file format.'); + } + } catch (err) { + alert('Error reading file: ' + err.message); + } + }; + reader.readAsText(file); + } + + /* --- Discovery & Recommendations --- */ + + renderDiscoverStations() { + this.ui.discoverList.innerHTML = ''; + this.discoverStations.forEach(station => { + const item = document.createElement('div'); + item.className = 'discover-item'; + item.dataset.name = station.name; + item.dataset.url = station.url; + item.dataset.genre = station.genre; + item.innerHTML = ` +
+

${station.name}

+

${station.genre}

+
+
+ +
+ `; + this.ui.discoverList.appendChild(item); + }); + } + + handleDiscoverListClick(e) { + const item = e.target.closest('.discover-item'); + if (!item) return; + + const newStation = { + id: Date.now(), + name: item.dataset.name, + url: item.dataset.url, + genre: item.dataset.genre, + }; + + if (this.stations.some(s => s.url === newStation.url)) { + alert('This station is already in your list.'); + return; + } + + this.stations.push(newStation); + this.saveStations(); + alert(`'${newStation.name}' has been added to your stations!`); + this.switchTab('my-stations-tab'); + } + + updateRecommendations() { + this.ui.recommendationsList.innerHTML = ''; + if (this.currentStationIndex === -1) return; + + const currentGenre = this.stations[this.currentStationIndex].genre; + if (!currentGenre) return; + + const recommendations = this.discoverStations.filter(s => s.genre.toLowerCase() === currentGenre.toLowerCase() && !this.stations.some(myS => myS.url === s.url)).slice(0, 3); + + if (recommendations.length === 0) { + this.ui.recommendationsList.innerHTML = '

No recommendations right now.

'; + return; + } + + recommendations.forEach(station => { + const item = document.createElement('div'); + item.className = 'rec-item'; + item.dataset.name = station.name; + item.dataset.url = station.url; + item.dataset.genre = station.genre; + item.innerHTML = `

${station.name}

${station.genre}

`; + item.addEventListener('click', () => { + if (confirm(`Add '${station.name}' to your stations?`)) { + this.handleDiscoverListClick({ target: item }); + } + }); + this.ui.recommendationsList.appendChild(item); + }); + } + + /* --- Default Data --- */ + + getDefaultStations() { + return [ + { id: 1, name: 'Lofi Girl', url: 'https://stream.lofigirl.com/lofi', genre: 'Lofi' }, + { id: 2, name: 'BBC Radio 1', url: 'http://stream.live.vc.bbcmedia.co.uk/bbc_radio_one', genre: 'Pop' }, + { id: 3, name: 'KEXP 90.3 FM', url: 'https://kexp-mp3-128.streamguys1.com/kexp128.mp3', genre: 'Indie' }, + ]; + } + + getDiscoverStations() { + return [ + { name: 'NTS Radio', url: 'http://stream-relay-geo.ntslive.net/stream', genre: 'Electronic' }, + { name: 'Dublab', url: 'https://dublab.out.airtime.pro/dublab_128.mp3', genre: 'Eclectic' }, + { name: 'Worldwide FM', url: 'https://worldwidefm.out.airtime.pro/worldwidefm_128.mp3', genre: 'World' }, + { name: 'The Lot Radio', url: 'https://thelot.out.airtime.pro/thelot_128.mp3', genre: 'Dance' }, + { name: 'Rinse FM', url: 'https://rinse.fm/streams/rinse.mp3', genre: 'Grime' }, + { name: 'Jazz24', url: 'https://jazz24.streamguys1.com/jazz24.mp3', genre: 'Jazz' }, + { name: 'SomaFM: Groove Salad', url: 'http://ice1.somafm.com/groovesalad-128-mp3', genre: 'Ambient' }, + { name: 'Classical KUSC', url: 'https://kusc.streamguys1.com/kusc-128.mp3', genre: 'Classical' }, + { name: 'Rock FM', url: 'https://live.rockfm.ro/rockfm.aacp', genre: 'Rock' }, + { name: 'Radio Paradise', url: 'http://stream.radioparadise.com/mp3-128', genre: 'Rock' }, + ]; + } +} + +document.addEventListener('DOMContentLoaded', () => { + new RadioWaveApp(); +}); \ No newline at end of file diff --git a/index.php b/index.php index 7205f3d..62ed1d1 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,215 @@ - - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + Radio Wave + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+ + + + +
+
+ +
+
+
+ Station Art +
+

Select a Station

+

---

+
+
+
+ + + + +
+
+ + + +
+
+
+

You might also like...

+
+ +
+
+
+ + +
+ + + + +
-
- + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file