mirror of https://github.com/btahir/next-beats
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			829 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			829 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			TypeScript
		
	
'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 (
 | 
						|
    <div className="pointer-events-none absolute inset-0 opacity-10 mix-blend-screen">
 | 
						|
      {staticPoints.map((point, i) => (
 | 
						|
        <div key={i} className="absolute h-px w-px bg-white" style={point} />
 | 
						|
      ))}
 | 
						|
    </div>
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
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<string>(
 | 
						|
    'lofi-theme',
 | 
						|
    'dark'
 | 
						|
  )
 | 
						|
  const [effectsVolume, setEffectsVolume] = useLocalStorage(
 | 
						|
    'lofi-effects-volume',
 | 
						|
    0.5
 | 
						|
  )
 | 
						|
  const [customChannels, setCustomChannels] = useLocalStorage<Channel[]>(
 | 
						|
    '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<any>(null)
 | 
						|
  const audioRefs = useRef<{ [key: string]: AudioCache }>({})
 | 
						|
  const [activeEffects, setActiveEffects] = useState<Set<string>>(new Set())
 | 
						|
  const [loadingEffects, setLoadingEffects] = useState<Set<string>>(new Set())
 | 
						|
  const [isAddingChannel, setIsAddingChannel] = useState(false)
 | 
						|
  const [newChannel, setNewChannel] = useState<Channel>({
 | 
						|
    name: '',
 | 
						|
    url: '',
 | 
						|
    description: '',
 | 
						|
    creator: '',
 | 
						|
    isCustom: true,
 | 
						|
  })
 | 
						|
  const [showDeleteConfirm, setShowDeleteConfirm] = useState<number | null>(
 | 
						|
    null
 | 
						|
  )
 | 
						|
  const [isEditingChannel, setIsEditingChannel] = useState<number | null>(null)
 | 
						|
  const [editingChannel, setEditingChannel] = useState<Channel>({
 | 
						|
    name: '',
 | 
						|
    url: '',
 | 
						|
    description: '',
 | 
						|
    creator: '',
 | 
						|
    isCustom: true,
 | 
						|
  })
 | 
						|
  const [isSettingsOpen, setIsSettingsOpen] = useState(false)
 | 
						|
 | 
						|
  const isBrowser = typeof window !== 'undefined'
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    if (!isBrowser) return
 | 
						|
    setMounted(true)
 | 
						|
  }, [isBrowser])
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    Object.entries(audioRefs.current).forEach(([effectId, cache]) => {
 | 
						|
      if (cache?.audio) {
 | 
						|
        cache.audio.volume = effectVolumes[effectId] * effectsVolume
 | 
						|
      }
 | 
						|
    })
 | 
						|
  }, [effectVolumes, effectsVolume])
 | 
						|
 | 
						|
  // Function to load audio on demand
 | 
						|
  const loadAudio = async (effectId: string) => {
 | 
						|
    const effect = soundEffects.find((e) => e.id === effectId)
 | 
						|
    if (!effect || effect.isYoutube) return
 | 
						|
 | 
						|
    if (loadingEffects.has(effectId) || audioRefs.current[effectId]?.loaded) {
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    setLoadingEffects((prev) => new Set(prev).add(effectId))
 | 
						|
 | 
						|
    try {
 | 
						|
      const audio = new Audio()
 | 
						|
 | 
						|
      const loadPromise = new Promise<void>((resolve, reject) => {
 | 
						|
        audio.addEventListener('canplaythrough', () => resolve(), {
 | 
						|
          once: true,
 | 
						|
        })
 | 
						|
        audio.addEventListener(
 | 
						|
          'error',
 | 
						|
          (error: ErrorEvent) => {
 | 
						|
            reject(
 | 
						|
              new Error(
 | 
						|
                `Audio loading error: ${error.message || 'Unknown error'}`
 | 
						|
              )
 | 
						|
            )
 | 
						|
          },
 | 
						|
          { once: true }
 | 
						|
        )
 | 
						|
 | 
						|
        audio.src = effect.file
 | 
						|
        audio.load()
 | 
						|
      })
 | 
						|
 | 
						|
      await loadPromise
 | 
						|
 | 
						|
      audio.loop = true
 | 
						|
      audio.volume = effectVolumes[effectId] * effectsVolume
 | 
						|
 | 
						|
      audioRefs.current[effectId] = {
 | 
						|
        audio,
 | 
						|
        loaded: true,
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      console.error(`Failed to load audio for ${effectId}:`, error)
 | 
						|
    } finally {
 | 
						|
      setLoadingEffects((prev) => {
 | 
						|
        const next = new Set(prev)
 | 
						|
        next.delete(effectId)
 | 
						|
        return next
 | 
						|
      })
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Toggle effect function
 | 
						|
  const toggleEffect = async (effectId: string) => {
 | 
						|
    const effect =
 | 
						|
      soundEffects.find((e) => e.id === effectId) ||
 | 
						|
      customEffects.find((e) => e.id === effectId)
 | 
						|
 | 
						|
    if (!effect) return
 | 
						|
 | 
						|
    try {
 | 
						|
      if (effect.isYoutube) {
 | 
						|
        // For YouTube effects, just toggle the active state
 | 
						|
        setActiveEffects((prev) => {
 | 
						|
          const newEffects = new Set(prev)
 | 
						|
          if (newEffects.has(effectId)) {
 | 
						|
            newEffects.delete(effectId)
 | 
						|
          } else {
 | 
						|
            newEffects.add(effectId)
 | 
						|
          }
 | 
						|
          return newEffects
 | 
						|
        })
 | 
						|
        return
 | 
						|
      }
 | 
						|
 | 
						|
      // Handle native audio effects
 | 
						|
      if (!audioRefs.current[effectId]?.loaded) {
 | 
						|
        await loadAudio(effectId)
 | 
						|
      }
 | 
						|
 | 
						|
      const audioCache = audioRefs.current[effectId]
 | 
						|
      if (!audioCache?.audio) return
 | 
						|
 | 
						|
      setActiveEffects((prev) => {
 | 
						|
        const newEffects = new Set(prev)
 | 
						|
        if (newEffects.has(effectId)) {
 | 
						|
          newEffects.delete(effectId)
 | 
						|
          audioCache.audio.pause()
 | 
						|
        } else {
 | 
						|
          newEffects.add(effectId)
 | 
						|
          audioCache.audio.play().catch((error) => {
 | 
						|
            console.error('Error playing audio:', error)
 | 
						|
            newEffects.delete(effectId)
 | 
						|
          })
 | 
						|
        }
 | 
						|
        return newEffects
 | 
						|
      })
 | 
						|
    } catch (error) {
 | 
						|
      console.error('Error toggling effect:', error)
 | 
						|
      // Clear loading state and active state on error
 | 
						|
      setLoadingEffects((prev) => {
 | 
						|
        const next = new Set(prev)
 | 
						|
        next.delete(effectId)
 | 
						|
        return next
 | 
						|
      })
 | 
						|
      setActiveEffects((prev) => {
 | 
						|
        const next = new Set(prev)
 | 
						|
        next.delete(effectId)
 | 
						|
        return next
 | 
						|
      })
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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)
 | 
						|
    // Update all active effect volumes
 | 
						|
    Object.entries(audioRefs.current).forEach(([effectId, cache]) => {
 | 
						|
      if (cache?.audio) {
 | 
						|
        cache.audio.volume = effectVolumes[effectId] * 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<Channel[]>(() => {
 | 
						|
    // 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,
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  const handleResetDefaults = () => {
 | 
						|
    if (
 | 
						|
      window.confirm(
 | 
						|
        'Are you sure you want to reset to default settings? This will remove all custom channels and effects.'
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      setCustomChannels([])
 | 
						|
      setHiddenDefaultChannels([])
 | 
						|
      setCustomEffects([])
 | 
						|
      setCurrentChannel(0)
 | 
						|
      setVolume(0.7)
 | 
						|
      setEffectsVolume(0.5)
 | 
						|
      setEffectVolumes(
 | 
						|
        Object.fromEntries(soundEffects.map((effect) => [effect.id, 0.5]))
 | 
						|
      )
 | 
						|
      setCurrentTheme('dark')
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Add useEffect to initialize and handle theme changes
 | 
						|
  useEffect(() => {
 | 
						|
    if (typeof window !== 'undefined') {
 | 
						|
      // Get theme from localStorage directly to ensure immediate application
 | 
						|
      const savedTheme = localStorage.getItem('lofi-theme') || 'dark'
 | 
						|
 | 
						|
      // Apply theme to root element
 | 
						|
      document.documentElement.dataset.theme = savedTheme
 | 
						|
 | 
						|
      // Update state if different
 | 
						|
      if (currentTheme !== savedTheme) {
 | 
						|
        setCurrentTheme(savedTheme)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }, []) // Run only on mount
 | 
						|
 | 
						|
  // Add useEffect to handle theme changes
 | 
						|
  useEffect(() => {
 | 
						|
    if (currentTheme) {
 | 
						|
      // Update root element when theme changes
 | 
						|
      document.documentElement.dataset.theme = currentTheme
 | 
						|
      // Also update localStorage directly
 | 
						|
      localStorage.setItem('lofi-theme', currentTheme)
 | 
						|
    }
 | 
						|
  }, [currentTheme])
 | 
						|
 | 
						|
  const handleLoadEffect = async (effectId: string, file: string) => {
 | 
						|
    if (audioRefs.current[effectId]?.loaded) return
 | 
						|
 | 
						|
    setLoadingEffects((prev) => new Set([...prev, effectId]))
 | 
						|
 | 
						|
    try {
 | 
						|
      const audio = new Audio(file)
 | 
						|
      await audio.load()
 | 
						|
      audio.loop = true
 | 
						|
      audioRefs.current[effectId] = { audio, loaded: true }
 | 
						|
    } catch (error) {
 | 
						|
      console.error(`Error loading effect ${effectId}:`, error)
 | 
						|
    } finally {
 | 
						|
      setLoadingEffects((prev) => {
 | 
						|
        const next = new Set(prev)
 | 
						|
        next.delete(effectId)
 | 
						|
        return next
 | 
						|
      })
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const handleToggleEffect = async (effectId: string, file: string) => {
 | 
						|
    if (!audioRefs.current[effectId]?.loaded) {
 | 
						|
      await handleLoadEffect(effectId, file)
 | 
						|
    }
 | 
						|
 | 
						|
    const audio = audioRefs.current[effectId]?.audio
 | 
						|
    if (!audio) return
 | 
						|
 | 
						|
    if (activeEffects.has(effectId)) {
 | 
						|
      audio.pause()
 | 
						|
      setActiveEffects((prev) => {
 | 
						|
        const next = new Set(prev)
 | 
						|
        next.delete(effectId)
 | 
						|
        return next
 | 
						|
      })
 | 
						|
    } else {
 | 
						|
      audio.volume = effectVolumes[effectId] * effectsVolume
 | 
						|
      audio.play()
 | 
						|
      setActiveEffects((prev) => new Set([...prev, effectId]))
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      className={styles['theme-container']}
 | 
						|
      data-theme={mounted ? currentTheme : 'dark'}
 | 
						|
    >
 | 
						|
      <a
 | 
						|
        href="https://github.com/btahir/next-beats"
 | 
						|
        target="_blank"
 | 
						|
        rel="noopener noreferrer"
 | 
						|
        className="fixed right-4 top-4 hidden text-[var(--lofi-text-primary)] transition-opacity hover:opacity-70 lg:block"
 | 
						|
        aria-label="View source on GitHub"
 | 
						|
      >
 | 
						|
        <GitHubIcon />
 | 
						|
      </a>
 | 
						|
      <div className="flex min-h-screen w-full items-start justify-center bg-[var(--lofi-background)] p-4 transition-colors duration-500 sm:items-center sm:p-8">
 | 
						|
        <div className="w-full max-w-4xl space-y-8 py-4">
 | 
						|
          {/* Retro TV */}
 | 
						|
          <div className="shadow-[var(--lofi-accent)]/30 relative aspect-video overflow-hidden rounded-2xl border-4 border-[var(--lofi-border)] bg-black shadow-md transition-all duration-500">
 | 
						|
            <div className="absolute inset-0">
 | 
						|
              {mounted && <StaticEffect />}
 | 
						|
              {mounted && (
 | 
						|
                // @ts-ignore
 | 
						|
                <ReactPlayer
 | 
						|
                  ref={playerRef}
 | 
						|
                  url={allChannels[currentChannel]?.url || ''}
 | 
						|
                  playing={isPlaying}
 | 
						|
                  volume={volume}
 | 
						|
                  loop
 | 
						|
                  width="100%"
 | 
						|
                  height="100%"
 | 
						|
                  onProgress={handleProgress}
 | 
						|
                  onError={(error: Error) =>
 | 
						|
                    console.error('Player error:', error)
 | 
						|
                  }
 | 
						|
                  config={{
 | 
						|
                    playerVars: {
 | 
						|
                      controls: 0,
 | 
						|
                      modestbranding: 1,
 | 
						|
                      iv_load_policy: 3,
 | 
						|
                      rel: 0,
 | 
						|
                      showinfo: 0,
 | 
						|
                    },
 | 
						|
                  }}
 | 
						|
                />
 | 
						|
              )}
 | 
						|
              <div className="absolute top-0 left-0 right-0 bg-gradient-to-b from-black/70 to-transparent p-4">
 | 
						|
                <div className="flex items-center justify-between">
 | 
						|
                  <div className="flex items-center space-x-2">
 | 
						|
                    <div className="h-2 w-2 animate-pulse rounded-full bg-red-500" />
 | 
						|
                    <span className="font-mono text-xs text-red-400">LIVE</span>
 | 
						|
                  </div>
 | 
						|
                  <span className="font-mono text-xs text-white/80">
 | 
						|
                    CH{currentChannel + 1}
 | 
						|
                  </span>
 | 
						|
                </div>
 | 
						|
              </div>
 | 
						|
              <div className="animate-scan absolute bottom-0 left-0 right-0 h-px bg-white/10" />
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
 | 
						|
          {/* Main Controls Section */}
 | 
						|
          <div className="space-y-6 rounded-xl bg-[var(--lofi-card)] p-4 transition-colors duration-500 sm:p-6">
 | 
						|
            {/* Channel Information */}
 | 
						|
            <div className="relative space-y-1 px-2 font-mono text-[var(--lofi-text-primary)]">
 | 
						|
              {/* Settings button */}
 | 
						|
              <div className="absolute top-0 right-0 flex justify-center">
 | 
						|
                <button
 | 
						|
                  onClick={() => setIsSettingsOpen(true)}
 | 
						|
                  className="rounded-full bg-[var(--lofi-button-bg)] p-2 text-[var(--lofi-button-text)] transition-colors hover:bg-[var(--lofi-button-hover)]"
 | 
						|
                >
 | 
						|
                  <Settings size={18} />
 | 
						|
                </button>
 | 
						|
              </div>
 | 
						|
              {mounted ? (
 | 
						|
                <>
 | 
						|
                  <h2 className="text-xl font-bold">
 | 
						|
                    {allChannels[currentChannel].name}
 | 
						|
                  </h2>
 | 
						|
                  <p className="text-sm text-[var(--lofi-text-secondary)]">
 | 
						|
                    {allChannels[currentChannel].description}
 | 
						|
                  </p>
 | 
						|
                  <p className="text-sm text-[var(--lofi-accent)]">
 | 
						|
                    by {allChannels[currentChannel].creator}
 | 
						|
                  </p>
 | 
						|
                </>
 | 
						|
              ) : (
 | 
						|
                <>
 | 
						|
                  <h2 className="text-xl font-bold">
 | 
						|
                    {DEFAULT_CHANNELS[0].name}
 | 
						|
                  </h2>
 | 
						|
                  <p className="text-sm text-[var(--lofi-text-secondary)]">
 | 
						|
                    {DEFAULT_CHANNELS[0].description}
 | 
						|
                  </p>
 | 
						|
                  <p className="text-sm text-purple-400">
 | 
						|
                    by {DEFAULT_CHANNELS[0].creator}
 | 
						|
                  </p>
 | 
						|
                </>
 | 
						|
              )}
 | 
						|
            </div>
 | 
						|
 | 
						|
            {/* Channel Buttons */}
 | 
						|
            {mounted && (
 | 
						|
              <div className="-mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0">
 | 
						|
                <ChannelButtons
 | 
						|
                  channels={allChannels}
 | 
						|
                  currentChannel={currentChannel}
 | 
						|
                  setCurrentChannel={setCurrentChannel}
 | 
						|
                  currentTheme={currentTheme}
 | 
						|
                />
 | 
						|
              </div>
 | 
						|
            )}
 | 
						|
 | 
						|
            {/* Controls Container */}
 | 
						|
            <div className="space-y-4">
 | 
						|
              <div className="flex items-center justify-between gap-3">
 | 
						|
                {/* Left Side - Playback Controls */}
 | 
						|
                {mounted && (
 | 
						|
                  <PlaybackControls
 | 
						|
                    isPlaying={isPlaying}
 | 
						|
                    setIsPlaying={setIsPlaying}
 | 
						|
                    volume={volume}
 | 
						|
                    setVolume={handleVolumeChange}
 | 
						|
                    changeChannel={changeChannel}
 | 
						|
                  />
 | 
						|
                )}
 | 
						|
 | 
						|
                {/* Right Side - Channel Management */}
 | 
						|
                {mounted && (
 | 
						|
                  <div className="flex shrink-0 items-center">
 | 
						|
                    <ChannelManagement
 | 
						|
                      isAddingChannel={isAddingChannel}
 | 
						|
                      setIsAddingChannel={setIsAddingChannel}
 | 
						|
                      newChannel={newChannel}
 | 
						|
                      setNewChannel={setNewChannel}
 | 
						|
                      saveChannel={handleSaveChannel}
 | 
						|
                      currentTheme={currentTheme}
 | 
						|
                      currentChannel={currentChannel}
 | 
						|
                      handleEditChannel={handleEditChannel}
 | 
						|
                      setShowDeleteConfirm={setShowDeleteConfirm}
 | 
						|
                    />
 | 
						|
                  </div>
 | 
						|
                )}
 | 
						|
              </div>
 | 
						|
 | 
						|
              {/* Progress Bar */}
 | 
						|
              <div className="">
 | 
						|
                <div className="h-1 w-full overflow-hidden rounded-full bg-[var(--lofi-card-hover)]">
 | 
						|
                  <div
 | 
						|
                    className="h-full bg-[var(--lofi-accent)] transition-all duration-300"
 | 
						|
                    style={{ width: `${played * 100}%` }}
 | 
						|
                  />
 | 
						|
                </div>
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
 | 
						|
          {/* Sound Effects Section - Separated into its own card */}
 | 
						|
          {mounted && (
 | 
						|
            <div className="rounded-xl bg-[var(--lofi-card)] p-4 transition-colors duration-500 sm:p-6">
 | 
						|
              <SoundEffectsControls
 | 
						|
                activeEffects={activeEffects}
 | 
						|
                toggleEffect={toggleEffect}
 | 
						|
                effectsVolume={effectsVolume}
 | 
						|
                setEffectsVolume={handleEffectsVolumeChange}
 | 
						|
                effectVolumes={effectVolumes}
 | 
						|
                setEffectVolumes={setEffectVolumes}
 | 
						|
                currentTheme={currentTheme}
 | 
						|
                customEffects={customEffects}
 | 
						|
                setCustomEffects={setCustomEffects}
 | 
						|
                loadingEffects={loadingEffects}
 | 
						|
              />
 | 
						|
            </div>
 | 
						|
          )}
 | 
						|
        </div>
 | 
						|
 | 
						|
        {/* Channel Edit Modal */}
 | 
						|
        {isEditingChannel !== null && (
 | 
						|
          <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
 | 
						|
            <div
 | 
						|
              className={`w-full max-w-md rounded-lg bg-[var(--lofi-card)] p-6`}
 | 
						|
            >
 | 
						|
              <h3
 | 
						|
                className={`mb-4 text-lg font-bold text-[var(--lofi-text-primary)]`}
 | 
						|
              >
 | 
						|
                Edit Channel {isEditingChannel + 1}
 | 
						|
              </h3>
 | 
						|
              <div className="space-y-3">
 | 
						|
                <input
 | 
						|
                  type="text"
 | 
						|
                  placeholder="Channel Name"
 | 
						|
                  value={editingChannel.name}
 | 
						|
                  onChange={(e) =>
 | 
						|
                    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)]`}
 | 
						|
                />
 | 
						|
                <input
 | 
						|
                  type="text"
 | 
						|
                  placeholder="YouTube URL"
 | 
						|
                  value={editingChannel.url}
 | 
						|
                  onChange={(e) =>
 | 
						|
                    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)]`}
 | 
						|
                />
 | 
						|
                <input
 | 
						|
                  type="text"
 | 
						|
                  placeholder="Description"
 | 
						|
                  value={editingChannel.description}
 | 
						|
                  onChange={(e) =>
 | 
						|
                    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)]`}
 | 
						|
                />
 | 
						|
                <input
 | 
						|
                  type="text"
 | 
						|
                  placeholder="Creator"
 | 
						|
                  value={editingChannel.creator}
 | 
						|
                  onChange={(e) =>
 | 
						|
                    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)]`}
 | 
						|
                />
 | 
						|
                <div className="flex justify-end space-x-2">
 | 
						|
                  <button
 | 
						|
                    onClick={() => setIsEditingChannel(null)}
 | 
						|
                    className={`rounded-full px-3 py-1 text-xs text-[var(--lofi-text-primary)] hover:text-[var(--lofi-text-secondary)]`}
 | 
						|
                  >
 | 
						|
                    Cancel
 | 
						|
                  </button>
 | 
						|
                  <button
 | 
						|
                    onClick={handleSaveEditedChannel}
 | 
						|
                    className="flex items-center space-x-2 rounded-full bg-[var(--lofi-button-bg)] px-3 py-1 text-xs text-[var(--lofi-button-text)] hover:bg-[var(--lofi-button-hover)]"
 | 
						|
                  >
 | 
						|
                    <Save size={14} />
 | 
						|
                    <span>Save Changes</span>
 | 
						|
                  </button>
 | 
						|
                </div>
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
 | 
						|
        {/* Delete Confirmation Modal */}
 | 
						|
        {showDeleteConfirm !== null && (
 | 
						|
          <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
 | 
						|
            <div
 | 
						|
              className={`w-full max-w-sm rounded-lg bg-[var(--lofi-card)] p-6`}
 | 
						|
            >
 | 
						|
              <h3
 | 
						|
                className={`mb-4 text-lg font-bold text-[var(--lofi-text-primary)]`}
 | 
						|
              >
 | 
						|
                Delete Channel
 | 
						|
              </h3>
 | 
						|
              {allChannels.length <= 1 ? (
 | 
						|
                <p className="mb-4 text-sm text-[var(--lofi-text-secondary)]">
 | 
						|
                  Cannot delete the last remaining channel.
 | 
						|
                </p>
 | 
						|
              ) : (
 | 
						|
                <p className={`mb-4 text-sm text-[var(--lofi-text-secondary)]`}>
 | 
						|
                  Are you sure you want to delete "
 | 
						|
                  {allChannels[showDeleteConfirm].name}"?
 | 
						|
                  {showDeleteConfirm <
 | 
						|
                    DEFAULT_CHANNELS.length - hiddenDefaultChannels.length &&
 | 
						|
                    ' This will hide the default channel.'}
 | 
						|
                </p>
 | 
						|
              )}
 | 
						|
              <div className="flex justify-end space-x-2">
 | 
						|
                <button
 | 
						|
                  onClick={() => setShowDeleteConfirm(null)}
 | 
						|
                  className={`rounded-full px-3 py-1 text-xs text-[var(--lofi-text-primary)] hover:text-[var(--lofi-text-secondary)]`}
 | 
						|
                >
 | 
						|
                  Cancel
 | 
						|
                </button>
 | 
						|
                <button
 | 
						|
                  onClick={() => handleDeleteChannel(showDeleteConfirm)}
 | 
						|
                  className={`flex items-center space-x-2 rounded-full ${
 | 
						|
                    allChannels.length <= 1
 | 
						|
                      ? 'cursor-not-allowed bg-[var(--lofi-button-bg)]'
 | 
						|
                      : 'bg-[var(--lofi-button-bg)] hover:bg-[var(--lofi-button-hover)]'
 | 
						|
                  } px-3 py-1 text-xs text-[var(--lofi-button-text)]`}
 | 
						|
                  disabled={allChannels.length <= 1}
 | 
						|
                >
 | 
						|
                  <X size={14} />
 | 
						|
                  <span>Delete Channel</span>
 | 
						|
                </button>
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
 | 
						|
        <SettingsModal
 | 
						|
          isOpen={isSettingsOpen}
 | 
						|
          onClose={() => setIsSettingsOpen(false)}
 | 
						|
          currentTheme={currentTheme}
 | 
						|
          setCurrentTheme={handleThemeChange}
 | 
						|
          onResetDefaults={handleResetDefaults}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
export default EnhancedLofiPlayer
 |