mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 17:58:38 +00:00
219 lines
6.3 KiB
TypeScript
219 lines
6.3 KiB
TypeScript
import { createContext, useContext, useState, useRef, useCallback, useEffect, ReactNode } from 'react';
|
|
import { useAudioAnalyzer } from './AudioAnalyzerContext';
|
|
|
|
export interface Station {
|
|
stationuuid: string;
|
|
name: string;
|
|
url: string;
|
|
favicon: string;
|
|
country: string;
|
|
tags: string;
|
|
bitrate: number;
|
|
}
|
|
|
|
interface MusicContextType {
|
|
isPlaying: boolean;
|
|
isBuffering: boolean;
|
|
volume: number;
|
|
stations: Station[];
|
|
currentIndex: number;
|
|
selectedStation: Station | null;
|
|
hasFetched: boolean;
|
|
setVolume: (volume: number) => void;
|
|
playStation: (station: Station, index: number) => void;
|
|
togglePlay: () => void;
|
|
playNext: () => void;
|
|
playPrevious: () => void;
|
|
fetchStations: () => Promise<void>;
|
|
stopAudio: () => void;
|
|
}
|
|
|
|
const MusicContext = createContext<MusicContextType | undefined>(undefined);
|
|
|
|
export const MusicProvider = ({ children }: { children: ReactNode }) => {
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [isBuffering, setIsBuffering] = useState(false);
|
|
const [volume, setVolumeState] = useState(50);
|
|
const [stations, setStations] = useState<Station[]>([]);
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [selectedStation, setSelectedStation] = useState<Station | null>(null);
|
|
const [hasFetched, setHasFetched] = useState(false);
|
|
const [failedStations, setFailedStations] = useState<Set<string>>(new Set());
|
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
const { connectAudioElement, disconnectAudioElement } = useAudioAnalyzer();
|
|
|
|
// Update volume on audio element when volume state changes
|
|
useEffect(() => {
|
|
if (audioRef.current) {
|
|
audioRef.current.volume = volume / 100;
|
|
}
|
|
}, [volume]);
|
|
|
|
const setVolume = useCallback((newVolume: number) => {
|
|
setVolumeState(newVolume);
|
|
}, []);
|
|
|
|
const fetchStations = useCallback(async () => {
|
|
if (hasFetched) return;
|
|
try {
|
|
const response = await fetch(
|
|
'https://de1.api.radio-browser.info/json/stations/topclick/100'
|
|
);
|
|
if (!response.ok) throw new Error('Failed to fetch stations');
|
|
const data: Station[] = await response.json();
|
|
const validStations = data.filter(s => !failedStations.has(s.stationuuid));
|
|
setStations(validStations);
|
|
setHasFetched(true);
|
|
} catch (err) {
|
|
console.error('Error fetching stations:', err);
|
|
}
|
|
}, [hasFetched, failedStations]);
|
|
|
|
const stopCurrentAudio = useCallback(() => {
|
|
if (audioRef.current) {
|
|
disconnectAudioElement(audioRef.current);
|
|
audioRef.current.pause();
|
|
audioRef.current.src = '';
|
|
audioRef.current.onplay = null;
|
|
audioRef.current.onpause = null;
|
|
audioRef.current.onerror = null;
|
|
audioRef.current.onwaiting = null;
|
|
audioRef.current.onplaying = null;
|
|
audioRef.current = null;
|
|
}
|
|
setIsBuffering(false);
|
|
}, [disconnectAudioElement]);
|
|
|
|
const playStation = useCallback((station: Station, index: number) => {
|
|
stopCurrentAudio();
|
|
setIsBuffering(true);
|
|
|
|
const audio = new Audio(station.url);
|
|
audio.crossOrigin = 'anonymous';
|
|
audio.volume = volume / 100;
|
|
audioRef.current = audio;
|
|
|
|
// Connect to analyzer for visualization
|
|
connectAudioElement(audio);
|
|
|
|
audio.onerror = () => {
|
|
console.error('Failed to play station:', station.name);
|
|
setFailedStations(prev => new Set(prev).add(station.stationuuid));
|
|
setIsPlaying(false);
|
|
setIsBuffering(false);
|
|
setSelectedStation(null);
|
|
};
|
|
|
|
audio.onwaiting = () => {
|
|
setIsBuffering(true);
|
|
};
|
|
|
|
audio.onplaying = () => {
|
|
setIsBuffering(false);
|
|
setIsPlaying(true);
|
|
// Set flag for music listener achievement (checked in AchievementsContext)
|
|
localStorage.setItem('has-listened-to-music', 'true');
|
|
};
|
|
|
|
audio.onplay = () => {
|
|
setIsPlaying(true);
|
|
};
|
|
|
|
audio.onpause = () => {
|
|
if (audioRef.current === audio) {
|
|
setIsPlaying(false);
|
|
}
|
|
};
|
|
|
|
audio.play().catch((err) => {
|
|
console.error('Playback error:', err);
|
|
setFailedStations(prev => new Set(prev).add(station.stationuuid));
|
|
setIsPlaying(false);
|
|
setIsBuffering(false);
|
|
});
|
|
|
|
setSelectedStation(station);
|
|
setCurrentIndex(index);
|
|
}, [volume, stopCurrentAudio, connectAudioElement]);
|
|
|
|
const togglePlay = useCallback(() => {
|
|
if (!audioRef.current || !selectedStation) {
|
|
if (stations.length > 0) {
|
|
playStation(stations[0], 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isPlaying) {
|
|
audioRef.current.pause();
|
|
} else {
|
|
setIsBuffering(true);
|
|
audioRef.current.play().catch(() => {
|
|
setIsBuffering(false);
|
|
});
|
|
}
|
|
}, [selectedStation, stations, playStation, isPlaying]);
|
|
|
|
const playNext = useCallback(() => {
|
|
if (stations.length === 0) return;
|
|
const nextIndex = (currentIndex + 1) % stations.length;
|
|
playStation(stations[nextIndex], nextIndex);
|
|
}, [currentIndex, stations, playStation]);
|
|
|
|
const playPrevious = useCallback(() => {
|
|
if (stations.length === 0) return;
|
|
const prevIndex = currentIndex === 0 ? stations.length - 1 : currentIndex - 1;
|
|
playStation(stations[prevIndex], prevIndex);
|
|
}, [currentIndex, stations, playStation]);
|
|
|
|
// Handle media keys
|
|
useEffect(() => {
|
|
const handleMediaKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'MediaPlayPause') {
|
|
e.preventDefault();
|
|
togglePlay();
|
|
} else if (e.key === 'MediaTrackNext') {
|
|
e.preventDefault();
|
|
playNext();
|
|
} else if (e.key === 'MediaTrackPrevious') {
|
|
e.preventDefault();
|
|
playPrevious();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleMediaKey);
|
|
return () => window.removeEventListener('keydown', handleMediaKey);
|
|
}, [togglePlay, playNext, playPrevious]);
|
|
|
|
return (
|
|
<MusicContext.Provider
|
|
value={{
|
|
isPlaying,
|
|
isBuffering,
|
|
volume,
|
|
stations,
|
|
currentIndex,
|
|
selectedStation,
|
|
hasFetched,
|
|
setVolume,
|
|
playStation,
|
|
togglePlay,
|
|
playNext,
|
|
playPrevious,
|
|
fetchStations,
|
|
stopAudio: stopCurrentAudio,
|
|
}}
|
|
>
|
|
{children}
|
|
</MusicContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useMusic = () => {
|
|
const context = useContext(MusicContext);
|
|
if (context === undefined) {
|
|
throw new Error('useMusic must be used within a MusicProvider');
|
|
}
|
|
return context;
|
|
};
|