'use client' import React, { useState, useRef, useEffect, useMemo } from 'react' import dynamic from 'next/dynamic' import { X, Save, Settings } from 'lucide-react' import { GitHubIcon } from '@/components/GitHubIcon' import { soundEffects, DEFAULT_CHANNELS } from '@/lib/lofi_data' import ChannelButtons from '@/components/ChannelButtons' import PlaybackControls from '@/components/PlaybackControls' import ChannelManagement from '@/components/ChannelManagement' import SoundEffectsControls from '@/components/SoundEffectsControls' import { useLocalStorage } from '@/hooks/useLocalStorage' import { Channel, CustomSoundEffect } from '@/types/lofi' import SettingsModal from '@/components/SettingsModal' import styles from '@/styles/Lofi.module.css' const ReactPlayer = dynamic(() => import('react-player/youtube'), { ssr: false, }) // Type Definitions type AudioCache = { audio: HTMLAudioElement loaded: boolean } const StaticEffect = () => { const [staticPoints, setStaticPoints] = useState< { left: string; top: string; opacity: number }[] >([]) useEffect(() => { setStaticPoints( Array.from({ length: 100 }, () => ({ left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, opacity: Math.random() * 0.5, })) ) }, []) return (
{staticPoints.map((point, i) => (
))}
) } const EnhancedLofiPlayer = () => { const [mounted, setMounted] = useState(false) const [currentChannel, setCurrentChannel] = useState(0) const [isPlaying, setIsPlaying] = useState(true) const [volume, setVolume] = useLocalStorage('lofi-volume', 0.7) const [played, setPlayed] = useState(0) const [currentTheme, setCurrentTheme] = useLocalStorage( 'lofi-theme', 'dark' ) const [effectsVolume, setEffectsVolume] = useLocalStorage( 'lofi-effects-volume', 0.5 ) const [customChannels, setCustomChannels] = useLocalStorage( 'customChannels', [] ) const [hiddenDefaultChannels, setHiddenDefaultChannels] = useLocalStorage< number[] >('hiddenDefaultChannels', []) const [effectVolumes, setEffectVolumes] = useLocalStorage<{ [key: string]: number }>( 'lofi-effect-volumes', Object.fromEntries(soundEffects.map((effect) => [effect.id, 0.5])) ) const [customEffects, setCustomEffects] = useLocalStorage< CustomSoundEffect[] >('customSoundEffects', []) const playerRef = useRef(null) const [activeEffects, setActiveEffects] = useState>(new Set()) const [isAddingChannel, setIsAddingChannel] = useState(false) const [newChannel, setNewChannel] = useState({ name: '', url: '', description: '', creator: '', isCustom: true, }) const [showDeleteConfirm, setShowDeleteConfirm] = useState( null ) const [isEditingChannel, setIsEditingChannel] = useState(null) const [editingChannel, setEditingChannel] = useState({ name: '', url: '', description: '', creator: '', isCustom: true, }) const [isSettingsOpen, setIsSettingsOpen] = useState(false) const isBrowser = typeof window !== 'undefined' useEffect(() => { if (!isBrowser) return setMounted(true) }, [isBrowser]) useEffect(() => { if (typeof window !== 'undefined') { // Get theme from localStorage directly to ensure immediate application const savedTheme = localStorage.getItem('lofi-theme') || 'dark' document.documentElement.dataset.theme = savedTheme if (currentTheme !== savedTheme) { setCurrentTheme(savedTheme) } } }, []) const toggleEffect = (effectId: string) => { setActiveEffects((prev) => { const newEffects = new Set(prev) if (newEffects.has(effectId)) { newEffects.delete(effectId) } else { newEffects.add(effectId) } return newEffects }) } const handleProgress = (state: { played: number }) => { if (!isPlaying) return setPlayed(state.played) } const handleChannelChange = (index: number) => { setCurrentChannel(index) setPlayed(0) } const handleVolumeChange = (newVolume: number) => { setVolume(newVolume) } const handleEffectsVolumeChange = (newVolume: number) => { setEffectsVolume(newVolume) } const handleThemeChange = (theme: string) => { setCurrentTheme(theme) } const handleAddChannel = () => { if (!newChannel.name || !newChannel.url) { alert('Channel Name and URL are required.') return } const updatedChannels: Channel[] = [ ...customChannels, { ...newChannel, isCustom: true }, ] setCustomChannels(updatedChannels) setIsAddingChannel(false) setNewChannel({ name: '', url: '', description: '', creator: '', isCustom: true, }) } const handleDeleteChannel = (channelIndex: number) => { const channelToDelete = allChannels[channelIndex] if (!channelToDelete) return let newChannelIndex = channelIndex if ( !channelToDelete.isCustom && typeof channelToDelete.originalIndex === 'number' ) { // It's a default channel if (!hiddenDefaultChannels.includes(channelToDelete.originalIndex)) { const updatedHidden = [ ...hiddenDefaultChannels, channelToDelete.originalIndex, ] setHiddenDefaultChannels(updatedHidden) } } else { // It's a custom channel const updatedChannels = customChannels.filter( (channel) => channel.name !== channelToDelete.name || channel.url !== channelToDelete.url ) setCustomChannels(updatedChannels) } // Switch channel if needed if (channelIndex === currentChannel) { newChannelIndex = Math.max(0, channelIndex - 1) setCurrentChannel(newChannelIndex) } setShowDeleteConfirm(null) } const changeChannel = (direction: 'next' | 'prev') => { setCurrentChannel((prev) => { if (direction === 'next') { return (prev + 1) % allChannels.length } else { return (prev - 1 + allChannels.length) % allChannels.length } }) } const allChannels = useMemo(() => { // Get visible default channels const visibleDefaultChannels = DEFAULT_CHANNELS.map((channel, index) => ({ ...channel, isCustom: false, originalIndex: index, })).filter((_, index) => !hiddenDefaultChannels.includes(index)) // Add custom channels return [...visibleDefaultChannels, ...customChannels] }, [customChannels, hiddenDefaultChannels]) const handleSaveChannel = () => { if (!newChannel.name || !newChannel.url) { alert('Channel Name and URL are required.') return } const updatedChannels: Channel[] = [ ...customChannels, { ...newChannel, isCustom: true }, ] setCustomChannels(updatedChannels) setIsAddingChannel(false) setNewChannel({ name: '', url: '', description: '', creator: '', isCustom: true, }) } const handleEditChannel = (channelIndex: number) => { console.log('Starting edit for channel:', { channelIndex, channel: allChannels[channelIndex], }) const channel = allChannels[channelIndex] setEditingChannel({ ...channel }) setIsEditingChannel(channelIndex) } const handleSaveEditedChannel = () => { if (!editingChannel.name || !editingChannel.url) { alert('Channel Name and URL are required.') return } const channelToEdit = allChannels[isEditingChannel ?? -1] if (!channelToEdit) return if (channelToEdit.isCustom) { // Editing a custom channel const customIndex = customChannels.findIndex( (channel) => channel.name === channelToEdit.name && channel.url === channelToEdit.url ) if (customIndex !== -1) { const updatedChannels = [...customChannels] updatedChannels[customIndex] = { ...editingChannel, isCustom: true } setCustomChannels(updatedChannels) } } else if (typeof channelToEdit.originalIndex === 'number') { // Editing a default channel - hide default and add as custom if (!hiddenDefaultChannels.includes(channelToEdit.originalIndex)) { const updatedHidden = [ ...hiddenDefaultChannels, channelToEdit.originalIndex, ] setHiddenDefaultChannels(updatedHidden) } const updatedChannels = [ ...customChannels, { ...editingChannel, isCustom: true }, ] setCustomChannels(updatedChannels) } setIsEditingChannel(null) setEditingChannel({ name: '', url: '', description: '', creator: '', isCustom: true, }) } return (
{/* Retro TV */}
{mounted && } {mounted && ( // @ts-ignore console.error('Player error:', error) } config={{ playerVars: { controls: 0, modestbranding: 1, iv_load_policy: 3, rel: 0, showinfo: 0, }, }} /> )}
LIVE
CH{currentChannel + 1}
{/* Main Controls Section */}
{/* Channel Information */}
{/* Settings button */}
{mounted ? ( <>

{allChannels[currentChannel].name}

{allChannels[currentChannel].description}

by {allChannels[currentChannel].creator}

) : ( <>

{DEFAULT_CHANNELS[0].name}

{DEFAULT_CHANNELS[0].description}

by {DEFAULT_CHANNELS[0].creator}

)}
{/* Channel Buttons */} {mounted && (
)} {/* Controls Container */}
{/* Left Side - Playback Controls */} {mounted && ( )} {/* Right Side - Channel Management */} {mounted && (
)}
{/* Progress Bar */}
{/* Sound Effects Section - Separated into its own card */} {mounted && (
)}
{/* Channel Edit Modal */} {isEditingChannel !== null && (

Edit Channel {isEditingChannel + 1}

setEditingChannel({ ...editingChannel, name: e.target.value, }) } className={`w-full rounded-lg bg-[var(--lofi-card-hover)] px-3 py-2 text-sm text-[var(--lofi-text-primary)] placeholder:text-[var(--lofi-text-secondary)]`} /> setEditingChannel({ ...editingChannel, url: e.target.value, }) } className={`w-full rounded-lg bg-[var(--lofi-card-hover)] px-3 py-2 text-sm text-[var(--lofi-text-primary)] placeholder:text-[var(--lofi-text-secondary)]`} /> setEditingChannel({ ...editingChannel, description: e.target.value, }) } className={`w-full rounded-lg bg-[var(--lofi-card-hover)] px-3 py-2 text-sm text-[var(--lofi-text-primary)] placeholder:text-[var(--lofi-text-secondary)]`} /> setEditingChannel({ ...editingChannel, creator: e.target.value, }) } className={`w-full rounded-lg bg-[var(--lofi-card-hover)] px-3 py-2 text-sm text-[var(--lofi-text-primary)] placeholder:text-[var(--lofi-text-secondary)]`} />
)} {/* Delete Confirmation Modal */} {showDeleteConfirm !== null && (

Delete Channel

{allChannels.length <= 1 ? (

Cannot delete the last remaining channel.

) : (

Are you sure you want to delete " {allChannels[showDeleteConfirm].name}"? {showDeleteConfirm < DEFAULT_CHANNELS.length - hiddenDefaultChannels.length && ' This will hide the default channel.'}

)}
)} setIsSettingsOpen(false)} currentTheme={currentTheme} setCurrentTheme={handleThemeChange} />
) } export default EnhancedLofiPlayer