import React, { useState, useEffect, useRef } from 'react'; import { Play, Search, Tv, AlertCircle, List, Loader2, ShieldAlert, Wifi, Github, ChevronDown, Clapperboard, Trophy, Music, Newspaper, Baby, Menu, X, RotateCw, Plus, Trash2, Library, FolderOpen, ExternalLink, RefreshCw } from 'lucide-react'; // Helper untuk memuat script HLS.js secara dinamis const useHlsScript = (src) => { const [loaded, setLoaded] = useState(false); useEffect(() => { if (window.Hls) { setLoaded(true); return; } const script = document.createElement('script'); script.src = src; script.async = true; script.onload = () => setLoaded(true); document.body.appendChild(script); return () => { document.body.removeChild(script); }; }, [src]); return loaded; }; const App = () => { const hlsLoaded = useHlsScript('https://cdn.jsdelivr.net/npm/hls.js@latest'); // -- STATE DATA -- const [playlists, setPlaylists] = useState([]); const [activePlaylistId, setActivePlaylistId] = useState(null); // -- STATE INPUT -- const [playlistUrl, setPlaylistUrl] = useState('https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8'); const [playlistNameInput, setPlaylistNameInput] = useState(''); const [rawM3U, setRawM3U] = useState(''); const [inputMode, setInputMode] = useState('url'); const [useProxy, setUseProxy] = useState(true); // -- STATE VIEW / UI -- const [showAddPlaylist, setShowAddPlaylist] = useState(true); const [channels, setChannels] = useState([]); const [categories, setCategories] = useState([]); // -- FILTER STATE -- const [selectedCategory, setSelectedCategory] = useState('All'); const [selectedTheme, setSelectedTheme] = useState('All'); const [filteredChannels, setFilteredChannels] = useState([]); const [visibleLimit, setVisibleLimit] = useState(50); const [selectedChannel, setSelectedChannel] = useState(null); const [searchTerm, setSearchTerm] = useState(''); // -- LAYOUT STATE -- const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isFloatingActive, setIsFloatingActive] = useState(false); const floatingTimer = useRef(null); // -- SYSTEM STATE -- const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [playerError, setPlayerError] = useState(null); // Error khusus player const [forceHttps, setForceHttps] = useState(false); // Toggle paksa HTTPS const videoRef = useRef(null); const hlsRef = useRef(null); const listRef = useRef(null); // Definisi Tema Pintar const THEMES = [ { id: 'Sports', label: 'Olahraga', keywords: ['sport', 'soccer', 'football', 'bola', 'racing', 'moto', 'nba', 'ufc', 'wwe', 'tennis', 'golf', 'cricket', 'bein', 'espn', 'fox sports'] }, { id: 'Movies', label: 'Film & Drama', keywords: ['movie', 'film', 'cinema', 'drama', 'action', 'hbo', 'blockbuster', 'series', 'bioskop'] }, { id: 'News', label: 'Berita', keywords: ['news', 'berita', 'cnn', 'bbc', 'jazeera', 'tvone', 'kompas', 'metrotv', 'cnbc', 'weather', 'info'] }, { id: 'Music', label: 'Musik', keywords: ['music', 'musik', 'mtv', 'hits', 'pop', 'dangdut', 'radio', 'concert', 'prambors'] }, { id: 'Kids', label: 'Anak & Kartun', keywords: ['kids', 'cartoon', 'kartun', 'anime', 'animation', 'disney', 'nick', 'jr', 'toddler', 'baby'] }, { id: 'Religious', label: 'Religi', keywords: ['islam', 'muslim', 'quran', 'church', 'god', 'tvri', 'mekkah', 'madinah', 'sunnah', 'dakwah'] }, { id: 'Documentary', label: 'Dokumenter', keywords: ['discovery', 'history', 'geo', 'nature', 'animal', 'wild', 'planet', 'docu'] } ]; // Helper Convert URL const convertGithubUrl = (url) => { let newUrl = url.trim(); if (newUrl.endsWith('.git')) newUrl = newUrl.slice(0, -4); if (newUrl.includes('github.com') && !newUrl.includes('raw.githubusercontent.com')) { newUrl = newUrl.replace('github.com', 'raw.githubusercontent.com'); newUrl = newUrl.replace('/blob/', '/'); } return newUrl; }; // Parsing M3U const parseM3U = (content) => { const lines = content.split('\n'); const result = []; const uniqueGroups = new Set(); let currentChannel = {}; lines.forEach(line => { line = line.trim(); if (line.startsWith('#EXTINF:')) { const info = line.substring(8); const props = info.split(/,(.*)/s); const logoMatch = line.match(/tvg-logo="([^"]*)"/); const groupMatch = line.match(/group-title="([^"]*)"/); const groupName = groupMatch ? groupMatch[1] : 'Uncategorized'; uniqueGroups.add(groupName); currentChannel = { name: props[1] || 'Unknown Channel', logo: logoMatch ? logoMatch[1] : null, group: groupName, }; } else if (line.startsWith('http')) { if (currentChannel.name) { currentChannel.url = line; result.push(currentChannel); currentChannel = {}; } } }); const sortedCategories = Array.from(uniqueGroups).sort(); return { result, categories: sortedCategories }; }; // -- CORE LOGIC: Add Playlist -- const addNewPlaylist = (text, type, sourceName = '') => { if (!text || text.length < 10) throw new Error("Data playlist kosong atau tidak valid."); const { result, categories } = parseM3U(text); if (result.length === 0) throw new Error("Tidak ada channel ditemukan."); const newId = Date.now().toString(); let finalName = playlistNameInput.trim(); if (!finalName) { finalName = type === 'url' ? (sourceName || 'Web Playlist') : 'Local Playlist'; const count = playlists.length + 1; finalName = `${finalName} #${count}`; } const newPlaylist = { id: newId, name: finalName, channels: result, categories: categories, sourceType: type }; setPlaylists(prev => [...prev, newPlaylist]); setActivePlaylistId(newId); setShowAddPlaylist(false); setPlaylistNameInput(''); if (window.innerWidth < 640) setIsSidebarOpen(true); }; // -- CORE LOGIC: Switch Playlist -- useEffect(() => { if (activePlaylistId) { const active = playlists.find(p => p.id === activePlaylistId); if (active) { setChannels(active.channels); setCategories(active.categories); setSelectedCategory('All'); setSelectedTheme('All'); setSearchTerm(''); setFilteredChannels(active.channels); setVisibleLimit(50); if (active.channels.length > 0) { if (!selectedChannel) setSelectedChannel(active.channels[0]); } } } else { setChannels([]); setFilteredChannels([]); } }, [activePlaylistId, playlists]); const fetchPlaylist = async () => { setLoading(true); setError(null); const targetUrl = convertGithubUrl(playlistUrl); if (targetUrl !== playlistUrl) setPlaylistUrl(targetUrl); try { try { const response = await fetch(targetUrl); if (response.ok) { const text = await response.text(); addNewPlaylist(text, 'url', 'Github/Web Playlist'); setLoading(false); return; } } catch (e) { console.log("Direct fetch failed, trying proxy..."); } if (useProxy) { const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(targetUrl)}`; const responseProxy = await fetch(proxyUrl); if (!responseProxy.ok) throw new Error("Gagal mengambil playlist via Proxy."); const textProxy = await responseProxy.text(); addNewPlaylist(textProxy, 'url', 'Proxy Playlist'); } else { throw new Error("Koneksi ditolak."); } } catch (err) { setError(`Gagal: ${err.message}`); } finally { setLoading(false); } }; const loadFromText = () => { if (!rawM3U) return; setLoading(true); try { addNewPlaylist(rawM3U, 'text'); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const removePlaylist = (e, id) => { e.stopPropagation(); const newPlaylists = playlists.filter(p => p.id !== id); setPlaylists(newPlaylists); if (activePlaylistId === id) { setActivePlaylistId(newPlaylists.length > 0 ? newPlaylists[0].id : null); if (newPlaylists.length === 0) setShowAddPlaylist(true); } }; useEffect(() => { let result = channels; if (selectedCategory !== 'All') { result = result.filter(ch => ch.group === selectedCategory); } if (selectedTheme !== 'All') { const themeDef = THEMES.find(t => t.id === selectedTheme); if (themeDef) { result = result.filter(ch => { const combinedText = (ch.name + ' ' + ch.group).toLowerCase(); return themeDef.keywords.some(keyword => combinedText.includes(keyword)); }); } } if (searchTerm !== '') { const lowerTerm = searchTerm.toLowerCase(); result = result.filter(ch => ch.name.toLowerCase().includes(lowerTerm)); } setFilteredChannels(result); setVisibleLimit(50); if (listRef.current) listRef.current.scrollTop = 0; }, [searchTerm, selectedCategory, selectedTheme, channels]); const handleScroll = (e) => { const bottom = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 100; if (bottom && visibleLimit < filteredChannels.length) { setVisibleLimit(prev => Math.min(prev + 50, filteredChannels.length)); } }; const handleChannelSelect = (channel) => { setSelectedChannel(channel); setPlayerError(null); // Reset error saat ganti channel if (window.innerWidth < 640) setIsSidebarOpen(false); }; const handleFloatingInteraction = () => { setIsFloatingActive(true); if (floatingTimer.current) clearTimeout(floatingTimer.current); floatingTimer.current = setTimeout(() => setIsFloatingActive(false), 3000); }; const toggleRotation = async () => { handleFloatingInteraction(); try { if (!document.fullscreenElement) { const elem = document.documentElement; if (elem.requestFullscreen) await elem.requestFullscreen(); else if (elem.webkitRequestFullscreen) await elem.webkitRequestFullscreen(); if (window.screen && window.screen.orientation && window.screen.orientation.lock) { window.screen.orientation.lock('landscape').catch(e => console.log(e)); } } else { if (document.exitFullscreen) await document.exitFullscreen(); else if (document.webkitExitFullscreen) await document.webkitExitFullscreen(); if (window.screen && window.screen.orientation && window.screen.orientation.unlock) { window.screen.orientation.unlock(); } } } catch (err) { console.error(err); } }; const getThemeIcon = (themeId) => { switch(themeId) { case 'Sports': return ; case 'Movies': return ; case 'Music': return ; case 'News': return ; case 'Kids': return ; default: return ; } }; // -- PLAYER LOGIC UPDATED -- useEffect(() => { if (!selectedChannel || !hlsLoaded) return; // Logika URL: Gunakan HTTPS jika dipaksa, jika tidak gunakan asli let streamUrl = selectedChannel.url; if (forceHttps && streamUrl.startsWith('http:')) { streamUrl = streamUrl.replace('http:', 'https:'); } // Cleanup player lama if (hlsRef.current) hlsRef.current.destroy(); // Fungsi handle error standard const handleError = (e) => { console.error("Media Error:", e); setPlayerError("Stream gagal diputar. Coba 'Force HTTPS' atau buka di VLC."); }; if (window.Hls.isSupported()) { const hls = new window.Hls({ debug: false, enableWorker: true, lowLatencyMode: true, // Konfigurasi agresif untuk recovery manifestLoadingTimeOut: 15000, manifestLoadingMaxRetry: 3, levelLoadingTimeOut: 15000, levelLoadingMaxRetry: 3, xhrSetup: function (xhr, url) { xhr.withCredentials = false; // Hindari masalah credential CORS }, }); hlsRef.current = hls; // Load source hls.loadSource(streamUrl); hls.attachMedia(videoRef.current); hls.on(window.Hls.Events.MANIFEST_PARSED, () => { setPlayerError(null); videoRef.current.play().catch(e => console.log("Autoplay blocked:", e)); }); hls.on(window.Hls.Events.ERROR, (event, data) => { if (data.fatal) { switch (data.type) { case window.Hls.ErrorTypes.NETWORK_ERROR: console.log("Network error, trying to recover..."); hls.startLoad(); break; case window.Hls.ErrorTypes.MEDIA_ERROR: console.log("Media error, trying to recover..."); hls.recoverMediaError(); break; default: hls.destroy(); setPlayerError("Format tidak didukung atau diblokir browser."); break; } } }); } else if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { // Native Safari HLS videoRef.current.src = streamUrl; videoRef.current.addEventListener('loadedmetadata', () => { setPlayerError(null); videoRef.current.play(); }); videoRef.current.addEventListener('error', handleError); } else { // Fallback untuk MP4 dll videoRef.current.src = streamUrl; videoRef.current.play().catch(handleError); } return () => { if (hlsRef.current) hlsRef.current.destroy(); }; }, [selectedChannel, hlsLoaded, forceHttps]); return (
{/* Floating Rotation Button */}
{/* Header */}

IPTVPRO

0 ? "text-green-500" : "text-gray-600"} /> {playlists.length > 0 ? `${playlists.length} Playlist` : "No Playlist"}
{isSidebarOpen && (
setIsSidebarOpen(false)} /> )} {/* Sidebar */}