26
src/App.js
26
src/App.js
@@ -1,13 +1,7 @@
|
||||
import { use, useCallback, useEffect, useRef, useState } from "react";
|
||||
import WaveForm from "./components/WaveForm/WaveForm";
|
||||
import { Button, Icon, Range } from "react-materialize";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import style from "./App.module.css";
|
||||
import { Marquee } from "./components/Marquee/Marquee";
|
||||
import { Player } from "./components/Player/Player";
|
||||
import { Stream } from "./components/Stream/Stream";
|
||||
import useWallpaperList from "./hooks/useWallpaperList";
|
||||
|
||||
import Radio from "./icons/Radio.svg";
|
||||
|
||||
function getRandomInt(min, max) {
|
||||
@@ -18,28 +12,30 @@ function getRandomInt(min, max) {
|
||||
|
||||
const App = () => {
|
||||
const wallpapers = useWallpaperList();
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [nextIndex, setNextIndex] = useState(0);
|
||||
|
||||
// Preload and transition to next wallpaper
|
||||
useEffect(() => {
|
||||
let img = new Image();
|
||||
if (!wallpapers.length) return;
|
||||
|
||||
const img = new window.Image();
|
||||
img.src = `/wallpapers/${wallpapers[nextIndex]}`;
|
||||
img.onload = () => {
|
||||
setIndex(nextIndex);
|
||||
|
||||
setTimeout(() => {
|
||||
setNextIndex(getRandomInt(0, wallpapers.length - 1));
|
||||
}, getRandomInt(20000, 50000));
|
||||
};
|
||||
}, [nextIndex]);
|
||||
}, [nextIndex, wallpapers]);
|
||||
|
||||
// Initialize nextIndex when wallpapers are loaded
|
||||
useEffect(() => {
|
||||
if (wallpapers.length === 0) return;
|
||||
if (!wallpapers.length) return;
|
||||
setNextIndex(getRandomInt(0, wallpapers.length - 1));
|
||||
}, [wallpapers]);
|
||||
|
||||
const current = `/wallpapers/${wallpapers[index]}`;
|
||||
const currentWallpaper = `/wallpapers/${wallpapers[index]}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -49,7 +45,7 @@ const App = () => {
|
||||
height: "100%",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundImage: `url(${current})`,
|
||||
backgroundImage: `url(${currentWallpaper})`,
|
||||
transition: "background-image 1s ease-in-out",
|
||||
}}
|
||||
>
|
||||
@@ -79,4 +75,4 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
@@ -5,9 +5,8 @@ import Pause from "../../icons/Pause.svg";
|
||||
import Volume from "../../icons/Volume.svg";
|
||||
import VolumeX from "../../icons/VolumeX.svg";
|
||||
import UsersIcon from "../../icons/Users.svg";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { VolumeSlider } from "../VolumeSlider/VolumeSlider";
|
||||
import WaveForm from "../WaveForm/WaveForm";
|
||||
|
||||
export const Player = ({
|
||||
src,
|
||||
@@ -30,22 +29,21 @@ export const Player = ({
|
||||
|
||||
const togglePlay = () => {
|
||||
if (onTogglePlay) onTogglePlay();
|
||||
|
||||
if (paused) {
|
||||
audio.current.play();
|
||||
} else {
|
||||
audio.current.pause();
|
||||
}
|
||||
setPaused(!paused);
|
||||
setPaused((prev) => !prev);
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
if (muted) {
|
||||
audio.current.volume = 1; // Unmute
|
||||
audio.current.volume = 1;
|
||||
} else {
|
||||
audio.current.volume = 0; // Mute
|
||||
audio.current.volume = 0;
|
||||
}
|
||||
setMuted(!muted);
|
||||
setMuted((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -72,12 +70,12 @@ export const Player = ({
|
||||
<img
|
||||
tabIndex={-1}
|
||||
src={paused ? Play : Pause}
|
||||
alt="Play"
|
||||
alt={paused ? "Play" : "Pause"}
|
||||
style={paused ? { marginLeft: "6px" } : { marginLeft: "1px" }}
|
||||
/>
|
||||
</button>
|
||||
<button onClick={toggleMute} className={style.mute}>
|
||||
<img tabIndex={-1} src={muted ? VolumeX : Volume} alt="Mute" />
|
||||
<img tabIndex={-1} src={muted ? VolumeX : Volume} alt={muted ? "Unmute" : "Mute"} />
|
||||
</button>
|
||||
<VolumeSlider muted={muted} audioElmRef={audio} setMute={setMuted} />
|
||||
<span className={style.users} title="Users">
|
||||
@@ -91,4 +89,4 @@ export const Player = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Player } from "../Player/Player";
|
||||
import { useListenerCount } from "../../hooks/useListenerCount";
|
||||
import { useAudioInfo } from "../../hooks/useAudioInfo";
|
||||
@@ -7,7 +7,6 @@ import WaveForm from "../WaveForm/WaveForm";
|
||||
export function Stream() {
|
||||
const { count, peak } = useListenerCount(10000);
|
||||
const audioInfo = useAudioInfo();
|
||||
|
||||
const [analyzerData, setAnalyzerData] = useState(null);
|
||||
const audio = useRef(null);
|
||||
|
||||
@@ -28,26 +27,25 @@ export function Stream() {
|
||||
|
||||
source.connect(analyzer);
|
||||
source.connect(audioCtx.destination);
|
||||
source.onended = () => {
|
||||
source.disconnect();
|
||||
};
|
||||
source.onended = () => source.disconnect();
|
||||
|
||||
setAnalyzerData({ analyzer, bufferLength, dataArray });
|
||||
}
|
||||
};
|
||||
|
||||
const marqueeText = `Now Playing: ${Math.floor(audioInfo.duration)}s - ${audioInfo.title} by ${audioInfo.performer} - ${audioInfo.album} (${audioInfo.date})`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{analyzerData && <WaveForm analyzerData={analyzerData} />}
|
||||
<Player
|
||||
src="/stream.mp3"
|
||||
onTogglePlay={onTogglePlay}
|
||||
marquee={`Now Playing: ${Math.floor(audioInfo.duration)}s - ${audioInfo.title
|
||||
} by ${audioInfo.performer} - ${audioInfo.album} (${audioInfo.date})`}
|
||||
marquee={marqueeText}
|
||||
marqueeUrl={audioInfo.url}
|
||||
usersCount={`${count}/${peak}`}
|
||||
ref={setAudioRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,29 @@ import { useState } from "react";
|
||||
import style from "./VolumeSlider.module.css";
|
||||
|
||||
export function VolumeSlider({ muted, setMute, audioElmRef }) {
|
||||
const [currentVolume, setCurrentVolume] = useState(100);
|
||||
const [currentVolume, setCurrentVolume] = useState(100);
|
||||
|
||||
const handleVolumeChange = (event) => {
|
||||
const newVolume = event.target.value;
|
||||
setCurrentVolume(newVolume);
|
||||
const handleVolumeChange = (event) => {
|
||||
const newVolume = Number(event.target.value);
|
||||
setCurrentVolume(newVolume);
|
||||
|
||||
audioElmRef.current.volume = newVolume / 100;
|
||||
if (audioElmRef.current) {
|
||||
audioElmRef.current.volume = newVolume / 100;
|
||||
}
|
||||
|
||||
if (newVolume === "0") {
|
||||
setMute(true);
|
||||
} else {
|
||||
setMute(false);
|
||||
}
|
||||
};
|
||||
setMute(newVolume === 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
className={style.volumeSlider}
|
||||
style={{ backgroundSize: `${muted ? "0" : currentVolume}% 100%` }}
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={muted ? "0" : currentVolume}
|
||||
onChange={handleVolumeChange}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<input
|
||||
className={style.volumeSlider}
|
||||
style={{ backgroundSize: `${muted ? "0" : currentVolume}% 100%` }}
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={muted ? 0 : currentVolume}
|
||||
onChange={handleVolumeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import useSize from "../../hooks/useSize";
|
||||
|
||||
const animateBars = (analyser, canvas, canvasCtx, dataArray, bufferLength) => {
|
||||
const BLUE_SHADES = [
|
||||
"rgba(255,255,255,0.5)",
|
||||
"rgba(255,255,255,0.4)",
|
||||
"rgba(255,255,255,0.3)",
|
||||
"rgba(255,255,255,0.2)",
|
||||
];
|
||||
|
||||
const animateBars = (analyser, canvas, ctx, dataArray, bufferLength) => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
canvasCtx.fillStyle = "#000";
|
||||
const HEIGHT = canvas.height / 2;
|
||||
var barWidth = Math.ceil(canvas.width / bufferLength) * 2.5;
|
||||
let barHeight;
|
||||
const barWidth = Math.ceil(canvas.width / bufferLength) * 2.5;
|
||||
let x = 0;
|
||||
for (var i = 0; i < bufferLength; i++) {
|
||||
barHeight = (dataArray[i] / 255) * HEIGHT;
|
||||
const blueShade = Math.floor((dataArray[i] / 255) * 4); // generate a shade of blue based on the audio input
|
||||
const blueHex = [
|
||||
"rgba(255,255,255,0.5)",
|
||||
"rgba(255,255,255,0.4)",
|
||||
"rgba(255,255,255,0.3)",
|
||||
"rgba(255,255,255,0.2)",
|
||||
][blueShade]; // use react logo blue shades
|
||||
canvasCtx.fillStyle = blueHex;
|
||||
canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const barHeight = (dataArray[i] / 255) * HEIGHT;
|
||||
const blueShade = Math.floor((dataArray[i] / 255) * 4);
|
||||
ctx.fillStyle = BLUE_SHADES[blueShade];
|
||||
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
|
||||
x += barWidth + 1;
|
||||
}
|
||||
};
|
||||
@@ -28,36 +27,36 @@ const WaveForm = ({ analyzerData }) => {
|
||||
const { dataArray, analyzer, bufferLength } = analyzerData;
|
||||
const [width, height] = useSize();
|
||||
|
||||
const draw = (dataArray, analyzer, bufferLength) => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas || !analyzer) return;
|
||||
|
||||
const canvasCtx = canvas.getContext("2d");
|
||||
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// eslint-disable-next-line no-self-assign
|
||||
canvas.width = canvas.width;
|
||||
canvasCtx.translate(0, canvas.offsetHeight / 2);
|
||||
|
||||
animateBars(analyzer, canvas, canvasCtx, dataArray, bufferLength);
|
||||
};
|
||||
animate();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
draw(dataArray, analyzer, bufferLength);
|
||||
}, [dataArray, analyzer, bufferLength]);
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas || !analyzer) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
let animationId;
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.save();
|
||||
ctx.translate(0, canvas.height / 2);
|
||||
animateBars(analyzer, canvas, ctx, dataArray, bufferLength);
|
||||
ctx.restore();
|
||||
animationId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
};
|
||||
}, [dataArray, analyzer, bufferLength, width, height]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
zIndex: "0",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 0,
|
||||
}}
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
@@ -66,4 +65,4 @@ const WaveForm = ({ analyzerData }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WaveForm;
|
||||
export default WaveForm;
|
||||
@@ -1,65 +1,52 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useAudioInfo() {
|
||||
const [json, setJson] = useState(null);
|
||||
const [info, setInfo] = useState({
|
||||
title: "",
|
||||
performer: "",
|
||||
album: "",
|
||||
url: "",
|
||||
duration: 0,
|
||||
date: "",
|
||||
});
|
||||
const [info, setInfo] = useState({
|
||||
title: "",
|
||||
performer: "",
|
||||
album: "",
|
||||
url: "",
|
||||
duration: 0,
|
||||
date: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const fetchAudioInfo = async () => {
|
||||
try {
|
||||
const response = await fetch("/stream.json");
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const json = await response.json();
|
||||
|
||||
if (!isMounted || !json) return;
|
||||
|
||||
let url = json?.media?.['@ref'];
|
||||
const url = json?.media?.["@ref"] || "";
|
||||
const data = json.media?.track?.[0];
|
||||
|
||||
if (json.media?.track && json.media.track[0]) {
|
||||
const data = json.media.track[0];
|
||||
setInfo({
|
||||
title: data.Title || "",
|
||||
performer: data.Performer || "",
|
||||
album: data.Album || "",
|
||||
url: url || "",
|
||||
duration: data.Duration || 0,
|
||||
date: data.Recorded_Date || "",
|
||||
});
|
||||
if (data) {
|
||||
setInfo({
|
||||
title: data.Title || "",
|
||||
performer: data.Performer || "",
|
||||
album: data.Album || "",
|
||||
url,
|
||||
duration: data.Duration || 0,
|
||||
date: data.Recorded_Date || "",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching audio info:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [json]);
|
||||
fetchAudioInfo();
|
||||
const intervalId = setInterval(fetchAudioInfo, 30000);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAudioInfo = async () => {
|
||||
try {
|
||||
const response = await fetch('/stream.json');
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
setJson(data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching audio info:', error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
fetchAudioInfo();
|
||||
|
||||
const intervalId = setInterval(fetchAudioInfo, 30000); // Refresh every 10 seconds
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return info;
|
||||
return info;
|
||||
}
|
||||
@@ -1,50 +1,52 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useListenerCount(refreshInterval = 10000) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [peak, setPeak] = useState(0);
|
||||
const [count, setCount] = useState(0);
|
||||
const [peak, setPeak] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function fetchListenerCount() {
|
||||
try {
|
||||
const response = await fetch('/status.xsl');
|
||||
const data = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(data, 'text/html');
|
||||
const listeners = xmlDoc.getElementsByTagName('td');
|
||||
const fetchListenerCount = async () => {
|
||||
try {
|
||||
const response = await fetch("/status.xsl");
|
||||
const data = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(data, "text/html");
|
||||
const listeners = xmlDoc.getElementsByTagName("td");
|
||||
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (i === 9) {
|
||||
if (isMounted) {
|
||||
setCount(listeners[i].textContent);
|
||||
}
|
||||
}
|
||||
else if (i === 11) {
|
||||
const currentPeak = parseInt(listeners[i].textContent, 10);
|
||||
if (isMounted && currentPeak > peak) {
|
||||
setPeak(currentPeak);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching listener count:', error);
|
||||
}
|
||||
let newCount = count;
|
||||
let newPeak = peak;
|
||||
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (i === 9) {
|
||||
newCount = parseInt(listeners[i].textContent, 10);
|
||||
} else if (i === 11) {
|
||||
newPeak = parseInt(listeners[i].textContent, 10);
|
||||
}
|
||||
}
|
||||
|
||||
fetchListenerCount();
|
||||
|
||||
const intervalId = setInterval(fetchListenerCount, refreshInterval);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
count: parseInt(count, 10),
|
||||
peak: parseInt(peak, 10)
|
||||
if (isMounted) {
|
||||
setCount(newCount);
|
||||
setPeak((prevPeak) => (newPeak > prevPeak ? newPeak : prevPeak));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching listener count:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchListenerCount();
|
||||
const intervalId = setInterval(fetchListenerCount, refreshInterval);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
count: parseInt(count, 10),
|
||||
peak: parseInt(peak, 10),
|
||||
};
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useWallpaperList() {
|
||||
const [list, setList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/wallpapers/list.txt")
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
const lines = text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
setList(lines);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return list;
|
||||
}
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useWallpaperList() {
|
||||
const [list, setList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchList = async () => {
|
||||
try {
|
||||
const res = await fetch("/wallpapers/list.txt");
|
||||
const text = await res.text();
|
||||
const lines = text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
setList(lines);
|
||||
} catch (error) {
|
||||
console.error("Error fetching wallpaper list:", error);
|
||||
}
|
||||
};
|
||||
fetchList();
|
||||
}, []);
|
||||
|
||||
return list;
|
||||
}
|
||||
Referencia en una nueva incidencia
Block a user