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;
|
||||
}
|
||||
}
|
||||
}
|
||||
281
index.php
281
index.php
@ -1,150 +1,147 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Radio Wave</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=<?php echo time(); ?>">
|
||||
<meta name="description" content="Radio Wave - The ultimate web radio experience.">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<body class="dark-theme">
|
||||
|
||||
<div id="app-container">
|
||||
<!-- Header -->
|
||||
<header id="main-header">
|
||||
<div class="logo">
|
||||
<i class="fas fa-broadcast-tower"></i>
|
||||
<h1>Radio Wave</h1>
|
||||
</div>
|
||||
<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>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<div id="settings-modal" class="modal">
|
||||
<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>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user