Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Clarke Player Pro • 4K IPTV Player</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Clarke Player Pro - Complete CSS */ | |
| :root { | |
| --primary: #8B5CF6; | |
| --primary-dark: #7C3AED; | |
| --primary-light: #A78BFA; | |
| --accent: #10B981; | |
| --accent-dark: #059669; | |
| --danger: #EF4444; | |
| --warning: #F59E0B; | |
| --info: #3B82F6; | |
| --bg-dark: #0F172A; | |
| --bg-card: #1E293B; | |
| --bg-panel: #334155; | |
| --bg-surface: rgba(255, 255, 255, 0.05); | |
| --text-primary: #F1F5F9; | |
| --text-secondary: #94A3B8; | |
| --text-muted: #64748B; | |
| --border: rgba(255, 255, 255, 0.1); | |
| --border-light: rgba(255, 255, 255, 0.05); | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| --gradient-primary: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| --gradient-accent: linear-gradient(135deg, var(--accent), var(--accent-dark)); | |
| --gradient-dark: linear-gradient(135deg, var(--bg-dark), #1A1F35); | |
| --glass-bg: rgba(255, 255, 255, 0.025); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg-dark); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| font-weight: 400; | |
| line-height: 1.5; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Animated Background */ | |
| .bg-animation { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| z-index: -1; | |
| overflow: hidden; | |
| } | |
| .gradient-circle { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(80px); | |
| opacity: 0.15; | |
| animation: float 20s infinite ease-in-out; | |
| } | |
| .gradient-circle:nth-child(1) { | |
| width: 600px; | |
| height: 600px; | |
| background: var(--primary); | |
| top: -300px; | |
| left: -300px; | |
| animation-delay: 0s; | |
| } | |
| .gradient-circle:nth-child(2) { | |
| width: 500px; | |
| height: 500px; | |
| background: var(--accent); | |
| bottom: -250px; | |
| right: -250px; | |
| animation-delay: 5s; | |
| } | |
| .gradient-circle:nth-child(3) { | |
| width: 400px; | |
| height: 400px; | |
| background: var(--info); | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| animation-delay: 10s; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0) scale(1); } | |
| 50% { transform: translateY(-20px) scale(1.05); } | |
| } | |
| /* Main Header */ | |
| .main-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem 2rem; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--glass-border); | |
| z-index: 100; | |
| box-shadow: var(--glass-shadow); | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 2rem; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .logo i { | |
| font-size: 2rem; | |
| color: var(--primary-light); | |
| background: var(--gradient-primary); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .logo h1 { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, var(--text-primary), var(--primary-light)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .logo h1 span { | |
| color: var(--primary); | |
| } | |
| .logo sup { | |
| font-size: 0.7rem; | |
| background: var(--gradient-accent); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-left: 0.25rem; | |
| font-weight: 600; | |
| } | |
| .player-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 0.5rem 1rem; | |
| border-radius: 20px; | |
| border: 1px solid var(--border-light); | |
| } | |
| .status-indicator { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.5; transform: scale(1.1); } | |
| 100% { opacity: 1; transform: scale(1); } | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .ui-btn { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 12px; | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| } | |
| .ui-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow); | |
| } | |
| .user-profile .avatar { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 12px; | |
| background: var(--gradient-primary); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| color: white; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .user-profile .avatar:hover { | |
| transform: scale(1.05); | |
| box-shadow: var(--shadow); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| display: grid; | |
| grid-template-columns: 320px 1fr 320px; | |
| gap: 1.5rem; | |
| padding: 1.5rem; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| @media (max-width: 1920px) { | |
| .main-content { | |
| grid-template-columns: 300px 1fr 300px; | |
| } | |
| } | |
| /* Panel Cards */ | |
| .panel-card { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 20px; | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| box-shadow: var(--glass-shadow); | |
| transition: var(--transition); | |
| } | |
| .panel-card:hover { | |
| border-color: rgba(255, 255, 255, 0.15); | |
| } | |
| .panel-card h3 { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .panel-card h3 i { | |
| color: var(--primary-light); | |
| } | |
| /* Left Panel - Playlist Input */ | |
| .input-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| .input-group label { | |
| display: block; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| } | |
| .input-with-icon { | |
| position: relative; | |
| } | |
| .input-with-icon i { | |
| position: absolute; | |
| left: 1rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| } | |
| .input-with-icon input { | |
| width: 100%; | |
| padding: 0.875rem 1rem 0.875rem 2.75rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| color: var(--text-primary); | |
| font-size: 0.875rem; | |
| transition: var(--transition); | |
| } | |
| .input-with-icon input:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| background: rgba(255, 255, 255, 0.08); | |
| box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); | |
| } | |
| .file-upload-area { | |
| border: 2px dashed var(--border); | |
| border-radius: 12px; | |
| padding: 2rem 1rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| background: rgba(255, 255, 255, 0.02); | |
| } | |
| .file-upload-area:hover { | |
| border-color: var(--primary); | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .file-upload-area i { | |
| font-size: 2rem; | |
| color: var(--text-muted); | |
| margin-bottom: 0.75rem; | |
| } | |
| .file-upload-area p { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| margin: 0; | |
| } | |
| .quick-presets { | |
| margin-bottom: 1.5rem; | |
| } | |
| .quick-presets h4 { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.75rem; | |
| } | |
| .preset-buttons { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .preset-btn { | |
| padding: 0.75rem 1rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .preset-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-color: var(--primary); | |
| color: var(--text-primary); | |
| transform: translateX(4px); | |
| } | |
| .action-buttons { | |
| display: flex; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .btn-primary, .btn-secondary { | |
| flex: 1; | |
| padding: 1rem; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .btn-primary { | |
| background: var(--gradient-primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(139, 92, 246, 0.3); | |
| } | |
| .btn-secondary { | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| } | |
| .btn-secondary:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| .playlist-info .info-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 1rem; | |
| } | |
| .info-item { | |
| text-align: center; | |
| } | |
| .info-item span { | |
| display: block; | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .info-item strong { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| } | |
| /* Center Panel - Video Player */ | |
| .center-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .video-container { | |
| position: relative; | |
| background: #000; | |
| border-radius: 20px; | |
| overflow: hidden; | |
| aspect-ratio: 16/9; | |
| box-shadow: var(--shadow-xl); | |
| } | |
| #videoPlayer { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| outline: none; | |
| } | |
| .video-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.7)); | |
| display: flex; | |
| align-items: flex-end; | |
| padding: 2rem; | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .video-container:hover .video-overlay { | |
| opacity: 1; | |
| } | |
| .now-playing { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .channel-logo-large { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 15px; | |
| background: rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| color: var(--primary-light); | |
| } | |
| .now-playing-info h2 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin-bottom: 0.25rem; | |
| } | |
| .now-playing-info p { | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| } | |
| .player-controls { | |
| position: absolute; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| align-items: center; | |
| gap: 2rem; | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(20px); | |
| padding: 1rem 2rem; | |
| border-radius: 50px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .video-container:hover .player-controls { | |
| opacity: 1; | |
| } | |
| .control-group { | |
| display: flex; | |
| gap: 1rem; | |
| } | |
| .control-btn { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| color: white; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| } | |
| .control-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: scale(1.1); | |
| } | |
| .control-btn.play-btn { | |
| background: var(--gradient-primary); | |
| border: none; | |
| } | |
| .control-btn.play-btn:hover { | |
| background: var(--primary-dark); | |
| box-shadow: 0 0 20px rgba(139, 92, 246, 0.5); | |
| } | |
| .volume-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .volume-control i { | |
| color: var(--text-secondary); | |
| } | |
| .volume-control input[type="range"] { | |
| width: 100px; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 2px; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| .volume-control input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| cursor: pointer; | |
| border: 2px solid white; | |
| } | |
| .loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1rem; | |
| } | |
| .loading-overlay .spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-top-color: var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* EPG Timeline */ | |
| .epg-timeline { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 15px; | |
| padding: 1.25rem; | |
| } | |
| .timeline-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .timeline-header h4 { | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .timeline-nav { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .nav-btn { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 8px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| } | |
| .nav-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| .timeline-content { | |
| display: flex; | |
| gap: 1rem; | |
| overflow-x: auto; | |
| padding-bottom: 0.5rem; | |
| } | |
| .timeline-content::-webkit-scrollbar { | |
| height: 4px; | |
| } | |
| .timeline-content::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 2px; | |
| } | |
| .timeline-content::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 2px; | |
| } | |
| .epg-item { | |
| flex-shrink: 0; | |
| padding: 0.75rem 1rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 10px; | |
| border: 1px solid transparent; | |
| transition: var(--transition); | |
| min-width: 180px; | |
| } | |
| .epg-item.active { | |
| background: var(--gradient-primary); | |
| border-color: var(--primary); | |
| } | |
| .epg-time { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .epg-item.active .epg-time { | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .epg-show { | |
| font-size: 0.875rem; | |
| color: var(--text-primary); | |
| } | |
| .epg-item.active .epg-show { | |
| color: white; | |
| font-weight: 500; | |
| } | |
| /* Channels Grid */ | |
| .channels-grid { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .grid-header { | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .grid-header h3 { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .grid-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .search-box { | |
| display: flex; | |
| align-items: center; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 0.5rem 0.75rem; | |
| gap: 0.5rem; | |
| } | |
| .search-box i { | |
| color: var(--text-muted); | |
| font-size: 0.875rem; | |
| } | |
| .search-box input { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-primary); | |
| font-size: 0.875rem; | |
| width: 200px; | |
| outline: none; | |
| } | |
| .search-box input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .view-toggle { | |
| display: flex; | |
| gap: 0.25rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 0.25rem; | |
| } | |
| .view-btn { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 8px; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| } | |
| .view-btn.active { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| .view-btn:hover:not(.active) { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .channels-container { | |
| flex: 1; | |
| padding: 1.5rem; | |
| overflow-y: auto; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| align-content: start; | |
| } | |
| .channels-container::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .channels-container::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 4px; | |
| } | |
| .channels-container::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 4px; | |
| } | |
| .channels-container::-webkit-scrollbar-thumb:hover { | |
| background: var(--primary-dark); | |
| } | |
| .channel-card { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border); | |
| border-radius: 15px; | |
| padding: 1.25rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .channel-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 2px; | |
| background: var(--gradient-primary); | |
| transform: scaleX(0); | |
| transition: var(--transition); | |
| } | |
| .channel-card:hover { | |
| background: rgba(255, 255, 255, 0.06); | |
| border-color: var(--primary); | |
| transform: translateY(-4px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .channel-card:hover::before { | |
| transform: scaleX(1); | |
| } | |
| .channel-card.active { | |
| background: rgba(139, 92, 246, 0.1); | |
| border-color: var(--primary); | |
| } | |
| .channel-card.active::before { | |
| transform: scaleX(1); | |
| } | |
| .channel-logo { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| background: rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 1rem; | |
| font-size: 1.25rem; | |
| color: var(--primary-light); | |
| } | |
| .channel-info h4 { | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.25rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .channel-meta { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| .channel-number { | |
| font-weight: 600; | |
| color: var(--primary-light); | |
| } | |
| .channel-fav { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .channel-fav:hover { | |
| color: #FFD700; | |
| transform: scale(1.2); | |
| } | |
| .channel-fav.active { | |
| color: #FFD700; | |
| } | |
| .welcome-state { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 3rem; | |
| text-align: center; | |
| color: var(--text-secondary); | |
| } | |
| .welcome-state i { | |
| font-size: 4rem; | |
| margin-bottom: 1.5rem; | |
| color: var(--primary-light); | |
| opacity: 0.5; | |
| } | |
| .welcome-state h3 { | |
| font-size: 1.5rem; | |
| color: var(--text-primary); | |
| margin-bottom: 0.5rem; | |
| } | |
| /* Right Panel */ | |
| .current-channel-info { | |
| margin-bottom: 1.5rem; | |
| } | |
| .channel-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .channel-logo { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| background: rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.25rem; | |
| color: var(--primary-light); | |
| } | |
| .channel-details { | |
| flex: 1; | |
| } | |
| .channel-details h4 { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .channel-region { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 6px; | |
| display: inline-block; | |
| } | |
| .fav-btn { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 10px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| } | |
| .fav-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #FFD700; | |
| } | |
| .fav-btn.active { | |
| color: #FFD700; | |
| border-color: rgba(255, 215, 0, 0.3); | |
| } | |
| .quality-indicator { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0.75rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| } | |
| .quality-badge { | |
| padding: 0.25rem 0.75rem; | |
| background: var(--gradient-accent); | |
| color: white; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| border-radius: 20px; | |
| } | |
| .resolution { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| .program-info { | |
| margin-bottom: 1.5rem; | |
| } | |
| .program-info h4 { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.75rem; | |
| } | |
| .program-details p { | |
| font-size: 0.875rem; | |
| color: var(--text-primary); | |
| margin-bottom: 0.75rem; | |
| line-height: 1.5; | |
| } | |
| .program-time { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .time-badge { | |
| padding: 0.25rem 0.5rem; | |
| background: var(--danger); | |
| color: white; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| border-radius: 6px; | |
| } | |
| .duration { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| .audio-tracks h4 { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.75rem; | |
| } | |
| .track-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .track-item { | |
| padding: 0.75rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .track-item:hover { | |
| background: rgba(255, 255, 255, 0.06); | |
| border-color: var(--primary); | |
| } | |
| .track-item.active { | |
| background: rgba(139, 92, 246, 0.1); | |
| border-color: var(--primary); | |
| color: var(--text-primary); | |
| } | |
| .filter-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .filter-item label { | |
| display: block; | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| } | |
| .filter-item select { | |
| width: 100%; | |
| padding: 0.75rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| color: var(--text-primary); | |
| font-size: 0.875rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .filter-item select:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| background: rgba(255, 255, 255, 0.08); | |
| } | |
| .quality-filter { | |
| display: flex; | |
| gap: 1rem; | |
| } | |
| .checkbox-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .checkbox-label input[type="checkbox"] { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 4px; | |
| border: 1px solid var(--border); | |
| background: rgba(255, 255, 255, 0.05); | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .checkbox-label input[type="checkbox"]:checked { | |
| background: var(--primary); | |
| border-color: var(--primary); | |
| } | |
| .checkbox-label:hover { | |
| color: var(--text-primary); | |
| } | |
| .sort-buttons { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .sort-btn { | |
| flex: 1; | |
| padding: 0.5rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| font-size: 0.75rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .sort-btn.active { | |
| background: rgba(139, 92, 246, 0.1); | |
| border-color: var(--primary); | |
| color: var(--text-primary); | |
| } | |
| .sort-btn:hover:not(.active) { | |
| background: rgba(255, 255, 255, 0.08); | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 1rem; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| padding: 1rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| } | |
| .stat-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 10px; | |
| background: rgba(139, 92, 246, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin: 0 auto 0.75rem; | |
| color: var(--primary-light); | |
| font-size: 1.25rem; | |
| } | |
| .stat-info span { | |
| display: block; | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .stat-info strong { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| } | |
| /* Bottom Bar */ | |
| .bottom-bar { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-top: 1px solid var(--glass-border); | |
| padding: 0.75rem 2rem; | |
| box-shadow: var(--glass-shadow); | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 3px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 2px; | |
| margin-bottom: 0.75rem; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--gradient-primary); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .footer-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .playback-info { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.25rem; | |
| } | |
| .separator { | |
| color: var(--text-muted); | |
| } | |
| .shortcuts-info { | |
| display: flex; | |
| gap: 1.5rem; | |
| } | |
| .shortcut-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .shortcut-item kbd { | |
| padding: 0.25rem 0.5rem; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| font-family: 'Inter', monospace; | |
| font-size: 0.75rem; | |
| color: var(--text-primary); | |
| min-width: 36px; | |
| text-align: center; | |
| } | |
| .shortcut-item span { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| .version-info { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| opacity: 0.7; | |
| } | |
| /* Favorites Section */ | |
| .favorites-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .favorites-list::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .favorites-list::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .favorites-list::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 2px; | |
| } | |
| .favorite-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| padding: 0.75rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .favorite-item:hover { | |
| background: rgba(255, 255, 255, 0.06); | |
| border-color: var(--primary); | |
| } | |
| .favorite-item i { | |
| color: #FFD700; | |
| font-size: 0.875rem; | |
| } | |
| .favorite-item span { | |
| font-size: 0.875rem; | |
| color: var(--text-primary); | |
| flex: 1; | |
| } | |
| .empty-favorites { | |
| text-align: center; | |
| padding: 2rem 1rem; | |
| color: var(--text-secondary); | |
| } | |
| .empty-favorites i { | |
| font-size: 2rem; | |
| margin-bottom: 0.75rem; | |
| opacity: 0.5; | |
| } | |
| .empty-favorites p { | |
| font-size: 0.875rem; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1600px) { | |
| .main-content { | |
| grid-template-columns: 280px 1fr 280px; | |
| } | |
| } | |
| @media (max-width: 1366px) { | |
| .main-content { | |
| grid-template-columns: 260px 1fr 260px; | |
| } | |
| } | |
| @media (max-width: 1200px) { | |
| .main-content { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto 1fr auto; | |
| } | |
| .left-panel, .right-panel { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Animated Background --> | |
| <div class="bg-animation"> | |
| <div class="gradient-circle"></div> | |
| <div class="gradient-circle"></div> | |
| <div class="gradient-circle"></div> | |
| </div> | |
| <!-- Main Header --> | |
| <header class="main-header"> | |
| <div class="header-left"> | |
| <div class="logo"> | |
| <i class="fas fa-broadcast-tower"></i> | |
| <h1>Clarke<span>Player</span><sup>PRO</sup></h1> | |
| </div> | |
| <div class="player-status"> | |
| <div class="status-indicator"></div> | |
| <span id="statusText">Ready</span> | |
| </div> | |
| </div> | |
| <div class="header-right"> | |
| <button class="ui-btn" id="settingsBtn" title="Settings"> | |
| <i class="fas fa-sliders-h"></i> | |
| </button> | |
| <button class="ui-btn" id="fullscreenBtn" title="Fullscreen"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| <div class="user-profile"> | |
| <div class="avatar">CP</div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Left Panel - Playlist Input & Controls --> | |
| <aside class="left-panel"> | |
| <div class="panel-card"> | |
| <h3><i class="fas fa-cloud-upload-alt"></i> Load Playlist</h3> | |
| <div class="input-group"> | |
| <label>Playlist URL</label> | |
| <div class="input-with-icon"> | |
| <i class="fas fa-link"></i> | |
| <input type="text" id="playlistUrl" | |
| placeholder="https://example.com/playlist.m3u8" | |
| value="https://i.mjh.nz/PlutoTV/us.m3u8"> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label>Or upload file</label> | |
| <div class="file-upload-area" id="uploadArea"> | |
| <i class="fas fa-file-upload"></i> | |
| <p>Drop .m3u/.m3u8 file here or click to browse</p> | |
| <input type="file" id="fileInput" accept=".m3u,.m3u8" hidden> | |
| </div> | |
| </div> | |
| <div class="quick-presets"> | |
| <h4>Quick Presets</h4> | |
| <div class="preset-buttons"> | |
| <button class="preset-btn" data-url="https://i.mjh.nz/PlutoTV/us.m3u8"> | |
| <i class="fas fa-flag-usa"></i> PlutoTV US | |
| </button> | |
| <button class="preset-btn" data-url="https://i.mjh.nz/PlutoTV/gb.m3u8"> | |
| <i class="fas fa-flag-uk"></i> PlutoTV UK | |
| </button> | |
| <button class="preset-btn" data-url="https://iptv-org.github.io/iptv/index.m3u"> | |
| <i class="fas fa-globe"></i> Global IPTV | |
| </button> | |
| </div> | |
| </div> | |
| <div class="action-buttons"> | |
| <button class="btn-primary" id="loadPlaylistBtn"> | |
| <i class="fas fa-play-circle"></i> Load & Play | |
| </button> | |
| <button class="btn-secondary" id="clearBtn"> | |
| <i class="fas fa-trash"></i> Clear | |
| </button> | |
| </div> | |
| <div class="playlist-info" id="playlistInfo"> | |
| <h4>Playlist Info</h4> | |
| <div class="info-grid"> | |
| <div class="info-item"> | |
| <span>Channels</span> | |
| <strong id="channelCount">0</strong> | |
| </div> | |
| <div class="info-item"> | |
| <span>Last Updated</span> | |
| <strong id="lastUpdate">--</strong> | |
| </div> | |
| <div class="info-item"> | |
| <span>Quality</span> | |
| <strong id="avgQuality">Auto</strong> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Favorites Section --> | |
| <div class="panel-card favorites-section"> | |
| <h3><i class="fas fa-star"></i> Favorites</h3> | |
| <div class="favorites-list" id="favoritesList"> | |
| <div class="empty-favorites"> | |
| <i class="fas fa-star"></i> | |
| <p>No favorites yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Center Panel - Video Player --> | |
| <main class="center-panel"> | |
| <div class="video-container" id="videoContainer"> | |
| <div class="video-overlay"> | |
| <div class="now-playing"> | |
| <div class="channel-logo-large"> | |
| <i class="fas fa-satellite"></i> | |
| </div> | |
| <div class="now-playing-info"> | |
| <h2 id="currentChannel">Welcome to Clarke Player Pro</h2> | |
| <p id="currentProgram">Load a playlist to start streaming</p> | |
| </div> | |
| </div> | |
| </div> | |
| <video id="videoPlayer" controls playsinline></video> | |
| <div class="player-controls"> | |
| <div class="control-group"> | |
| <button class="control-btn" id="prevChannel"> | |
| <i class="fas fa-step-backward"></i> | |
| </button> | |
| <button class="control-btn play-btn" id="playPauseBtn"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <button class="control-btn" id="nextChannel"> | |
| <i class="fas fa-step-forward"></i> | |
| </button> | |
| </div> | |
| <div class="volume-control"> | |
| <i class="fas fa-volume-up"></i> | |
| <input type="range" id="volumeSlider" min="0" max="100" value="80"> | |
| </div> | |
| </div> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="spinner"></div> | |
| <p>Loading stream...</p> | |
| </div> | |
| </div> | |
| <!-- EPG Timeline --> | |
| <div class="epg-timeline"> | |
| <div class="timeline-header"> | |
| <h4><i class="fas fa-tv"></i> What's On Now</h4> | |
| <div class="timeline-nav"> | |
| <button class="nav-btn"><i class="fas fa-chevron-left"></i></button> | |
| <button class="nav-btn"><i class="fas fa-chevron-right"></i></button> | |
| </div> | |
| </div> | |
| <div class="timeline-content" id="epgTimeline"> | |
| <div class="epg-item active"> | |
| <div class="epg-time">NOW</div> | |
| <div class="epg-show">Select a channel</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Channel Grid --> | |
| <div class="channels-grid"> | |
| <div class="grid-header"> | |
| <h3><i class="fas fa-th-large"></i> Channels</h3> | |
| <div class="grid-controls"> | |
| <div class="search-box"> | |
| <i class="fas fa-search"></i> | |
| <input type="text" id="channelSearch" placeholder="Search channels..."> | |
| </div> | |
| <div class="view-toggle"> | |
| <button class="view-btn active"><i class="fas fa-th-large"></i></button> | |
| <button class="view-btn"><i class="fas fa-list"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="channels-container" id="channelsContainer"> | |
| <!-- Channels will be loaded here --> | |
| <div class="welcome-state"> | |
| <i class="fas fa-broadcast-tower"></i> | |
| <h3>Load Your Playlist</h3> | |
| <p>Enter a playlist URL above or upload a file to get started</p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Right Panel - Channel Guide --> | |
| <aside class="right-panel"> | |
| <div class="panel-card"> | |
| <h3><i class="fas fa-tv"></i> Now Playing</h3> | |
| <div class="current-channel-info"> | |
| <div class="channel-display"> | |
| <div class="channel-logo"> | |
| <i class="fas fa-satellite"></i> | |
| </div> | |
| <div class="channel-details"> | |
| <h4 id="nowPlayingChannel">--</h4> | |
| <p class="channel-region" id="nowPlayingRegion">--</p> | |
| </div> | |
| <button class="fav-btn" id="toggleFav"> | |
| <i class="far fa-star"></i> | |
| </button> | |
| </div> | |
| <div class="quality-indicator"> | |
| <div class="quality-badge" id="qualityBadge">Auto</div> | |
| <div class="resolution">4K Ready</div> | |
| </div> | |
| </div> | |
| <div class="program-info"> | |
| <h4>Program Information</h4> | |
| <div class="program-details"> | |
| <p id="programDescription">No program information available</p> | |
| <div class="program-time"> | |
| <span class="time-badge">LIVE</span> | |
| <span class="duration">00:00 - 00:00</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="audio-tracks"> | |
| <h4>Audio Tracks</h4> | |
| <div class="track-list" id="audioTracks"> | |
| <div class="track-item active"> | |
| <i class="fas fa-volume-up"></i> | |
| <span>English</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Filter Section --> | |
| <div class="panel-card"> | |
| <h3><i class="fas fa-filter"></i> Filter Channels</h3> | |
| <div class="filter-group"> | |
| <div class="filter-item"> | |
| <label>Category</label> | |
| <select id="categoryFilter"> | |
| <option value="all">All Categories</option> | |
| <option value="entertainment">Entertainment</option> | |
| <option value="news">News</option> | |
| <option value="sports">Sports</option> | |
| <option value="movies">Movies</option> | |
| </select> | |
| </div> | |
| <div class="filter-item"> | |
| <label>Quality</label> | |
| <div class="quality-filter"> | |
| <label class="checkbox-label"> | |
| <input type="checkbox" checked> 4K | |
| </label> | |
| <label class="checkbox-label"> | |
| <input type="checkbox" checked> HD | |
| </label> | |
| <label class="checkbox-label"> | |
| <input type="checkbox" checked> SD | |
| </label> | |
| </div> | |
| </div> | |
| <div class="filter-item"> | |
| <label>Sort By</label> | |
| <div class="sort-buttons"> | |
| <button class="sort-btn active">Name A-Z</button> | |
| <button class="sort-btn">Favorites</button> | |
| <button class="sort-btn">Recent</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats Section --> | |
| <div class="panel-card stats-card"> | |
| <h3><i class="fas fa-chart-line"></i> Player Stats</h3> | |
| <div class="stats-grid"> | |
| <div class="stat-item"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-wifi"></i> | |
| </div> | |
| <div class="stat-info"> | |
| <span>Bitrate</span> | |
| <strong id="bitrateStat">0 Mbps</strong> | |
| </div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-tachometer-alt"></i> | |
| </div> | |
| <div class="stat-info"> | |
| <span>Buffer</span> | |
| <strong id="bufferStat">0s</strong> | |
| </div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-heartbeat"></i> | |
| </div> | |
| <div class="stat-info"> | |
| <span>Health</span> | |
| <strong id="healthStat">100%</strong> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Bottom Bar --> | |
| <footer class="bottom-bar"> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="bufferProgress"></div> | |
| </div> | |
| <div class="footer-content"> | |
| <div class="playback-info"> | |
| <span id="currentTime">00:00</span> | |
| <span class="separator">/</span> | |
| <span id="totalTime">00:00</span> | |
| </div> | |
| <div class="shortcuts-info"> | |
| <div class="shortcut-item"> | |
| <kbd>SPACE</kbd> | |
| <span>Play/Pause</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <kbd>F</kbd> | |
| <span>Fullscreen</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <kbd>M</kbd> | |
| <span>Mute</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <kbd>← →</kbd> | |
| <span>Navigate</span> | |
| </div> | |
| </div> | |
| <div class="version-info"> | |
| Clarke Player Pro v2.1 • 4K Optimized | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <!-- Scripts --> | |
| <script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.10/dist/hls.min.js"></script> | |
| <script> | |
| // Clarke Player Pro - Complete JavaScript | |
| class ClarkePlayerPro { | |
| constructor() { | |
| this.state = { | |
| playlists: [], | |
| currentPlaylist: null, | |
| channels: [], | |
| filteredChannels: [], | |
| favorites: JSON.parse(localStorage.getItem('clarke_favs')) || {}, | |
| currentChannel: null, | |
| hls: null, | |
| isPlaying: false, | |
| quality: 'Auto', | |
| searchQuery: '', | |
| filterCategory: 'all' | |
| }; | |
| this.elements = { | |
| video: document.getElementById('videoPlayer'), | |
| playlistUrl: document.getElementById('playlistUrl'), | |
| fileInput: document.getElementById('fileInput'), | |
| uploadArea: document.getElementById('uploadArea'), | |
| loadPlaylistBtn: document.getElementById('loadPlaylistBtn'), | |
| clearBtn: document.getElementById('clearBtn'), | |
| channelsContainer: document.getElementById('channelsContainer'), | |
| currentChannel: document.getElementById('currentChannel'), | |
| currentProgram: document.getElementById('currentProgram'), | |
| nowPlayingChannel: document.getElementById('nowPlayingChannel'), | |
| nowPlayingRegion: document.getElementById('nowPlayingRegion'), | |
| statusText: document.getElementById('statusText'), | |
| channelCount: document.getElementById('channelCount'), | |
| lastUpdate: document.getElementById('lastUpdate'), | |
| avgQuality: document.getElementById('avgQuality'), | |
| loadingOverlay: document.getElementById('loadingOverlay'), | |
| channelSearch: document.getElementById('channelSearch'), | |
| toggleFav: document.getElementById('toggleFav'), | |
| playPauseBtn: document.getElementById('playPauseBtn'), | |
| prevChannel: document.getElementById('prevChannel'), | |
| nextChannel: document.getElementById('nextChannel'), | |
| volumeSlider: document.getElementById('volumeSlider'), | |
| favoritesList: document.getElementById('favoritesList'), | |
| categoryFilter: document.getElementById('categoryFilter'), | |
| fullscreenBtn: document.getElementById('fullscreenBtn'), | |
| settingsBtn: document.getElementById('settingsBtn'), | |
| bitrateStat: document.getElementById('bitrateStat'), | |
| bufferStat: document.getElementById('bufferStat'), | |
| healthStat: document.getElementById('healthStat'), | |
| qualityBadge: document.getElementById('qualityBadge'), | |
| bufferProgress: document.getElementById('bufferProgress'), | |
| currentTime: document.getElementById('currentTime'), | |
| totalTime: document.getElementById('totalTime') | |
| }; | |
| this.init(); | |
| } | |
| init() { | |
| console.log('🚀 Clarke Player Pro v2.1 Initializing...'); | |
| this.setupEventListeners(); | |
| this.setupKeyboardControls(); | |
| this.loadDefaultPlaylist(); | |
| this.updateStats(); | |
| this.renderFavorites(); | |
| // Set initial volume | |
| this.elements.video.volume = this.elements.volumeSlider.value / 100; | |
| } | |
| setupEventListeners() { | |
| // Load playlist from URL | |
| this.elements.loadPlaylistBtn.addEventListener('click', () => { | |
| this.loadPlaylistFromUrl(); | |
| }); | |
| // Quick preset buttons | |
| document.querySelectorAll('.preset-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const url = e.currentTarget.dataset.url; | |
| this.elements.playlistUrl.value = url; | |
| this.loadPlaylistFromUrl(); | |
| }); | |
| }); | |
| // File upload | |
| this.elements.uploadArea.addEventListener('click', () => { | |
| this.elements.fileInput.click(); | |
| }); | |
| this.elements.fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| this.loadPlaylistFromFile(e.target.files[0]); | |
| } | |
| }); | |
| // Drag and drop for file upload | |
| this.elements.uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| e.currentTarget.style.borderColor = 'var(--primary)'; | |
| e.currentTarget.style.background = 'rgba(139, 92, 246, 0.1)'; | |
| }); | |
| this.elements.uploadArea.addEventListener('dragleave', (e) => { | |
| e.currentTarget.style.borderColor = 'var(--border)'; | |
| e.currentTarget.style.background = 'rgba(255, 255, 255, 0.02)'; | |
| }); | |
| this.elements.uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| e.currentTarget.style.borderColor = 'var(--border)'; | |
| e.currentTarget.style.background = 'rgba(255, 255, 255, 0.02)'; | |
| const file = e.dataTransfer.files[0]; | |
| if (file && (file.name.endsWith('.m3u') || file.name.endsWith('.m3u8'))) { | |
| this.loadPlaylistFromFile(file); | |
| } else { | |
| this.showNotification('Please drop a valid .m3u or .m3u8 file', 'error'); | |
| } | |
| }); | |
| // Clear button | |
| this.elements.clearBtn.addEventListener('click', () => { | |
| this.clearPlaylist(); | |
| }); | |
| // Search | |
| this.elements.channelSearch.addEventListener('input', (e) => { | |
| this.state.searchQuery = e.target.value.toLowerCase(); | |
| this.filterChannels(); | |
| }); | |
| // Category filter | |
| this.elements.categoryFilter.addEventListener('change', (e) => { | |
| this.state.filterCategory = e.target.value; | |
| this.filterChannels(); | |
| }); | |
| // Player controls | |
| this.elements.playPauseBtn.addEventListener('click', () => { | |
| this.togglePlayPause(); | |
| }); | |
| this.elements.prevChannel.addEventListener('click', () => { | |
| this.selectPreviousChannel(); | |
| }); | |
| this.elements.nextChannel.addEventListener('click', () => { | |
| this.selectNextChannel(); | |
| }); | |
| this.elements.toggleFav.addEventListener('click', () => { | |
| if (this.state.currentChannel) { | |
| this.toggleFavorite(this.state.currentChannel.id); | |
| } | |
| }); | |
| // Volume control | |
| this.elements.volumeSlider.addEventListener('input', (e) => { | |
| this.elements.video.volume = e.target.value / 100; | |
| }); | |
| // Fullscreen | |
| this.elements.fullscreenBtn.addEventListener('click', () => { | |
| this.toggleFullscreen(); | |
| }); | |
| // Video events | |
| this.elements.video.addEventListener('play', () => { | |
| this.state.isPlaying = true; | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| this.updateStatus('Playing'); | |
| }); | |
| this.elements.video.addEventListener('pause', () => { | |
| this.state.isPlaying = false; | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| this.updateStatus('Paused'); | |
| }); | |
| this.elements.video.addEventListener('waiting', () => { | |
| this.showLoading(true); | |
| this.updateStatus('Buffering...'); | |
| }); | |
| this.elements.video.addEventListener('playing', () => { | |
| this.showLoading(false); | |
| this.updateStatus('Playing'); | |
| }); | |
| this.elements.video.addEventListener('timeupdate', () => { | |
| this.updatePlaybackInfo(); | |
| }); | |
| this.elements.video.addEventListener('loadedmetadata', () => { | |
| this.updatePlaybackInfo(); | |
| }); | |
| // Auto-hide controls | |
| let controlsTimeout; | |
| this.elements.video.addEventListener('mousemove', () => { | |
| const overlay = document.querySelector('.video-overlay'); | |
| const controls = document.querySelector('.player-controls'); | |
| overlay.style.opacity = '1'; | |
| controls.style.opacity = '1'; | |
| clearTimeout(controlsTimeout); | |
| controlsTimeout = setTimeout(() => { | |
| if (!document.fullscreenElement) { | |
| overlay.style.opacity = '0'; | |
| controls.style.opacity = '0'; | |
| } | |
| }, 3000); | |
| }); | |
| } | |
| setupKeyboardControls() { | |
| document.addEventListener('keydown', (e) => { | |
| if (e.target.tagName === 'INPUT') return; | |
| switch (e.key.toLowerCase()) { | |
| case ' ': | |
| e.preventDefault(); | |
| this.togglePlayPause(); | |
| break; | |
| case 'arrowleft': | |
| e.preventDefault(); | |
| this.seek(-10); | |
| break; | |
| case 'arrowright': | |
| e.preventDefault(); | |
| this.seek(10); | |
| break; | |
| case 'arrowup': | |
| e.preventDefault(); | |
| this.selectPreviousChannel(); | |
| break; | |
| case 'arrowdown': | |
| e.preventDefault(); | |
| this.selectNextChannel(); | |
| break; | |
| case 'f': | |
| e.preventDefault(); | |
| this.toggleFullscreen(); | |
| break; | |
| case 'm': | |
| e.preventDefault(); | |
| this.toggleMute(); | |
| break; | |
| case 'l': | |
| e.preventDefault(); | |
| this.loadPlaylistFromUrl(); | |
| break; | |
| case 'escape': | |
| if (document.fullscreenElement) { | |
| document.exitFullscreen(); | |
| } | |
| break; | |
| } | |
| }); | |
| } | |
| async loadDefaultPlaylist() { | |
| const defaultUrl = this.elements.playlistUrl.value; | |
| if (defaultUrl) { | |
| await this.loadPlaylistFromUrl(); | |
| } | |
| } | |
| async loadPlaylistFromUrl() { | |
| const url = this.elements.playlistUrl.value.trim(); | |
| if (!url) { | |
| this.showNotification('Please enter a playlist URL', 'error'); | |
| return; | |
| } | |
| this.updateStatus('Loading playlist...'); | |
| this.showLoading(true); | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| const text = await response.text(); | |
| await this.parsePlaylist(text, url); | |
| this.showNotification('Playlist loaded successfully', 'success'); | |
| this.elements.lastUpdate.textContent = new Date().toLocaleTimeString(); | |
| } catch (error) { | |
| console.error('Failed to load playlist:', error); | |
| this.showNotification('Failed to load playlist. Please check the URL.', 'error'); | |
| this.updateStatus('Load failed'); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async loadPlaylistFromFile(file) { | |
| this.updateStatus('Reading playlist file...'); | |
| this.showLoading(true); | |
| try { | |
| const text = await file.text(); | |
| await this.parsePlaylist(text, file.name); | |
| this.showNotification(`Loaded playlist: ${file.name}`, 'success'); | |
| this.elements.lastUpdate.textContent = new Date().toLocaleTimeString(); | |
| } catch (error) { | |
| console.error('Failed to load file:', error); | |
| this.showNotification('Failed to load playlist file', 'error'); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async parsePlaylist(content, source) { | |
| const channels = []; | |
| const lines = content.split('\n'); | |
| let currentChannel = {}; | |
| let channelNumber = 1; | |
| for (let line of lines) { | |
| line = line.trim(); | |
| if (line.startsWith('#EXTINF')) { | |
| const titleMatch = line.match(/,(.*)$/); | |
| const logoMatch = line.match(/tvg-logo="([^"]+)"/); | |
| const groupMatch = line.match(/group-title="([^"]+)"/); | |
| currentChannel = { | |
| id: `channel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, | |
| title: titleMatch ? this.cleanTitle(titleMatch[1]) : `Channel ${channelNumber}`, | |
| logo: logoMatch ? logoMatch[1] : '', | |
| group: groupMatch ? groupMatch[1] : 'General', | |
| number: channelNumber, | |
| source: source, | |
| url: '', | |
| isFavorite: false, | |
| category: this.detectCategory(titleMatch ? titleMatch[1] : '') | |
| }; | |
| channelNumber++; | |
| } | |
| else if (line.startsWith('http')) { | |
| currentChannel.url = line; | |
| channels.push({...currentChannel}); | |
| } | |
| } | |
| this.state.channels = channels; | |
| this.state.filteredChannels = [...channels]; | |
| this.updateChannelCount(); | |
| this.renderChannels(); | |
| if (channels.length > 0) { | |
| this.selectChannel(0); | |
| } | |
| } | |
| cleanTitle(title) { | |
| return title | |
| .replace(/^Pluto TV\s*/i, '') | |
| .replace(/^\[.*?\]\s*/, '') | |
| .replace(/\|.*$/, '') | |
| .trim(); | |
| } | |
| detectCategory(title) { | |
| const lowerTitle = title.toLowerCase(); | |
| if (lowerTitle.includes('news') || lowerTitle.includes('cnn') || lowerTitle.includes('bbc')) { | |
| return 'news'; | |
| } else if (lowerTitle.includes('sport') || lowerTitle.includes('espn') || lowerTitle.includes('football')) { | |
| return 'sports'; | |
| } else if (lowerTitle.includes('movie') || lowerTitle.includes('cinema') || lowerTitle.includes('film')) { | |
| return 'movies'; | |
| } else if (lowerTitle.includes('music') || lowerTitle.includes('mtv') || lowerTitle.includes('vibe')) { | |
| return 'music'; | |
| } else if (lowerTitle.includes('kids') || lowerTitle.includes('cartoon') || lowerTitle.includes('disney')) { | |
| return 'kids'; | |
| } else { | |
| return 'entertainment'; | |
| } | |
| } | |
| renderChannels() { | |
| const container = this.elements.channelsContainer; | |
| if (this.state.filteredChannels.length === 0) { | |
| container.innerHTML = ` | |
| <div class="welcome-state"> | |
| <i class="fas fa-broadcast-tower"></i> | |
| <h3>No Channels Found</h3> | |
| <p>Try a different search term or filter</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| let html = ''; | |
| this.state.filteredChannels.forEach((channel, index) => { | |
| const isActive = this.state.currentChannel && | |
| this.state.currentChannel.id === channel.id; | |
| const isFavorite = this.state.favorites[channel.id]; | |
| html += ` | |
| <div class="channel-card ${isActive ? 'active' : ''}" | |
| data-index="${index}" | |
| data-id="${channel.id}"> | |
| <div class="channel-logo"> | |
| ${channel.logo ? | |
| `<img src="${channel.logo}" alt="${channel.title}" | |
| onerror="this.style.display='none'; this.parentElement.innerHTML='<i class=\"fas fa-tv\"></i>'">` : | |
| `<i class="fas fa-tv"></i>`} | |
| </div> | |
| <div class="channel-info"> | |
| <h4>${channel.title}</h4> | |
| <div class="channel-meta"> | |
| <span class="channel-number">${channel.number}</span> | |
| <span class="channel-category">${channel.category}</span> | |
| </div> | |
| </div> | |
| <div class="channel-fav ${isFavorite ? 'active' : ''}" | |
| data-id="${channel.id}"> | |
| <i class="fas fa-star"></i> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| container.innerHTML = html; | |
| // Add event listeners | |
| container.querySelectorAll('.channel-card').forEach(card => { | |
| card.addEventListener('click', (e) => { | |
| if (!e.target.closest('.channel-fav')) { | |
| const index = parseInt(card.dataset.index); | |
| this.selectChannel(index); | |
| } | |
| }); | |
| }); | |
| container.querySelectorAll('.channel-fav').forEach(fav => { | |
| fav.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const channelId = fav.dataset.id; | |
| this.toggleFavorite(channelId); | |
| fav.classList.toggle('active'); | |
| }); | |
| }); | |
| } | |
| filterChannels() { | |
| let filtered = this.state.channels; | |
| // Apply search filter | |
| if (this.state.searchQuery) { | |
| filtered = filtered.filter(ch => | |
| ch.title.toLowerCase().includes(this.state.searchQuery) || | |
| ch.group.toLowerCase().includes(this.state.searchQuery) || | |
| ch.category.toLowerCase().includes(this.state.searchQuery) | |
| ); | |
| } | |
| // Apply category filter | |
| if (this.state.filterCategory !== 'all') { | |
| filtered = filtered.filter(ch => | |
| ch.category === this.state.filterCategory | |
| ); | |
| } | |
| this.state.filteredChannels = filtered; | |
| this.renderChannels(); | |
| this.updateChannelCount(); | |
| } | |
| selectChannel(index) { | |
| if (index < 0 || index >= this.state.filteredChannels.length) return; | |
| const channel = this.state.filteredChannels[index]; | |
| this.state.currentChannel = channel; | |
| // Update UI | |
| this.elements.currentChannel.textContent = channel.title; | |
| this.elements.currentProgram.textContent = `Now Playing • ${channel.group}`; | |
| this.elements.nowPlayingChannel.textContent = channel.title; | |
| this.elements.nowPlayingRegion.textContent = channel.group; | |
| // Update favorite button | |
| this.elements.toggleFav.classList.toggle('active', this.state.favorites[channel.id]); | |
| // Update active state in list | |
| document.querySelectorAll('.channel-card').forEach(card => { | |
| card.classList.remove('active'); | |
| }); | |
| const selectedCard = document.querySelector(`.channel-card[data-index="${index}"]`); | |
| if (selectedCard) { | |
| selectedCard.classList.add('active'); | |
| selectedCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| // Play the channel | |
| this.playChannel(channel.url); | |
| this.updateStatus(`Playing: ${channel.title}`); | |
| } | |
| selectNextChannel() { | |
| if (!this.state.currentChannel || this.state.filteredChannels.length === 0) return; | |
| const currentIndex = this.state.filteredChannels.findIndex( | |
| ch => ch.id === this.state.currentChannel.id | |
| ); | |
| if (currentIndex >= 0) { | |
| const nextIndex = (currentIndex + 1) % this.state.filteredChannels.length; | |
| this.selectChannel(nextIndex); | |
| } else if (this.state.filteredChannels.length > 0) { | |
| this.selectChannel(0); | |
| } | |
| } | |
| selectPreviousChannel() { | |
| if (!this.state.currentChannel || this.state.filteredChannels.length === 0) return; | |
| const currentIndex = this.state.filteredChannels.findIndex( | |
| ch => ch.id === this.state.currentChannel.id | |
| ); | |
| if (currentIndex >= 0) { | |
| const prevIndex = currentIndex - 1 >= 0 ? | |
| currentIndex - 1 : | |
| this.state.filteredChannels.length - 1; | |
| this.selectChannel(prevIndex); | |
| } else if (this.state.filteredChannels.length > 0) { | |
| this.selectChannel(0); | |
| } | |
| } | |
| playChannel(url) { | |
| this.showLoading(true); | |
| // Destroy previous HLS instance | |
| if (this.state.hls) { | |
| this.state.hls.destroy(); | |
| this.state.hls = null; | |
| } | |
| // Stop current video | |
| this.elements.video.pause(); | |
| this.elements.video.src = ''; | |
| if (Hls.isSupported()) { | |
| this.state.hls = new Hls({ | |
| enableWorker: true, | |
| lowLatencyMode: true, | |
| backBufferLength: 30, | |
| maxBufferLength: 60, | |
| debug: false, | |
| liveSyncDurationCount: 3, | |
| liveMaxLatencyDurationCount: 10 | |
| }); | |
| this.state.hls.loadSource(url); | |
| this.state.hls.attachMedia(this.elements.video); | |
| this.state.hls.on(Hls.Events.MANIFEST_PARSED, () => { | |
| this.elements.video.play().then(() => { | |
| this.showLoading(false); | |
| this.state.isPlaying = true; | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| }).catch(error => { | |
| console.log('Autoplay prevented:', error); | |
| this.showLoading(false); | |
| this.updateStatus('Click play button to start'); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| }); | |
| }); | |
| this.state.hls.on(Hls.Events.ERROR, (event, data) => { | |
| console.error('HLS Error:', data); | |
| if (data.fatal) { | |
| switch (data.type) { | |
| case Hls.ErrorTypes.NETWORK_ERROR: | |
| this.updateStatus('Network error - retrying...'); | |
| this.state.hls.startLoad(); | |
| break; | |
| case Hls.ErrorTypes.MEDIA_ERROR: | |
| this.updateStatus('Media error - recovering...'); | |
| this.state.hls.recoverMediaError(); | |
| break; | |
| default: | |
| this.state.hls.destroy(); | |
| this.showNotification('Playback failed. Trying next channel...', 'error'); | |
| this.selectNextChannel(); | |
| break; | |
| } | |
| } | |
| }); | |
| this.state.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => { | |
| const level = this.state.hls.levels[data.level]; | |
| if (level) { | |
| const bitrate = Math.round(level.bitrate / 1000); | |
| const height = level.height || 'Auto'; | |
| this.state.quality = height === 'Auto' ? 'Auto' : `${height}p`; | |
| this.elements.qualityBadge.textContent = this.state.quality; | |
| this.elements.bitrateStat.textContent = `${bitrate} Mbps`; | |
| this.updateQualityStats(); | |
| } | |
| }); | |
| this.state.hls.on(Hls.Events.BUFFER_CREATED, () => { | |
| this.updateBufferStats(); | |
| }); | |
| } else if (this.elements.video.canPlayType('application/vnd.apple.mpegurl')) { | |
| // Safari native HLS support | |
| this.elements.video.src = url; | |
| this.elements.video.play().then(() => { | |
| this.showLoading(false); | |
| this.state.isPlaying = true; | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| }).catch(error => { | |
| console.log('Native HLS autoplay prevented:', error); | |
| this.showLoading(false); | |
| this.updateStatus('Click play button to start'); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| }); | |
| } else { | |
| this.showLoading(false); | |
| this.showNotification('Your browser does not support HLS streaming', 'error'); | |
| } | |
| } | |
| togglePlayPause() { | |
| if (this.elements.video.paused) { | |
| this.elements.video.play(); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| } else { | |
| this.elements.video.pause(); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| } | |
| } | |
| toggleFavorite(channelId) { | |
| if (this.state.favorites[channelId]) { | |
| delete this.state.favorites[channelId]; | |
| this.showNotification('Removed from favorites', 'info'); | |
| } else { | |
| this.state.favorites[channelId] = true; | |
| this.showNotification('Added to favorites', 'success'); | |
| } | |
| localStorage.setItem('clarke_favs', JSON.stringify(this.state.favorites)); | |
| this.renderFavorites(); | |
| // Update favorite button if this is the current channel | |
| if (this.state.currentChannel && this.state.currentChannel.id === channelId) { | |
| this.elements.toggleFav.classList.toggle('active', this.state.favorites[channelId]); | |
| } | |
| } | |
| renderFavorites() { | |
| const container = this.elements.favoritesList; | |
| const favoriteChannels = this.state.channels.filter(ch => this.state.favorites[ch.id]); | |
| if (favoriteChannels.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-favorites"> | |
| <i class="fas fa-star"></i> | |
| <p>No favorites yet</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| let html = ''; | |
| favoriteChannels.forEach(channel => { | |
| html += ` | |
| <div class="favorite-item" data-id="${channel.id}"> | |
| <i class="fas fa-star"></i> | |
| <span>${channel.title}</span> | |
| </div> | |
| `; | |
| }); | |
| container.innerHTML = html; | |
| // Add click listeners | |
| container.querySelectorAll('.favorite-item').forEach(item => { | |
| item.addEventListener('click', () => { | |
| const channelId = item.dataset.id; | |
| const channelIndex = this.state.filteredChannels.findIndex(ch => ch.id === channelId); | |
| if (channelIndex >= 0) { | |
| this.selectChannel(channelIndex); | |
| } | |
| }); | |
| }); | |
| } | |
| toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen().catch(err => { | |
| console.log(`Fullscreen error: ${err.message}`); | |
| }); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| toggleMute() { | |
| this.elements.video.muted = !this.elements.video.muted; | |
| this.updateStatus(this.elements.video.muted ? 'Muted' : 'Unmuted'); | |
| } | |
| seek(seconds) { | |
| this.elements.video.currentTime += seconds; | |
| } | |
| updateStatus(text) { | |
| this.elements.statusText.textContent = text; | |
| } | |
| updateChannelCount() { | |
| const count = this.state.filteredChannels.length; | |
| const total = this.state.channels.length; | |
| this.elements.channelCount.textContent = `${count}`; | |
| // Update average quality estimation | |
| if (total > 0) { | |
| const hdCount = this.state.channels.filter(ch => | |
| ch.title.toLowerCase().includes('hd') || | |
| ch.title.toLowerCase().includes('1080') || | |
| ch.title.toLowerCase().includes('4k') | |
| ).length; | |
| const hdPercentage = Math.round((hdCount / total) * 100); | |
| this.elements.avgQuality.textContent = `${hdPercentage}% HD`; | |
| } | |
| } | |
| updatePlaybackInfo() { | |
| const current = this.elements.video.currentTime; | |
| const duration = this.elements.video.duration || 0; | |
| // Format time | |
| const formatTime = (seconds) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| }; | |
| this.elements.currentTime.textContent = formatTime(current); | |
| this.elements.totalTime.textContent = formatTime(duration); | |
| // Update buffer progress | |
| if (this.elements.video.buffered.length > 0) { | |
| const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1); | |
| const bufferPercentage = duration > 0 ? (bufferedEnd / duration) * 100 : 0; | |
| this.elements.bufferProgress.style.width = `${bufferPercentage}%`; | |
| } | |
| } | |
| updateStats() { | |
| // Update health based on buffering | |
| if (this.state.hls) { | |
| const bufferLength = this.state.hls.media.buffered.length; | |
| const health = bufferLength > 0 ? 100 : 80; | |
| this.elements.healthStat.textContent = `${health}%`; | |
| } | |
| // Update buffer time | |
| if (this.elements.video.buffered.length > 0) { | |
| const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1); | |
| const bufferTime = Math.round(bufferedEnd - this.elements.video.currentTime); | |
| this.elements.bufferStat.textContent = `${bufferTime}s`; | |
| } | |
| // Update every second | |
| setTimeout(() => this.updateStats(), 1000); | |
| } | |
| updateBufferStats() { | |
| if (this.state.hls) { | |
| const bufferInfo = this.state.hls.media.buffered; | |
| if (bufferInfo.length > 0) { | |
| const bufferTime = bufferInfo.end(bufferInfo.length - 1) - this.elements.video.currentTime; | |
| this.elements.bufferStat.textContent = `${Math.round(bufferTime)}s`; | |
| } | |
| } | |
| } | |
| updateQualityStats() { | |
| // Update quality distribution | |
| if (this.state.channels.length > 0) { | |
| const hdChannels = this.state.channels.filter(ch => | |
| ch.title.toLowerCase().includes('hd') || | |
| ch.title.toLowerCase().includes('1080') || | |
| ch.title.toLowerCase().includes('4k') | |
| ).length; | |
| const hdPercentage = Math.round((hdChannels / this.state.channels.length) * 100); | |
| this.elements.avgQuality.textContent = `${hdPercentage}% HD`; | |
| } | |
| } | |
| clearPlaylist() { | |
| this.state.channels = []; | |
| this.state.filteredChannels = []; | |
| this.state.currentChannel = null; | |
| if (this.state.hls) { | |
| this.state.hls.destroy(); | |
| this.state.hls = null; | |
| } | |
| this.elements.video.pause(); | |
| this.elements.video.src = ''; | |
| this.renderChannels(); | |
| this.updateChannelCount(); | |
| this.updateStatus('Playlist cleared'); | |
| this.elements.currentChannel.textContent = 'Welcome to Clarke Player Pro'; | |
| this.elements.currentProgram.textContent = 'Load a playlist to start streaming'; | |
| this.elements.nowPlayingChannel.textContent = '--'; | |
| this.elements.nowPlayingRegion.textContent = '--'; | |
| this.showNotification('Playlist cleared', 'info'); | |
| } | |
| showLoading(show) { | |
| this.elements.loadingOverlay.style.display = show ? 'flex' : 'none'; | |
| } | |
| showNotification(message, type = 'info') { | |
| // Create notification element | |
| const notification = document.createElement('div'); | |
| notification.className = `notification notification-${type}`; | |
| notification.innerHTML = ` | |
| <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> | |
| <span>${message}</span> | |
| `; | |
| // Add styles for notification | |
| notification.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: ${type === 'success' ? 'var(--accent)' : type === 'error' ? 'var(--danger)' : 'var(--info)'}; | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| z-index: 10000; | |
| animation: slideIn 0.3s ease; | |
| box-shadow: var(--shadow-lg); | |
| max-width: 300px; | |
| `; | |
| document.body.appendChild(notification); | |
| // Remove after 3 seconds | |
| setTimeout(() => { | |
| notification.style.animation = 'slideOut 0.3s ease'; | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 300); | |
| }, 3000); | |
| // Add animation keyframes | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes slideOut { | |
| from { transform: translateX(0); opacity: 1; } | |
| to { transform: translateX(100%); opacity: 0; } | |
| } | |
| `; | |
| if (!document.querySelector('#notification-styles')) { | |
| style.id = 'notification-styles'; | |
| document.head.appendChild(style); | |
| } | |
| } | |
| } | |
| // Initialize Clarke Player Pro | |
| let player; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| player = new ClarkePlayerPro(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |