Initial commit
This commit is contained in:
344
src/App.jsx
Normal file
344
src/App.jsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function formatClock(date) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function EditIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ListIcon() {
|
||||
return (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" />
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" />
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function InstallIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 3v12" />
|
||||
<path d="m7 10 5 5 5-5" />
|
||||
<path d="M5 21h14" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CastIcon({ size = 22 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2 16.1A5 5 0 0 1 5.9 20" />
|
||||
<path d="M2 12.05A9 9 0 0 1 9.95 20" />
|
||||
<path d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function VolumeIcon() {
|
||||
return (
|
||||
<svg id="icon-volume" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MutedIcon() {
|
||||
return (
|
||||
<svg id="icon-muted" className="hidden" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||
<line x1="23" y1="9" x2="17" y2="15" />
|
||||
<line x1="17" y1="9" x2="23" y2="15" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderControls() {
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = window.setInterval(() => setNow(new Date()), 1000);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="header-top-row">
|
||||
<div className="brand-block">
|
||||
<img className="brand-logo" src="assets/radioplayer-logo-192.png" alt="" aria-hidden="true" />
|
||||
<div className="brand-copy">
|
||||
<div className="app-title">RadioPlayer</div>
|
||||
<time className="datetime" dateTime={now.toISOString()}>
|
||||
<span className="datetime-time">{formatClock(now)}</span>
|
||||
<span className="datetime-date">{formatDate(now)}</span>
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-icons-left">
|
||||
<button id="edit-stations-btn" className="icon-btn" title="Edit Stations" aria-label="Edit Stations" type="button">
|
||||
<EditIcon />
|
||||
</button>
|
||||
<button id="stations-list-btn" className="icon-btn" aria-label="All Stations" title="All Stations" type="button">
|
||||
<ListIcon />
|
||||
</button>
|
||||
<button id="install-app-btn" className="icon-btn install-btn hidden" title="Install app" aria-label="Install app" type="button">
|
||||
<InstallIcon />
|
||||
<span>Install</span>
|
||||
</button>
|
||||
<button id="cast-btn" className="icon-btn cast-btn" title="Cast to device" aria-label="Cast to device" type="button">
|
||||
<CastIcon />
|
||||
<span>Cast</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="header-close" />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtworkPanel() {
|
||||
return (
|
||||
<section className="artwork-section">
|
||||
<div className="artwork-stack">
|
||||
<div className="artwork-container">
|
||||
<div className="artwork-placeholder">
|
||||
<img id="station-logo-img" className="station-logo-img hidden" alt="station logo" />
|
||||
<span className="station-logo-text">1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="artwork-coverflow" className="artwork-coverflow" aria-label="Stations">
|
||||
<button id="artwork-prev" className="coverflow-arrow left" aria-label="Previous station" type="button">‹</button>
|
||||
<div id="artwork-coverflow-stage" className="artwork-coverflow-stage" role="list" aria-label="Station icons" />
|
||||
<button id="artwork-next" className="coverflow-arrow right" aria-label="Next station" type="button">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TrackInfo() {
|
||||
return (
|
||||
<section className="track-info">
|
||||
<h2 id="station-name" />
|
||||
<div id="now-playing" className="now-playing hidden" aria-live="polite">
|
||||
<div id="now-artist" className="now-artist" aria-hidden="false" />
|
||||
<div id="now-title" className="now-title" aria-hidden="false" />
|
||||
</div>
|
||||
<p id="station-subtitle" />
|
||||
<div id="status-indicator" className="status-indicator-wrap" aria-hidden="true">
|
||||
<span className="status-dot" />
|
||||
<span id="status-text" />
|
||||
<span id="engine-badge" className="engine-badge engine-html" title="HTML5 Audio playback">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M4 15V9" />
|
||||
<path d="M8 19V5" />
|
||||
<path d="M12 16V8" />
|
||||
<path d="M16 18V6" />
|
||||
<path d="M20 15V9" />
|
||||
</svg>
|
||||
<span id="engine-label">HTML5</span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="cast-output-row" className="cast-output-row hidden" aria-live="polite">
|
||||
<span className="cast-output-label">Output:</span>
|
||||
<button id="cast-output-btn" className="cast-output-toggle" aria-pressed="false"
|
||||
title="Toggle: Cast only / Cast + This computer" type="button">
|
||||
<span id="cast-output-icon">
|
||||
<CastIcon size={14} />
|
||||
</span>
|
||||
<span id="cast-output-text">Cast only</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressBar() {
|
||||
return (
|
||||
<div className="progress-container">
|
||||
<div className="progress-bar">
|
||||
<div className="progress-fill" />
|
||||
<div className="progress-handle" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayerControls() {
|
||||
return (
|
||||
<section className="controls-section">
|
||||
<button id="prev-btn" className="control-btn secondary" aria-label="Previous Station" type="button">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="play-btn" className="control-btn primary" aria-label="Play" type="button">
|
||||
<div className="icon-container">
|
||||
<svg id="icon-play" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<svg id="icon-stop" className="hidden" width="32" height="32" viewBox="0 0 24 24" fill="currentColor"
|
||||
aria-hidden="true">
|
||||
<path d="M6 6h12v12H6z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button id="next-btn" className="control-btn secondary" aria-label="Next Station" type="button">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function VolumeControl() {
|
||||
return (
|
||||
<section className="volume-section">
|
||||
<button id="mute-btn" className="icon-btn small" type="button" aria-label="Mute">
|
||||
<VolumeIcon />
|
||||
<MutedIcon />
|
||||
</button>
|
||||
<div className="slider-container">
|
||||
<input type="range" id="volume-slider" min="0" max="100" defaultValue="80" aria-label="Volume" />
|
||||
</div>
|
||||
<span id="volume-value">80%</span>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function RetroStarfield() {
|
||||
const stars = Array.from({ length: 120 }, (_, index) => {
|
||||
const angle = (index * 137.508) % 360;
|
||||
const radius = 90 + ((index * 47) % 520);
|
||||
const verticalBias = ((index * 29) % 180) - 90;
|
||||
const x0 = Math.cos(angle * Math.PI / 180) * radius * 0.34;
|
||||
const y0 = Math.sin(angle * Math.PI / 180) * radius * 0.24 + verticalBias * 0.18;
|
||||
const x1 = Math.cos((angle + 24) * Math.PI / 180) * radius * 1.08;
|
||||
const y1 = Math.sin((angle + 18) * Math.PI / 180) * radius * 0.72 + verticalBias;
|
||||
const size = 2 + (index % 6) * 0.75;
|
||||
const duration = 7 + (index % 9) * 1.1;
|
||||
const delay = -((index * 0.71) % duration);
|
||||
|
||||
return {
|
||||
id: index,
|
||||
style: {
|
||||
'--x0': `${x0.toFixed(1)}px`,
|
||||
'--y0': `${y0.toFixed(1)}px`,
|
||||
'--x1': `${x1.toFixed(1)}px`,
|
||||
'--y1': `${y1.toFixed(1)}px`,
|
||||
'--star-size': `${size.toFixed(1)}px`,
|
||||
'--star-duration': `${duration.toFixed(1)}s`,
|
||||
'--star-delay': `${delay.toFixed(1)}s`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="starfield" aria-hidden="true">
|
||||
<div className="starfield-plane">
|
||||
{stars.map((star) => (
|
||||
<span key={star.id} className="star" style={star.style} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StationsOverlay() {
|
||||
return (
|
||||
<div id="cast-overlay" className="overlay hidden" aria-hidden="true">
|
||||
<div className="modal" role="dialog" aria-modal="true" aria-labelledby="deviceTitle">
|
||||
<h2 id="deviceTitle">Stations</h2>
|
||||
<ul id="device-list" className="device-list">
|
||||
<li className="device">
|
||||
<div className="device-main">Loading...</div>
|
||||
<div className="device-sub">Preparing stations</div>
|
||||
</li>
|
||||
</ul>
|
||||
<button id="close-overlay" className="btn cancel" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorOverlay() {
|
||||
return (
|
||||
<div id="editor-overlay" className="overlay hidden" aria-hidden="true">
|
||||
<div className="modal" role="dialog" aria-modal="true" aria-labelledby="editorTitle">
|
||||
<h2 id="editorTitle">Edit Stations</h2>
|
||||
<ul id="editor-list" className="device-list" />
|
||||
<form id="add-station-form">
|
||||
<div className="field-row">
|
||||
<input id="us_title" placeholder="Title" required />
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<input id="us_url" placeholder="Stream URL" required />
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<input id="us_logo" placeholder="Logo URL (optional)" />
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<input id="us_www" placeholder="Website (optional)" />
|
||||
</div>
|
||||
<input type="hidden" id="us_id" />
|
||||
<input type="hidden" id="us_index" />
|
||||
<div className="editor-actions">
|
||||
<button id="us_save_btn" className="btn cancel" type="submit">Save</button>
|
||||
<button id="editor-close-btn" className="btn secondary" type="button">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="app-container">
|
||||
<main className="glass-card">
|
||||
<RetroStarfield />
|
||||
<HeaderControls />
|
||||
<ArtworkPanel />
|
||||
<TrackInfo />
|
||||
<ProgressBar />
|
||||
<PlayerControls />
|
||||
<VolumeControl />
|
||||
<StationsOverlay />
|
||||
<EditorOverlay />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user