CCX
This commit is contained in:
parent
806416f85d
commit
2fad95a89a
607
assets/css/style.css
Normal file
607
assets/css/style.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
720
assets/js/main.js
Normal file
720
assets/js/main.js
Normal file
@ -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 = '<i class="fas fa-pause"></i>');
|
||||
this.audio.addEventListener('pause', () => this.ui.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>');
|
||||
|
||||
// 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 = '<p style="text-align:center; opacity: 0.7;">No stations found.</p>';
|
||||
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 = `
|
||||
<div class="station-item-info">
|
||||
<h3>${station.name}</h3>
|
||||
<p>${station.genre || 'No Genre'}</p>
|
||||
</div>
|
||||
<div class="station-item-controls">
|
||||
<button class="edit-btn"><i class="fas fa-pencil-alt"></i></button>
|
||||
<button class="delete-btn"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
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 => `<option value="${g}">${g.charAt(0).toUpperCase() + g.slice(1)}</option>`).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 = `
|
||||
<input type="range" min="-20" max="20" step="1" value="0" data-index="${i}">
|
||||
<label>${freq < 1000 ? freq + 'Hz' : (freq / 1000) + 'k'}</label>
|
||||
`;
|
||||
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 = `
|
||||
<div class="station-item-info">
|
||||
<h3>${station.name}</h3>
|
||||
<p>${station.genre}</p>
|
||||
</div>
|
||||
<div class="discover-item-controls">
|
||||
<button class="add-from-discover-btn"><i class="fas fa-plus-circle"></i></button>
|
||||
</div>
|
||||
`;
|
||||
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 = '<p style="opacity:0.7">No recommendations right now.</p>';
|
||||
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 = `<h4>${station.name}</h4><p>${station.genre}</p>`;
|
||||
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();
|
||||
});
|
||||
351
index.php
351
index.php
@ -1,150 +1,215 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Radio Wave</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
<div id="app-container">
|
||||
<!-- Sidebar with station lists -->
|
||||
<div id="sidebar">
|
||||
<div id="sidebar-header">
|
||||
<h1>Radio Wave</h1>
|
||||
<div class="theme-switcher">
|
||||
<i class="fas fa-sun"></i>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="theme-toggle">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<i class="fas fa-moon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebar-tabs">
|
||||
<button class="tab-link active" data-tab="my-stations-tab">My Stations</button>
|
||||
<button class="tab-link" data-tab="discover-tab">Discover</button>
|
||||
</div>
|
||||
|
||||
<!-- My Stations Tab -->
|
||||
<div id="my-stations-tab" class="tab-content active">
|
||||
<div class="toolbar">
|
||||
<div class="search-bar">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="search-input" placeholder="Search stations...">
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<select id="genre-filter">
|
||||
<option value="all">All Genres</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="station-list">
|
||||
<!-- Stations will be dynamically loaded here -->
|
||||
</div>
|
||||
<button id="add-station-btn" class="sidebar-btn"><i class="fas fa-plus"></i> Add New Station</button>
|
||||
</div>
|
||||
|
||||
<!-- Discover Tab -->
|
||||
<div id="discover-tab" class="tab-content">
|
||||
<div class="toolbar">
|
||||
<div class="search-bar">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="discover-search-input" placeholder="Search global library...">
|
||||
</div>
|
||||
</div>
|
||||
<div id="discover-list">
|
||||
<!-- Global stations will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content with player and visualizer -->
|
||||
<div id="main-content">
|
||||
<div id="visualizer-container">
|
||||
<canvas id="visualizer"></canvas>
|
||||
</div>
|
||||
<div id="player-container">
|
||||
<div id="now-playing">
|
||||
<img id="station-art" src="assets/images/default-art.png" alt="Station Art">
|
||||
<div id="station-info">
|
||||
<h2 id="player-station-name">Select a Station</h2>
|
||||
<p id="player-station-genre">---</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="player-controls">
|
||||
<button id="prev-station-btn" title="Previous Station"><i class="fas fa-backward"></i></button>
|
||||
<button id="play-pause-btn" class="play-btn" title="Play/Pause"><i class="fas fa-play"></i></button>
|
||||
<button id="next-station-btn" title="Next Station"><i class="fas fa-forward"></i></button>
|
||||
<button id="record-btn" title="Start Recording"><i class="fas fa-circle"></i></button>
|
||||
</div>
|
||||
<div class="volume-control">
|
||||
<i class="fas fa-volume-down"></i>
|
||||
<input type="range" id="volume-slider" min="0" max="1" step="0.01" value="0.8">
|
||||
<i class="fas fa-volume-up"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recommendations-container">
|
||||
<h3>You might also like...</h3>
|
||||
<div id="recommendations-list">
|
||||
<!-- AI recommendations will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App-wide controls -->
|
||||
<div id="app-controls">
|
||||
<button id="settings-btn" title="Settings"><i class="fas fa-cog"></i></button>
|
||||
<button id="equalizer-btn" title="Equalizer"><i class="fas fa-sliders-h"></i></button>
|
||||
<button id="sleep-timer-btn" title="Sleep Timer"><i class="fas fa-clock"></i></button>
|
||||
<button id="import-export-btn" title="Import/Export"><i class="fas fa-file-import"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
|
||||
<!-- Modals -->
|
||||
<!-- Add/Edit Station Modal -->
|
||||
<div id="station-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<h2 id="modal-title">Add Station</h2>
|
||||
<form id="station-form">
|
||||
<input type="hidden" id="station-id">
|
||||
<label for="station-name">Name:</label>
|
||||
<input type="text" id="station-name" required>
|
||||
<label for="station-url">Stream URL:</label>
|
||||
<input type="url" id="station-url" required>
|
||||
<label for="station-genre">Genre:</label>
|
||||
<input type="text" id="station-genre" placeholder="e.g., Rock, Pop, News">
|
||||
<button type="submit" id="save-station-btn">Save Station</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<h2>Settings</h2>
|
||||
<div class="color-picker-section">
|
||||
<h3>Customize Colors</h3>
|
||||
<div class="color-input">
|
||||
<label for="primary-color">Primary:</label>
|
||||
<input type="color" id="primary-color" value="#3498db">
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label for="accent-color">Accent:</label>
|
||||
<input type="color" id="accent-color" value="#2ecc71">
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label for="text-color-light">Text (Light):</label>
|
||||
<input type="color" id="text-color-light" value="#2c3e50">
|
||||
</div>
|
||||
<div class="color-input">
|
||||
<label for="text-color-dark">Text (Dark):</label>
|
||||
<input type="color" id="text-color-dark" value="#ecf0f1">
|
||||
</div>
|
||||
<button id="reset-colors-btn">Reset to Default</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sleep Timer Modal -->
|
||||
<div id="sleep-timer-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<h2>Sleep Timer</h2>
|
||||
<p>Stop playback after:</p>
|
||||
<div id="timer-options">
|
||||
<button class="timer-option" data-minutes="15">15 min</button>
|
||||
<button class="timer-option" data-minutes="30">30 min</button>
|
||||
<button class="timer-option" data-minutes="60">60 min</button>
|
||||
</div>
|
||||
<p id="timer-display">Timer not set</p>
|
||||
<button id="cancel-timer-btn">Cancel Timer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import/Export Modal -->
|
||||
<div id="import-export-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<h2>Import / Export</h2>
|
||||
<div class="import-section">
|
||||
<h3>Import Stations</h3>
|
||||
<p>Load stations from a `.json` file.</p>
|
||||
<input type="file" id="import-file-input" accept=".json">
|
||||
<button id="import-btn">Import</button>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<h3>Export Stations</h3>
|
||||
<p>Save your current station list to a file.</p>
|
||||
<button id="export-btn">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equalizer Modal -->
|
||||
<div id="equalizer-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<h2>Graphic Equalizer</h2>
|
||||
<div id="equalizer-controls">
|
||||
<div class="eq-presets">
|
||||
<label for="eq-presets-select">Presets:</label>
|
||||
<select id="eq-presets-select">
|
||||
<option value="custom">Custom</option>
|
||||
<option value="flat">Flat</option>
|
||||
<option value="bass-boost">Bass Boost</option>
|
||||
<option value="rock">Rock</option>
|
||||
<option value="pop">Pop</option>
|
||||
<option value="vocal-booster">Vocal Booster</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="eq-bands">
|
||||
<!-- EQ sliders will be generated by JS -->
|
||||
</div>
|
||||
<button id="reset-eq-btn">Reset EQ</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="audio-player" crossOrigin="anonymous"></audio>
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user