Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c184472fb4 | ||
|
|
1287e70095 | ||
|
|
a783045d8a | ||
|
|
2fad95a89a |
125
admin/dashboard.php
Normal file
125
admin/dashboard.php
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||||
|
header('Location: /admin/index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
// Create table if it doesn't exist
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS stations (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
url VARCHAR(255) NOT NULL,
|
||||||
|
logo_url VARCHAR(255)
|
||||||
|
)");
|
||||||
|
|
||||||
|
// Handle form submissions for add/edit/delete
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_POST['add'])) {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO stations (name, url, logo_url) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$_POST['name'], $_POST['url'], $_POST['logo_url']]);
|
||||||
|
} elseif (isset($_POST['edit'])) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE stations SET name = ?, url = ?, logo_url = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['name'], $_POST['url'], $_POST['logo_url'], $_POST['id']]);
|
||||||
|
} elseif (isset($_POST['delete'])) {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM stations WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['id']]);
|
||||||
|
}
|
||||||
|
header("Location: dashboard.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all stations
|
||||||
|
$stations = $pdo->query("SELECT * FROM stations ORDER BY name")->fetchAll();
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$error = "Database error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Dashboard</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f8f9fa; }
|
||||||
|
.container { max-width: 900px; }
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Manage Radio Stations</h3>
|
||||||
|
<a href="/admin/index.php?logout=true" class="btn btn-danger">Logout</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Add Station Form -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4>Add New Station</h4>
|
||||||
|
<form action="dashboard.php" method="POST">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="text" name="name" class="form-control" placeholder="Station Name" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="url" name="url" class="form-control" placeholder="Stream URL" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="url" name="logo_url" class="form-control" placeholder="Logo URL">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="add" class="btn btn-primary mt-2">Add Station</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>Existing Stations</h4>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Stream URL</th>
|
||||||
|
<th>Logo URL</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($stations)): ?>
|
||||||
|
<tr><td colspan="4" class="text-center">No stations found.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($stations as $station): ?>
|
||||||
|
<tr>
|
||||||
|
<form action="dashboard.php" method="POST">
|
||||||
|
<input type="hidden" name="id" value="<?= $station['id'] ?>">
|
||||||
|
<td><input type="text" name="name" class="form-control" value="<?= htmlspecialchars($station['name']) ?>"></td>
|
||||||
|
<td><input type="text" name="url" class="form-control" value="<?= htmlspecialchars($station['url']) ?>"></td>
|
||||||
|
<td><input type="text" name="logo_url" class="form-control" value="<?= htmlspecialchars($station['logo_url']) ?>"></td>
|
||||||
|
<td>
|
||||||
|
<button type="submit" name="edit" class="btn btn-sm btn-success">Save</button>
|
||||||
|
<button type="submit" name="delete" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
||||||
|
</td>
|
||||||
|
</form>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
93
admin/index.php
Normal file
93
admin/index.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// If already logged in, redirect to dashboard
|
||||||
|
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
|
||||||
|
header('Location: dashboard.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardcoded password - CHANGE THIS!
|
||||||
|
define('ADMIN_PASSWORD', 'password123');
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_POST['password']) && $_POST['password'] === ADMIN_PASSWORD) {
|
||||||
|
$_SESSION['loggedin'] = true;
|
||||||
|
header('Location: dashboard.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = 'Invalid password.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Login</title>
|
||||||
|
<link rel="stylesheet" href="../assets/css/style.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.login-container h2 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>Admin Login</h2>
|
||||||
|
<form action="index.php" method="POST">
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<p class="error"><?php echo htmlspecialchars($error); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
api/get_stations.php
Normal file
12
api/get_stations.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stations = $pdo->query("SELECT id, name, url, logo_url FROM stations ORDER BY name")->fetchAll();
|
||||||
|
echo json_encode($stations);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
455
assets/css/style.css
Normal file
455
assets/css/style.css
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #FF8C00; /* Vibrant Orange */
|
||||||
|
--dark-bg-1: #121212;
|
||||||
|
--dark-bg-2: #1E1E1E;
|
||||||
|
--dark-bg-3: #282828;
|
||||||
|
--dark-text-1: #FFFFFF;
|
||||||
|
--dark-text-2: #B3B3B3;
|
||||||
|
--dark-border: #404040;
|
||||||
|
|
||||||
|
--light-bg-1: #FFFFFF;
|
||||||
|
--light-bg-2: #F0F0F0;
|
||||||
|
--light-bg-3: #E0E0E0;
|
||||||
|
--light-text-1: #000000;
|
||||||
|
--light-text-2: #555555;
|
||||||
|
--light-border: #D1D1D1;
|
||||||
|
|
||||||
|
--font-family: 'Inter', sans-serif;
|
||||||
|
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
--border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme (Default) */
|
||||||
|
body.dark-theme {
|
||||||
|
background-color: var(--dark-bg-1);
|
||||||
|
color: var(--dark-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme #main-header, .dark-theme .modal-content {
|
||||||
|
background-color: var(--dark-bg-2);
|
||||||
|
border-bottom: 1px solid var(--dark-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme #player-column, .dark-theme #stations-column {
|
||||||
|
background-color: var(--dark-bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .tab-content, .dark-theme #now-playing-card {
|
||||||
|
background-color: var(--dark-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .control-button, .dark-theme input, .dark-theme select {
|
||||||
|
background-color: var(--dark-bg-3);
|
||||||
|
color: var(--dark-text-1);
|
||||||
|
border: 1px solid var(--dark-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .control-button:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--dark-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .tab-link {
|
||||||
|
color: var(--dark-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .tab-link.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme #station-list li:hover {
|
||||||
|
background-color: var(--dark-bg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme #station-list li.active {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--dark-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Theme */
|
||||||
|
body.light-theme {
|
||||||
|
background-color: var(--light-bg-2);
|
||||||
|
color: var(--light-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme #main-header, .light-theme .modal-content {
|
||||||
|
background-color: var(--light-bg-1);
|
||||||
|
border-bottom: 1px solid var(--light-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme #player-column, .light-theme #stations-column {
|
||||||
|
background-color: var(--light-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .tab-content, .light-theme #now-playing-card {
|
||||||
|
background-color: var(--light-bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .control-button, .light-theme input, .light-theme select {
|
||||||
|
background-color: var(--light-bg-3);
|
||||||
|
color: var(--light-text-1);
|
||||||
|
border: 1px solid var(--light-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .control-button:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--light-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .tab-link {
|
||||||
|
color: var(--light-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .tab-link.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme #station-list li:hover {
|
||||||
|
background-color: var(--light-bg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme #station-list li.active {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--light-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
#main-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
#main-content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-column, #stations-column {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-column {
|
||||||
|
flex-basis: 35%;
|
||||||
|
min-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stations-column {
|
||||||
|
flex-basis: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Player Column */
|
||||||
|
#now-playing-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#album-art-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 100%; /* 1:1 Aspect Ratio */
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#station-logo, #visualizer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visualizer {
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#station-info h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#station-info p {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#live-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#live-indicator .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { box-shadow: 0 0 0 0 var(--primary-color); }
|
||||||
|
70% { box-shadow: 0 0 0 10px rgba(255, 140, 0, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(255, 140, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-button {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
font-size: 2rem;
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume-and-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume-control {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume-slider {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#record-button.recording {
|
||||||
|
color: red;
|
||||||
|
animation: pulse-record 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-record {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stations Column */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-link {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-list-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#station-list, #discover-list, #genre-list {
|
||||||
|
list-style: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#station-list li, #discover-list li, #genre-list li {
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#station-list li img, #discover-list li img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modals */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.6);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
float: right;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item, #add-station-form > * {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Equalizer */
|
||||||
|
#eq-bands-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-band {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-band input[type=range] {
|
||||||
|
-webkit-appearance: slider-vertical;
|
||||||
|
width: 8px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--dark-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.light-theme ::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--light-border);
|
||||||
|
}
|
||||||
514
assets/js/main.js
Normal file
514
assets/js/main.js
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const app = new RadioWave();
|
||||||
|
app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
class RadioWave {
|
||||||
|
constructor() {
|
||||||
|
this.stations = [];
|
||||||
|
this.currentStationIndex = -1;
|
||||||
|
this.audio = new Audio();
|
||||||
|
this.audio.crossOrigin = 'anonymous';
|
||||||
|
this.isPlaying = false;
|
||||||
|
this.audioContext = null;
|
||||||
|
this.analyser = null;
|
||||||
|
this.eqBands = [];
|
||||||
|
this.mediaRecorder = null;
|
||||||
|
this.recordedChunks = [];
|
||||||
|
this.sleepTimer = null;
|
||||||
|
|
||||||
|
this.ui = {}; // To cache UI elements
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.initUI();
|
||||||
|
await this.loadStations();
|
||||||
|
this.bindEvents();
|
||||||
|
this.applyAccentColor(localStorage.getItem('accentColor') || '#FF8C00');
|
||||||
|
if (localStorage.getItem('theme') === 'light') {
|
||||||
|
document.body.classList.replace('dark-theme', 'light-theme');
|
||||||
|
this.ui.themeSwitcher.innerHTML = '<i class="fas fa-moon"></i>';
|
||||||
|
}
|
||||||
|
this.populateGenres();
|
||||||
|
this.populateDiscoverStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
initUI() {
|
||||||
|
const ids = [
|
||||||
|
'app-container', 'main-header', 'theme-switcher', 'equalizer-button', 'settings-button',
|
||||||
|
'main-content', 'player-column', 'now-playing-card', 'album-art-container', 'station-logo',
|
||||||
|
'visualizer', 'station-info', 'station-name', 'station-genre', 'live-indicator',
|
||||||
|
'player-controls', 'prev-station', 'play-pause-button', 'next-station', 'volume-and-more',
|
||||||
|
'volume-control', 'volume-slider', 'record-button', 'stations-column', 'tabs',
|
||||||
|
'my-stations-tab', 'discover-tab', 'genres-tab', 'station-list-header', 'search-input',
|
||||||
|
'add-station-button', 'station-list', 'ai-recommendations', 'recommendations-container',
|
||||||
|
'discover-list', 'genre-list', 'settings-modal', 'color-picker', 'sleep-timer-select',
|
||||||
|
'import-button', 'export-button', 'import-file-input', 'add-station-modal', 'add-station-form',
|
||||||
|
'new-station-name', 'new-station-url', 'new-station-logo', 'new-station-genre',
|
||||||
|
'equalizer-modal', 'equalizer-controls', 'eq-preset-select', 'eq-reset-button', 'eq-bands-container'
|
||||||
|
];
|
||||||
|
ids.forEach(id => {
|
||||||
|
const camelCaseId = id.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
||||||
|
this.ui[camelCaseId] = document.getElementById(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ui.modalCloseButtons = document.querySelectorAll('.modal .close-button');
|
||||||
|
this.ui.tabLinks = document.querySelectorAll('.tab-link');
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.ui.playPauseButton.addEventListener('click', () => this.togglePlayPause());
|
||||||
|
this.ui.nextStation.addEventListener('click', () => this.playNextStation());
|
||||||
|
this.ui.prevStation.addEventListener('click', () => this.playPreviousStation());
|
||||||
|
this.ui.volumeSlider.addEventListener('input', (e) => this.setVolume(e.target.value));
|
||||||
|
this.ui.stationList.addEventListener('click', (e) => this.handleStationClick(e));
|
||||||
|
this.ui.searchInput.addEventListener('input', (e) => this.filterStations(e.target.value));
|
||||||
|
this.ui.themeSwitcher.addEventListener('click', () => this.toggleTheme());
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
this.ui.settingsButton.addEventListener('click', () => this.showModal(this.ui.settingsModal));
|
||||||
|
this.ui.addStationButton.addEventListener('click', () => this.showModal(this.ui.addStationModal));
|
||||||
|
this.ui.equalizerButton.addEventListener('click', () => this.showModal(this.ui.equalizerModal));
|
||||||
|
this.ui.modalCloseButtons.forEach(btn => btn.addEventListener('click', () => this.hideAllModals()));
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('modal')) this.hideAllModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
this.ui.colorPicker.addEventListener('input', (e) => this.applyAccentColor(e.target.value));
|
||||||
|
this.ui.sleepTimerSelect.addEventListener('change', (e) => this.setSleepTimer(e.target.value));
|
||||||
|
this.ui.importButton.addEventListener('click', () => this.ui.importFileInput.click());
|
||||||
|
this.ui.importFileInput.addEventListener('change', (e) => this.importStations(e));
|
||||||
|
this.ui.exportButton.addEventListener('click', () => this.exportStations());
|
||||||
|
|
||||||
|
// Add Station Form
|
||||||
|
this.ui.addStationForm.addEventListener('submit', (e) => this.addStation(e));
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
this.ui.tabLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', () => this.switchTab(link.dataset.tab));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Audio Lifecycle
|
||||||
|
this.audio.addEventListener('playing', () => this.isPlaying = true);
|
||||||
|
this.audio.addEventListener('pause', () => this.isPlaying = false);
|
||||||
|
|
||||||
|
// Equalizer & Recorder
|
||||||
|
this.ui.recordButton.addEventListener('click', () => this.toggleRecording());
|
||||||
|
this.initEqualizer();
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', (e) => this.handleKeyPress(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Station Management
|
||||||
|
async loadStations() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/get_stations.php');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const stations = await response.json();
|
||||||
|
this.stations = stations.map(station => ({
|
||||||
|
...station,
|
||||||
|
logo: station.logo_url // map db field to app field
|
||||||
|
}));
|
||||||
|
this.renderStationList();
|
||||||
|
if (this.stations.length > 0 && this.currentStationIndex === -1) {
|
||||||
|
this.playStation(0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Could not load stations:", error);
|
||||||
|
this.ui.stationList.innerHTML = `<li class="error">Could not load stations.</li>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStationList() {
|
||||||
|
this.ui.stationList.innerHTML = '';
|
||||||
|
this.stations.forEach((station, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.dataset.index = index;
|
||||||
|
li.className = (index === this.currentStationIndex) ? 'active' : '';
|
||||||
|
li.innerHTML = `
|
||||||
|
<img src="${station.logo || 'https://picsum.photos/seed/'+station.name+'/40'}" alt="${station.name}">
|
||||||
|
<span>${station.name}</span>
|
||||||
|
`;
|
||||||
|
this.ui.stationList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStationClick(e) {
|
||||||
|
const li = e.target.closest('li');
|
||||||
|
if (li) {
|
||||||
|
const index = parseInt(li.dataset.index, 10);
|
||||||
|
this.playStation(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStations(query) {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const filtered = this.stations.filter(s => s.name.toLowerCase().includes(lowerQuery));
|
||||||
|
// A bit of a hack, but re-rendering the whole list is easiest
|
||||||
|
this.ui.stationList.innerHTML = '';
|
||||||
|
filtered.forEach(station => {
|
||||||
|
const index = this.stations.indexOf(station);
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.dataset.index = index;
|
||||||
|
li.className = (index === this.currentStationIndex) ? 'active' : '';
|
||||||
|
li.innerHTML = `
|
||||||
|
<img src="${station.logo || 'https://picsum.photos/seed/'+station.name+'/40'}" alt="${station.name}">
|
||||||
|
<span>${station.name}</span>
|
||||||
|
`;
|
||||||
|
this.ui.stationList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player Logic
|
||||||
|
playStation(index) {
|
||||||
|
if (index < 0 || index >= this.stations.length) return;
|
||||||
|
|
||||||
|
this.currentStationIndex = index;
|
||||||
|
const station = this.stations[index];
|
||||||
|
|
||||||
|
this.audio.src = station.url;
|
||||||
|
this.togglePlayPause(true);
|
||||||
|
|
||||||
|
this.ui.stationLogo.src = station.logo || `https://picsum.photos/seed/${station.name}/600`;
|
||||||
|
this.ui.stationName.textContent = station.name;
|
||||||
|
this.ui.stationGenre.textContent = station.genre || 'Radio';
|
||||||
|
|
||||||
|
this.renderStationList(); // To update active state
|
||||||
|
this.updateAIRecommendations(station.genre);
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlayPause(forcePlay = null) {
|
||||||
|
if (this.currentStationIndex === -1) {
|
||||||
|
if (this.stations.length > 0) this.playStation(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldPlay = forcePlay !== null ? forcePlay : this.audio.paused;
|
||||||
|
|
||||||
|
if (shouldPlay) {
|
||||||
|
this.audio.play().catch(e => console.error("Playback failed:", e));
|
||||||
|
this.ui.playPauseButton.innerHTML = '<i class="fas fa-pause"></i>';
|
||||||
|
this.setupAudioVisualizer();
|
||||||
|
} else {
|
||||||
|
this.audio.pause();
|
||||||
|
this.ui.playPauseButton.innerHTML = '<i class="fas fa-play"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playNextStation() {
|
||||||
|
let nextIndex = this.currentStationIndex + 1;
|
||||||
|
if (nextIndex >= this.stations.length) nextIndex = 0;
|
||||||
|
this.playStation(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
playPreviousStation() {
|
||||||
|
let prevIndex = this.currentStationIndex - 1;
|
||||||
|
if (prevIndex < 0) prevIndex = this.stations.length - 1;
|
||||||
|
this.playStation(prevIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(value) {
|
||||||
|
this.audio.volume = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio Features
|
||||||
|
setupAudioVisualizer() {
|
||||||
|
if (this.audioContext) return;
|
||||||
|
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const source = this.audioContext.createMediaElementSource(this.audio);
|
||||||
|
this.analyser = this.audioContext.createAnalyser();
|
||||||
|
this.analyser.fftSize = 256;
|
||||||
|
|
||||||
|
// EQ setup
|
||||||
|
const frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000];
|
||||||
|
let lastNode = source;
|
||||||
|
frequencies.forEach((freq, i) => {
|
||||||
|
const eq = this.audioContext.createBiquadFilter();
|
||||||
|
eq.type = 'peaking';
|
||||||
|
eq.frequency.value = freq;
|
||||||
|
eq.Q.value = 1.5;
|
||||||
|
eq.gain.value = 0;
|
||||||
|
lastNode.connect(eq);
|
||||||
|
lastNode = eq;
|
||||||
|
this.eqBands.push(eq);
|
||||||
|
});
|
||||||
|
|
||||||
|
lastNode.connect(this.analyser);
|
||||||
|
this.analyser.connect(this.audioContext.destination);
|
||||||
|
|
||||||
|
this.drawVisualizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawVisualizer() {
|
||||||
|
requestAnimationFrame(() => this.drawVisualizer());
|
||||||
|
if (!this.analyser) return;
|
||||||
|
|
||||||
|
const bufferLength = this.analyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
|
this.analyser.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
|
const canvas = this.ui.visualizer;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const { width, height } = canvas;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
const barWidth = (width / bufferLength) * 2.5;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
const barHeight = dataArray[i] * (height / 255);
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight);
|
||||||
|
gradient.addColorStop(0, getComputedStyle(document.documentElement).getPropertyValue('--primary-color'));
|
||||||
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.5)');
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
|
||||||
|
ctx.fillRect(x, height - barHeight, barWidth, barHeight);
|
||||||
|
x += barWidth + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initEqualizer() {
|
||||||
|
const frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000];
|
||||||
|
this.ui.eqBandsContainer.innerHTML = '';
|
||||||
|
frequencies.forEach((freq, i) => {
|
||||||
|
const band = document.createElement('div');
|
||||||
|
band.className = 'eq-band';
|
||||||
|
band.innerHTML = `
|
||||||
|
<input type="range" min="-12" max="12" step="0.1" value="0" data-index="${i}">
|
||||||
|
<label>${freq < 1000 ? freq : (freq/1000)+'k'} Hz</label>
|
||||||
|
`;
|
||||||
|
this.ui.eqBandsContainer.appendChild(band);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ui.eqBandsContainer.addEventListener('input', (e) => {
|
||||||
|
if (e.target.type === 'range') {
|
||||||
|
const index = e.target.dataset.index;
|
||||||
|
const value = e.target.value;
|
||||||
|
if (.eqBands[index]) {
|
||||||
|
this.eqBands[index].gain.value = value;
|
||||||
|
}
|
||||||
|
this.ui.eqPresetSelect.value = 'custom';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ui.eqPresetSelect.addEventListener('change', (e) => this.applyEQPreset(e.target.value));
|
||||||
|
this.ui.eqResetButton.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, 1, 0, -1, -2, -2, -1, 0],
|
||||||
|
'rock': [4, 2, -2, -4, -2, 2, 4, 5, 5, 4],
|
||||||
|
'pop': [-2, -1, 0, 2, 4, 4, 2, 0, -1, -2],
|
||||||
|
'vocal-booster': [-2, -1, 0, 2, 4, 4, 2, 0, -1, -2]
|
||||||
|
};
|
||||||
|
const values = presets[preset];
|
||||||
|
if (!values) return;
|
||||||
|
|
||||||
|
this.ui.eqBandsContainer.querySelectorAll('input[type=range]').forEach((slider, i) => {
|
||||||
|
slider.value = values[i];
|
||||||
|
if (this.eqBands[i]) {
|
||||||
|
this.eqBands[i].gain.value = values[i];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.ui.eqPresetSelect.value = preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRecording() {
|
||||||
|
if (!this.audioContext) {
|
||||||
|
alert("Please play a station first to start recording.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
||||||
|
// Stop recording
|
||||||
|
this.mediaRecorder.stop();
|
||||||
|
this.ui.recordButton.classList.remove('recording');
|
||||||
|
} else {
|
||||||
|
// Start recording
|
||||||
|
const destination = this.audioContext.createMediaStreamDestination();
|
||||||
|
this.analyser.connect(destination); // Record post-EQ and visualizer
|
||||||
|
this.mediaRecorder = new MediaRecorder(destination.stream);
|
||||||
|
|
||||||
|
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.recordedChunks = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mediaRecorder.start();
|
||||||
|
this.ui.recordButton.classList.add('recording');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme & Style
|
||||||
|
toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-theme');
|
||||||
|
document.body.classList.toggle('light-theme');
|
||||||
|
const isDark = document.body.classList.contains('dark-theme');
|
||||||
|
this.ui.themeSwitcher.innerHTML = isDark ? '<i class="fas fa-sun"></i>' : '<i class="fas fa-moon"></i>';
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
applyAccentColor(color) {
|
||||||
|
document.documentElement.style.setProperty('--primary-color', color);
|
||||||
|
this.ui.colorPicker.value = color;
|
||||||
|
localStorage.setItem('accentColor', color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data & Settings
|
||||||
|
setSleepTimer(minutes) {
|
||||||
|
clearTimeout(this.sleepTimer);
|
||||||
|
if (minutes > 0) {
|
||||||
|
this.sleepTimer = setTimeout(() => {
|
||||||
|
this.togglePlayPause(false); // Pause the player
|
||||||
|
this.ui.sleepTimerSelect.value = 0;
|
||||||
|
}, minutes * 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSleepTimer(minutes) {
|
||||||
|
clearTimeout(this.sleepTimer);
|
||||||
|
if (minutes > 0) {
|
||||||
|
this.sleepTimer = setTimeout(() => {
|
||||||
|
this.togglePlayPause(false); // Pause the player
|
||||||
|
this.ui.sleepTimerSelect.value = 0;
|
||||||
|
}, minutes * 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Helpers
|
||||||
|
showModal(modalElement) {
|
||||||
|
this.hideAllModals();
|
||||||
|
modalElement.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllModals() {
|
||||||
|
document.querySelectorAll('.modal.show').forEach(m => m.classList.remove('show'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTab(tabId) {
|
||||||
|
this.ui.tabLinks.forEach(link => link.classList.remove('active'));
|
||||||
|
document.querySelector(`.tab-link[data-tab="${tabId}"]`).classList.add('active');
|
||||||
|
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||||
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovery Features
|
||||||
|
populateGenres() {
|
||||||
|
const genres = [...new Set(this.stations.map(s => s.genre).filter(g => g))];
|
||||||
|
// Add some default genres
|
||||||
|
['Pop', 'Rock', 'Jazz', 'Electronic', 'Classical', 'Hip-Hop'].forEach(g => {
|
||||||
|
if (!genres.includes(g)) genres.push(g);
|
||||||
|
});
|
||||||
|
this.ui.genreList.innerHTML = '';
|
||||||
|
genres.sort().forEach(genre => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = genre;
|
||||||
|
li.addEventListener('click', () => this.filterStationsByGenre(genre));
|
||||||
|
this.ui.genreList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStationsByGenre(genre) {
|
||||||
|
this.switchTab('my-stations-tab');
|
||||||
|
this.ui.searchInput.value = genre;
|
||||||
|
this.filterStations(genre);
|
||||||
|
}
|
||||||
|
|
||||||
|
populateDiscoverStations() {
|
||||||
|
// In a real app, this would be an API call.
|
||||||
|
const discover = [
|
||||||
|
{ name: "Jazz Cafe", url: "http://192.99.35.215:5034/stream", logo: "https://cdn-radiotime-logos.tunein.com/s253631q.png", genre: "Jazz" },
|
||||||
|
{ name: "Classical FM", url: "http://media-ice.musicradio.com/ClassicFMMP3", logo: "https://cdn-radiotime-logos.tunein.com/s25365q.png", genre: "Classical" },
|
||||||
|
{ name: "Radio Paradise", url: "http://stream.radioparadise.com/flacm", logo: "https://i.radioparadise.com/img/logo-circle-250.png", genre: "Eclectic" },
|
||||||
|
];
|
||||||
|
this.ui.discoverList.innerHTML = '';
|
||||||
|
discover.forEach(station => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.innerHTML = `
|
||||||
|
<img src="${station.logo}" alt="${station.name}">
|
||||||
|
<span>${station.name}</span>
|
||||||
|
<button class="add-from-discover"><i class="fas fa-plus"></i></button>
|
||||||
|
`;
|
||||||
|
li.querySelector('.add-from-discover').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.stations.push(station);
|
||||||
|
this.saveStations();
|
||||||
|
this.renderStationList();
|
||||||
|
alert(`${station.name} added to your stations!`);
|
||||||
|
});
|
||||||
|
this.ui.discoverList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAIRecommendations(genre) {
|
||||||
|
if (!genre) {
|
||||||
|
this.ui.aiRecommendations.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Dummy recommendation logic
|
||||||
|
const recommendations = this.stations.filter(s => s.genre === genre && s.name !== this.stations[this.currentStationIndex].name);
|
||||||
|
if (recommendations.length > 0) {
|
||||||
|
this.ui.recommendationsContainer.innerHTML = '';
|
||||||
|
recommendations.slice(0, 2).forEach(station => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'rec-item';
|
||||||
|
div.innerHTML = `<span>Based on ${genre}, try: <b>${station.name}</b></span>`;
|
||||||
|
div.addEventListener('click', () => this.playStation(this.stations.indexOf(station)));
|
||||||
|
this.ui.recommendationsContainer.appendChild(div);
|
||||||
|
});
|
||||||
|
this.ui.aiRecommendations.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.ui.aiRecommendations.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress(e) {
|
||||||
|
// Don't interfere with text inputs
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
|
||||||
|
switch (e.code) {
|
||||||
|
case 'Space':
|
||||||
|
e.preventDefault();
|
||||||
|
this.togglePlayPause();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.playNextStation();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.playPreviousStation();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
this.ui.volumeSlider.value = Math.min(1, this.audio.volume + 0.05);
|
||||||
|
this.setVolume(this.ui.volumeSlider.value);
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
this.ui.volumeSlider.value = Math.max(0, this.audio.volume - 0.05);
|
||||||
|
this.setVolume(this.ui.volumeSlider.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
285
index.php
285
index.php
@ -1,150 +1,147 @@
|
|||||||
<?php
|
<!DOCTYPE html>
|
||||||
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>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>New Style</title>
|
<title>Radio Wave</title>
|
||||||
<?php
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
// Read project preview data from environment
|
<link rel="stylesheet" href="assets/css/style.css?v=<?php echo time(); ?>">
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<meta name="description" content="Radio Wave - The ultimate web radio experience.">
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<meta name="robots" content="noindex, nofollow">
|
||||||
?>
|
|
||||||
<?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>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="dark-theme">
|
||||||
<main>
|
|
||||||
<div class="card">
|
<div id="app-container">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<!-- Header -->
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<header id="main-header">
|
||||||
<span class="sr-only">Loading…</span>
|
<div class="logo">
|
||||||
</div>
|
<i class="fas fa-broadcast-tower"></i>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
<h1>Radio Wave</h1>
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
</div>
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
<div class="header-controls">
|
||||||
|
<button id="theme-switcher" class="control-button"><i class="fas fa-sun"></i></button>
|
||||||
|
<button id="equalizer-button" class="control-button"><i class="fas fa-sliders-h"></i></button>
|
||||||
|
<button id="settings-button" class="control-button"><i class="fas fa-cog"></i></button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main id="main-content">
|
||||||
|
<!-- Left Column: Player -->
|
||||||
|
<div id="player-column">
|
||||||
|
<div id="now-playing-card">
|
||||||
|
<div id="album-art-container">
|
||||||
|
<img src="https://picsum.photos/seed/radiowave/600" alt="Album Art" id="station-logo">
|
||||||
|
<canvas id="visualizer"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="station-info">
|
||||||
|
<h2 id="station-name">Select a Station</h2>
|
||||||
|
<p id="station-genre">Welcome to Radio Wave</p>
|
||||||
|
<p id="live-indicator"><span class="dot"></span> Live</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="player-controls">
|
||||||
|
<button id="prev-station" class="control-button"><i class="fas fa-backward"></i></button>
|
||||||
|
<button id="play-pause-button" class="control-button main-button">
|
||||||
|
<i class="fas fa-play"></i>
|
||||||
|
</button>
|
||||||
|
<button id="next-station" class="control-button"><i class="fas fa-forward"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="volume-and-more">
|
||||||
|
<div id="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>
|
||||||
|
<button id="record-button" class="control-button"><i class="fas fa-circle"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Stations -->
|
||||||
|
<div id="stations-column">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-link active" data-tab="my-stations-tab">My Stations</button>
|
||||||
|
<button class="tab-link" data-tab="discover-tab">Discover</button>
|
||||||
|
<button class="tab-link" data-tab="genres-tab">Genres</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="my-stations-tab" class="tab-content active">
|
||||||
|
<div class="station-list-header">
|
||||||
|
<input type="text" id="search-input" placeholder="Search your stations...">
|
||||||
|
</div>
|
||||||
|
<ul id="station-list">
|
||||||
|
<!-- Stations will be dynamically added here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="discover-tab" class="tab-content">
|
||||||
|
<div id="ai-recommendations">
|
||||||
|
<h3>Recommended For You</h3>
|
||||||
|
<div id="recommendations-container"></div>
|
||||||
|
</div>
|
||||||
|
<h3>Global Stations</h3>
|
||||||
|
<ul id="discover-list">
|
||||||
|
<!-- Discover stations will be added here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="genres-tab" class="tab-content">
|
||||||
|
<ul id="genre-list">
|
||||||
|
<!-- Genres will be dynamically added here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
<footer>
|
<!-- Modals -->
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<div id="settings-modal" class="modal">
|
||||||
</footer>
|
<div class="modal-content">
|
||||||
|
<span class="close-button">×</span>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="color-picker">Accent Color:</label>
|
||||||
|
<input type="color" id="color-picker" value="#FF8C00">
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="sleep-timer-select">Sleep Timer:</label>
|
||||||
|
<select id="sleep-timer-select">
|
||||||
|
<option value="0">Off</option>
|
||||||
|
<option value="15">15 Minutes</option>
|
||||||
|
<option value="30">30 Minutes</option>
|
||||||
|
<option value="60">1 Hour</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="equalizer-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-button">×</span>
|
||||||
|
<h2>10-Band Graphic Equalizer</h2>
|
||||||
|
<div id="equalizer-controls">
|
||||||
|
<div class="eq-presets">
|
||||||
|
<select id="eq-preset-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>
|
||||||
|
<button id="eq-reset-button" class="control-button">Reset</button>
|
||||||
|
</div>
|
||||||
|
<div id="eq-bands-container">
|
||||||
|
<!-- EQ bands will be generated by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user