Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,125 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<?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()]);
|
|
||||||
}
|
|
||||||
@ -1,455 +0,0 @@
|
|||||||
@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);
|
|
||||||
}
|
|
||||||
@ -1,514 +0,0 @@
|
|||||||
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,147 +1,150 @@
|
|||||||
<!DOCTYPE html>
|
<?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>
|
||||||
<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.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Radio Wave</title>
|
<title>New Style</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
<?php
|
||||||
<link rel="stylesheet" href="assets/css/style.css?v=<?php echo time(); ?>">
|
// Read project preview data from environment
|
||||||
<meta name="description" content="Radio Wave - The ultimate web radio experience.">
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
<meta name="robots" content="noindex, nofollow">
|
$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>
|
||||||
</head>
|
</head>
|
||||||
<body class="dark-theme">
|
<body>
|
||||||
|
<main>
|
||||||
<div id="app-container">
|
<div class="card">
|
||||||
<!-- Header -->
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
<header id="main-header">
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<div class="logo">
|
<span class="sr-only">Loading…</span>
|
||||||
<i class="fas fa-broadcast-tower"></i>
|
</div>
|
||||||
<h1>Radio Wave</h1>
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
</div>
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
<div class="header-controls">
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
<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>
|
||||||
<!-- Modals -->
|
<footer>
|
||||||
<div id="settings-modal" class="modal">
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
<div class="modal-content">
|
</footer>
|
||||||
<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