+ Select a Station
+---
+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.genre || 'No Genre'}
+${station.genre}
+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.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 @@ - - + - - -= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-This page will update automatically as the plan is implemented.
-Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+ ---
+