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 Ultra • Premium TV Experience</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 Ultra - Premium TV Interface */ | |
| :root { | |
| --primary: #0066FF; | |
| --primary-dark: #0052D4; | |
| --primary-light: #4D94FF; | |
| --accent: #FF3366; | |
| --accent-dark: #E62E5C; | |
| --success: #00CC88; | |
| --warning: #FFAA00; | |
| --info: #00C6FF; | |
| --bg-dark: #0A0E17; | |
| --bg-darker: #05070F; | |
| --bg-card: #13182B; | |
| --bg-panel: #1C243F; | |
| --bg-surface: rgba(255, 255, 255, 0.05); | |
| --text-primary: #FFFFFF; | |
| --text-secondary: #B0B7D6; | |
| --text-muted: #6B7299; | |
| --border: rgba(255, 255, 255, 0.08); | |
| --border-light: rgba(255, 255, 255, 0.04); | |
| --glow-primary: 0 0 30px rgba(0, 102, 255, 0.4); | |
| --glow-accent: 0 0 30px rgba(255, 51, 102, 0.4); | |
| --shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.5); | |
| --gradient-primary: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| --gradient-accent: linear-gradient(135deg, var(--accent), var(--accent-dark)); | |
| --gradient-dark: linear-gradient(180deg, #0A0E17 0%, #05070F 100%); | |
| --glass-bg: rgba(19, 24, 43, 0.8); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| --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(--gradient-dark); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| /* App Container */ | |
| .app-container { | |
| display: grid; | |
| grid-template-columns: 1fr 400px; | |
| grid-template-rows: auto 1fr auto; | |
| height: 100vh; | |
| gap: 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Premium TV Header */ | |
| .tv-header { | |
| grid-column: 1 / -1; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(40px); | |
| -webkit-backdrop-filter: blur(40px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 0.8rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| z-index: 1000; | |
| position: relative; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| } | |
| .logo-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .logo-icon { | |
| font-size: 2rem; | |
| color: var(--primary); | |
| position: relative; | |
| } | |
| .logo-icon::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 40px; | |
| height: 40px; | |
| background: var(--primary); | |
| border-radius: 50%; | |
| filter: blur(15px); | |
| opacity: 0.3; | |
| z-index: -1; | |
| } | |
| .logo-text { | |
| display: flex; | |
| flex-direction: column; | |
| line-height: 1.2; | |
| } | |
| .logo-main { | |
| font-size: 1.8rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, var(--text-primary), var(--primary-light)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.5px; | |
| } | |
| .logo-sub { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.5rem 1rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| position: relative; | |
| } | |
| .status-dot::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| opacity: 0.3; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 0.3; transform: translate(-50%, -50%) scale(1); } | |
| 50% { opacity: 0.6; transform: translate(-50%, -50%) scale(1.2); } | |
| } | |
| .status-text { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .header-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .tv-control-btn { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 12px; | |
| 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); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .tv-control-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent); | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .tv-control-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| transform: translateY(-2px); | |
| border-color: var(--primary); | |
| box-shadow: var(--glow-primary); | |
| } | |
| .tv-control-btn:hover::before { | |
| opacity: 1; | |
| } | |
| .tv-control-btn.active { | |
| background: var(--gradient-primary); | |
| color: white; | |
| border-color: transparent; | |
| box-shadow: var(--glow-primary); | |
| } | |
| /* Main Video Area */ | |
| .video-area { | |
| grid-column: 1; | |
| grid-row: 2; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| background: #000; | |
| overflow: hidden; | |
| } | |
| .video-container { | |
| flex: 1; | |
| position: relative; | |
| background: #000; | |
| overflow: hidden; | |
| } | |
| #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 50%, rgba(0, 0, 0, 0.9) 100%); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: flex-end; | |
| padding: 2rem; | |
| opacity: 0; | |
| transition: var(--transition-slow); | |
| pointer-events: none; | |
| } | |
| .video-container:hover .video-overlay { | |
| opacity: 1; | |
| } | |
| .channel-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .channel-logo { | |
| width: 70px; | |
| height: 70px; | |
| border-radius: 16px; | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(20px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.8rem; | |
| color: var(--primary-light); | |
| border: 1px solid var(--border); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .channel-logo::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent); | |
| } | |
| .channel-info { | |
| flex: 1; | |
| } | |
| .channel-name { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| background: linear-gradient(135deg, var(--text-primary), var(--primary-light)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .channel-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .channel-region { | |
| padding: 0.4rem 1rem; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| backdrop-filter: blur(20px); | |
| } | |
| .channel-quality { | |
| padding: 0.4rem 1rem; | |
| background: var(--gradient-primary); | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: white; | |
| } | |
| .player-controls { | |
| position: absolute; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(40px); | |
| padding: 1rem 2rem; | |
| border-radius: 50px; | |
| border: 1px solid var(--border); | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .video-container:hover .player-controls { | |
| opacity: 1; | |
| } | |
| .control-btn { | |
| width: 52px; | |
| height: 52px; | |
| 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); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .control-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.2), transparent); | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .control-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: scale(1.1); | |
| } | |
| .control-btn:hover::before { | |
| opacity: 1; | |
| } | |
| .control-btn.play-btn { | |
| background: var(--gradient-primary); | |
| border: none; | |
| box-shadow: var(--glow-primary); | |
| } | |
| .control-btn.play-btn:hover { | |
| background: var(--primary-dark); | |
| transform: scale(1.1); | |
| } | |
| .loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| z-index: 100; | |
| } | |
| .tv-spinner { | |
| width: 80px; | |
| height: 80px; | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-top-color: var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1.5s linear infinite; | |
| position: relative; | |
| } | |
| .tv-spinner::before { | |
| content: ''; | |
| position: absolute; | |
| top: -4px; | |
| left: -4px; | |
| right: -4px; | |
| bottom: -4px; | |
| border: 4px solid transparent; | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 2s linear infinite reverse; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* TV Info Bar */ | |
| .tv-info-bar { | |
| grid-column: 1; | |
| grid-row: 3; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(40px); | |
| border-top: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .playback-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .time-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| } | |
| .separator { | |
| color: var(--text-muted); | |
| } | |
| .progress-container { | |
| flex: 1; | |
| max-width: 400px; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin: 0 2rem; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: var(--gradient-primary); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| position: relative; | |
| } | |
| .progress-bar::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 8px; | |
| height: 8px; | |
| background: white; | |
| border-radius: 50%; | |
| transform: translate(50%, -2px); | |
| box-shadow: 0 0 10px var(--primary); | |
| } | |
| .tv-shortcuts { | |
| display: flex; | |
| gap: 2rem; | |
| } | |
| .shortcut-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .shortcut-key { | |
| padding: 0.3rem 0.6rem; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| font-family: 'Inter', monospace; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| min-width: 40px; | |
| text-align: center; | |
| } | |
| .shortcut-label { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| /* Channels Sidebar */ | |
| .channels-sidebar { | |
| grid-column: 2; | |
| grid-row: 2 / 4; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(40px); | |
| border-left: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .sidebar-header { | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(0, 0, 0, 0.3); | |
| } | |
| .sidebar-title { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .sidebar-title i { | |
| color: var(--primary); | |
| } | |
| .channel-count { | |
| padding: 0.3rem 0.8rem; | |
| background: var(--gradient-primary); | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: white; | |
| } | |
| .sidebar-controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .sidebar-btn { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| 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); | |
| } | |
| .sidebar-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| .sidebar-btn.active { | |
| background: var(--gradient-primary); | |
| color: white; | |
| border-color: transparent; | |
| } | |
| /* Playlist Input Panel */ | |
| .playlist-panel { | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(0, 0, 0, 0.3); | |
| } | |
| .panel-title { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .input-group { | |
| margin-bottom: 1rem; | |
| } | |
| .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 3rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| color: var(--text-primary); | |
| font-size: 0.9rem; | |
| 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(0, 102, 255, 0.1); | |
| } | |
| /* File Upload Area */ | |
| .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); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .file-upload-area:hover { | |
| border-color: var(--primary); | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .file-upload-area.dragover { | |
| border-color: var(--primary); | |
| background: rgba(0, 102, 255, 0.1); | |
| box-shadow: var(--glow-primary); | |
| } | |
| .file-upload-area i { | |
| font-size: 2.5rem; | |
| color: var(--text-muted); | |
| margin-bottom: 1rem; | |
| display: block; | |
| } | |
| .file-upload-area p { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| margin: 0; | |
| line-height: 1.4; | |
| } | |
| .file-upload-area .file-name { | |
| font-size: 0.85rem; | |
| color: var(--primary-light); | |
| margin-top: 0.5rem; | |
| font-weight: 500; | |
| } | |
| .file-input { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| .quick-presets { | |
| margin-top: 1.5rem; | |
| } | |
| .preset-title { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.75rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .preset-buttons { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 0.5rem; | |
| } | |
| .preset-btn { | |
| padding: 0.6rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| cursor: pointer; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.3rem; | |
| transition: var(--transition); | |
| text-align: center; | |
| } | |
| .preset-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-color: var(--primary); | |
| color: var(--text-primary); | |
| transform: translateY(-2px); | |
| } | |
| .preset-btn i { | |
| font-size: 1rem; | |
| } | |
| .action-buttons { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 0.75rem; | |
| margin-top: 1.5rem; | |
| } | |
| .tv-btn { | |
| padding: 0.875rem; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| transition: var(--transition); | |
| } | |
| .tv-btn-primary { | |
| background: var(--gradient-primary); | |
| color: white; | |
| } | |
| .tv-btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--glow-primary); | |
| } | |
| .tv-btn-secondary { | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| } | |
| .tv-btn-secondary:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| /* Channels List */ | |
| .channels-list-container { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .channels-filter { | |
| padding: 1rem 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| gap: 0.75rem; | |
| } | |
| .filter-btn { | |
| padding: 0.5rem 1rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| color: var(--text-secondary); | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| white-space: nowrap; | |
| } | |
| .filter-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| .filter-btn.active { | |
| background: var(--gradient-primary); | |
| color: white; | |
| border-color: transparent; | |
| } | |
| .search-box { | |
| position: relative; | |
| flex: 1; | |
| } | |
| .search-box i { | |
| position: absolute; | |
| left: 1rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| } | |
| .search-box input { | |
| width: 100%; | |
| padding: 0.75rem 1rem 0.75rem 2.5rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| color: var(--text-primary); | |
| font-size: 0.85rem; | |
| outline: none; | |
| } | |
| .channels-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0.5rem; | |
| } | |
| /* Hide scrollbar but keep functionality */ | |
| .channels-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .channels-list::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 3px; | |
| } | |
| .channels-list::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 3px; | |
| } | |
| .channel-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 1rem; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| margin-bottom: 0.5rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| border: 1px solid transparent; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .channel-item::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.05), transparent); | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .channel-item:hover { | |
| background: rgba(255, 255, 255, 0.06); | |
| border-color: var(--border); | |
| transform: translateX(5px); | |
| } | |
| .channel-item:hover::before { | |
| opacity: 1; | |
| } | |
| .channel-item.active { | |
| background: rgba(0, 102, 255, 0.15); | |
| border-color: var(--primary); | |
| box-shadow: 0 0 20px rgba(0, 102, 255, 0.2); | |
| } | |
| .channel-item.active::before { | |
| opacity: 1; | |
| } | |
| .channel-number { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 10px; | |
| background: rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-right: 1rem; | |
| flex-shrink: 0; | |
| position: relative; | |
| } | |
| .channel-item.active .channel-number { | |
| background: var(--gradient-primary); | |
| color: white; | |
| } | |
| .channel-info { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .channel-title { | |
| font-weight: 600; | |
| margin-bottom: 0.25rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .channel-category { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .channel-category::before { | |
| content: ''; | |
| width: 6px; | |
| height: 6px; | |
| background: var(--primary); | |
| border-radius: 50%; | |
| } | |
| .channel-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .fav-btn { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| background: transparent; | |
| 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 { | |
| color: #FFD700; | |
| border-color: rgba(255, 215, 0, 0.3); | |
| transform: scale(1.1); | |
| } | |
| .fav-btn.active { | |
| color: #FFD700; | |
| border-color: rgba(255, 215, 0, 0.3); | |
| background: rgba(255, 215, 0, 0.1); | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| padding: 2rem; | |
| text-align: center; | |
| color: var(--text-secondary); | |
| } | |
| .empty-icon { | |
| font-size: 4rem; | |
| margin-bottom: 1.5rem; | |
| opacity: 0.5; | |
| color: var(--primary); | |
| } | |
| .empty-state h3 { | |
| font-size: 1.5rem; | |
| color: var(--text-primary); | |
| margin-bottom: 0.5rem; | |
| } | |
| /* TV Stats Panel */ | |
| .tv-stats { | |
| padding: 1.5rem; | |
| border-top: 1px solid var(--border); | |
| background: rgba(0, 0, 0, 0.3); | |
| } | |
| .stats-title { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .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: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| background: rgba(0, 102, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin: 0 auto 0.75rem; | |
| color: var(--primary); | |
| font-size: 1.2rem; | |
| } | |
| .stat-value { | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .stat-label { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| /* TV Overlay */ | |
| .tv-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| backdrop-filter: blur(20px); | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .overlay-content { | |
| max-width: 500px; | |
| width: 90%; | |
| background: var(--glass-bg); | |
| border-radius: 24px; | |
| border: 1px solid var(--border); | |
| padding: 2.5rem; | |
| text-align: center; | |
| box-shadow: var(--shadow-xl); | |
| } | |
| .overlay-icon { | |
| font-size: 4rem; | |
| color: var(--primary); | |
| margin-bottom: 1.5rem; | |
| } | |
| .overlay-title { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 1rem; | |
| background: linear-gradient(135deg, var(--text-primary), var(--primary-light)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .overlay-message { | |
| color: var(--text-secondary); | |
| margin-bottom: 2rem; | |
| line-height: 1.6; | |
| } | |
| .overlay-buttons { | |
| display: flex; | |
| gap: 1rem; | |
| justify-content: center; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 1400px) { | |
| .app-container { | |
| grid-template-columns: 1fr 350px; | |
| } | |
| } | |
| @media (max-width: 1200px) { | |
| .app-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .channels-sidebar { | |
| display: none; | |
| } | |
| .sidebar-toggle { | |
| display: flex ; | |
| } | |
| } | |
| .sidebar-toggle { | |
| display: none; | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| z-index: 100; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- TV Header --> | |
| <header class="tv-header"> | |
| <div class="header-left"> | |
| <div class="logo-container"> | |
| <div class="logo-icon"> | |
| <i class="fas fa-broadcast-tower"></i> | |
| </div> | |
| <div class="logo-text"> | |
| <div class="logo-main">CLARKE ULTRA</div> | |
| <div class="logo-sub">PREMIUM TV EXPERIENCE</div> | |
| </div> | |
| </div> | |
| <div class="status-indicator"> | |
| <div class="status-dot"></div> | |
| <span class="status-text" id="statusText">Ready</span> | |
| </div> | |
| </div> | |
| <div class="header-controls"> | |
| <button class="tv-control-btn" id="guideBtn" title="TV Guide"> | |
| <i class="fas fa-tv"></i> | |
| </button> | |
| <button class="tv-control-btn" id="favoritesBtn" title="Favorites"> | |
| <i class="fas fa-star"></i> | |
| </button> | |
| <button class="tv-control-btn" id="settingsBtn" title="Settings"> | |
| <i class="fas fa-sliders-h"></i> | |
| </button> | |
| <button class="tv-control-btn" id="fullscreenBtn" title="Fullscreen"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| <button class="sidebar-toggle tv-control-btn" id="sidebarToggle"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Video Area --> | |
| <main class="video-area"> | |
| <div class="video-container"> | |
| <video id="videoPlayer" controls playsinline></video> | |
| <div class="video-overlay"> | |
| <div class="channel-display"> | |
| <div class="channel-logo"> | |
| <i class="fas fa-satellite"></i> | |
| </div> | |
| <div class="channel-info"> | |
| <div class="channel-name" id="currentChannel">Welcome to Clarke Ultra</div> | |
| <div class="channel-meta"> | |
| <span class="channel-region" id="currentRegion">TV</span> | |
| <span class="channel-quality" id="currentQuality">4K READY</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="player-controls"> | |
| <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 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="tv-spinner"></div> | |
| <p>Loading broadcast stream...</p> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- TV Info Bar --> | |
| <div class="tv-info-bar"> | |
| <div class="playback-info"> | |
| <div class="time-display"> | |
| <span id="currentTime">00:00</span> | |
| <span class="separator">/</span> | |
| <span id="totalTime">00:00</span> | |
| </div> | |
| </div> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="bufferProgress"></div> | |
| </div> | |
| <div class="tv-shortcuts"> | |
| <div class="shortcut-item"> | |
| <kbd class="shortcut-key">SPACE</kbd> | |
| <span class="shortcut-label">Play/Pause</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <kbd class="shortcut-key">F</kbd> | |
| <span class="shortcut-label">Fullscreen</span> | |
| </div> | |
| <div class="shortcut-item"> | |
| <kbd class="shortcut-key">↑↓</kbd> | |
| <span class="shortcut-label">Channels</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Channels Sidebar --> | |
| <aside class="channels-sidebar" id="channelsSidebar"> | |
| <div class="sidebar-header"> | |
| <div class="sidebar-title"> | |
| <i class="fas fa-th-list"></i> | |
| <span>CHANNEL GUIDE</span> | |
| </div> | |
| <div class="channel-count" id="channelCount">0</div> | |
| </div> | |
| <!-- Playlist Input --> | |
| <div class="playlist-panel"> | |
| <div class="panel-title"> | |
| <i class="fas fa-satellite-dish"></i> | |
| <span>STREAM SOURCE</span> | |
| </div> | |
| <div class="input-group"> | |
| <div class="input-with-icon"> | |
| <i class="fas fa-link"></i> | |
| <input type="text" id="playlistUrl" | |
| placeholder="Enter M3U/M3U8 URL" | |
| value="https://i.mjh.nz/PlutoTV/us.m3u8"> | |
| </div> | |
| </div> | |
| <!-- File Upload Area --> | |
| <div class="input-group"> | |
| <div class="file-upload-area" id="fileUploadArea"> | |
| <i class="fas fa-file-upload"></i> | |
| <p>Drag & drop .m3u/.m3u8 file here<br>or click to browse</p> | |
| <div class="file-name" id="fileName"></div> | |
| <input type="file" id="fileInput" class="file-input" accept=".m3u,.m3u8"> | |
| </div> | |
| </div> | |
| <div class="quick-presets"> | |
| <div class="preset-title"> | |
| <i class="fas fa-bolt"></i> | |
| <span>QUICK PRESETS</span> | |
| </div> | |
| <div class="preset-buttons"> | |
| <button class="preset-btn" data-url="https://i.mjh.nz/PlutoTV/us.m3u8"> | |
| <i class="fas fa-flag-usa"></i> | |
| <span>US TV</span> | |
| </button> | |
| <button class="preset-btn" data-url="https://i.mjh.nz/PlutoTV/gb.m3u8"> | |
| <i class="fas fa-flag-uk"></i> | |
| <span>UK TV</span> | |
| </button> | |
| <button class="preset-btn" data-url="https://iptv-org.github.io/iptv/index.m3u"> | |
| <i class="fas fa-globe"></i> | |
| <span>Global</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="action-buttons"> | |
| <button class="tv-btn tv-btn-primary" id="loadPlaylistBtn"> | |
| <i class="fas fa-play-circle"></i> | |
| <span>LOAD URL</span> | |
| </button> | |
| <button class="tv-btn tv-btn-secondary" id="clearBtn"> | |
| <i class="fas fa-trash"></i> | |
| <span>CLEAR</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Channels List --> | |
| <div class="channels-list-container"> | |
| <div class="channels-filter"> | |
| <div class="search-box"> | |
| <i class="fas fa-search"></i> | |
| <input type="text" id="channelSearch" placeholder="Search channels..."> | |
| </div> | |
| </div> | |
| <div class="channels-filter"> | |
| <button class="filter-btn active" data-filter="all">ALL</button> | |
| <button class="filter-btn" data-filter="us">US</button> | |
| <button class="filter-btn" data-filter="gb">UK</button> | |
| <button class="filter-btn" data-filter="fav"> | |
| <i class="fas fa-star"></i> | |
| </button> | |
| </div> | |
| <div class="channels-list" id="channelsList"> | |
| <div class="empty-state"> | |
| <div class="empty-icon"> | |
| <i class="fas fa-broadcast-tower"></i> | |
| </div> | |
| <h3>No Channels Loaded</h3> | |
| <p>Enter a playlist URL or upload a file to begin</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TV Stats --> | |
| <div class="tv-stats"> | |
| <div class="stats-title"> | |
| <i class="fas fa-chart-line"></i> | |
| <span>STREAM STATS</span> | |
| </div> | |
| <div class="stats-grid"> | |
| <div class="stat-item"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-wifi"></i> | |
| </div> | |
| <div class="stat-value" id="bitrateStat">0</div> | |
| <div class="stat-label">BITRATE</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-tachometer-alt"></i> | |
| </div> | |
| <div class="stat-value" id="bufferStat">0s</div> | |
| <div class="stat-label">BUFFER</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon"> | |
| <i class="fas fa-heartbeat"></i> | |
| </div> | |
| <div class="stat-value" id="healthStat">100%</div> | |
| <div class="stat-label">HEALTH</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Loading Overlay --> | |
| <div class="tv-overlay" id="initialLoadOverlay"> | |
| <div class="overlay-content"> | |
| <div class="overlay-icon"> | |
| <i class="fas fa-satellite"></i> | |
| </div> | |
| <h1 class="overlay-title">CLARKE ULTRA</h1> | |
| <p class="overlay-message">Premium Television Experience<br>Loading broadcast system...</p> | |
| <div class="tv-spinner" style="width: 50px; height: 50px; margin: 0 auto 2rem;"></div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.10/dist/hls.min.js"></script> | |
| <script> | |
| // Clarke Player Ultra - Premium TV JavaScript | |
| class ClarkePlayerUltra { | |
| constructor() { | |
| // State Management | |
| this.state = { | |
| channels: [], | |
| filteredChannels: [], | |
| favorites: JSON.parse(localStorage.getItem('clarke_ultra_favs')) || {}, | |
| currentChannel: null, | |
| currentFilter: 'all', | |
| searchQuery: '', | |
| hls: null, | |
| isPlaying: false, | |
| stats: { | |
| bitrate: 0, | |
| buffer: 0, | |
| health: 100 | |
| }, | |
| currentFile: null | |
| }; | |
| // DOM Elements | |
| this.elements = { | |
| video: document.getElementById('videoPlayer'), | |
| playlistUrl: document.getElementById('playlistUrl'), | |
| fileInput: document.getElementById('fileInput'), | |
| fileUploadArea: document.getElementById('fileUploadArea'), | |
| fileName: document.getElementById('fileName'), | |
| loadPlaylistBtn: document.getElementById('loadPlaylistBtn'), | |
| clearBtn: document.getElementById('clearBtn'), | |
| channelsList: document.getElementById('channelsList'), | |
| currentChannel: document.getElementById('currentChannel'), | |
| currentRegion: document.getElementById('currentRegion'), | |
| currentQuality: document.getElementById('currentQuality'), | |
| statusText: document.getElementById('statusText'), | |
| channelCount: document.getElementById('channelCount'), | |
| channelSearch: document.getElementById('channelSearch'), | |
| playPauseBtn: document.getElementById('playPauseBtn'), | |
| prevChannel: document.getElementById('prevChannel'), | |
| nextChannel: document.getElementById('nextChannel'), | |
| volumeSlider: document.getElementById('volumeSlider'), | |
| loadingOverlay: document.getElementById('loadingOverlay'), | |
| bufferProgress: document.getElementById('bufferProgress'), | |
| currentTime: document.getElementById('currentTime'), | |
| totalTime: document.getElementById('totalTime'), | |
| bitrateStat: document.getElementById('bitrateStat'), | |
| bufferStat: document.getElementById('bufferStat'), | |
| healthStat: document.getElementById('healthStat'), | |
| sidebarToggle: document.getElementById('sidebarToggle'), | |
| channelsSidebar: document.getElementById('channelsSidebar'), | |
| initialLoadOverlay: document.getElementById('initialLoadOverlay'), | |
| favoritesBtn: document.getElementById('favoritesBtn'), | |
| guideBtn: document.getElementById('guideBtn'), | |
| settingsBtn: document.getElementById('settingsBtn'), | |
| fullscreenBtn: document.getElementById('fullscreenBtn') | |
| }; | |
| // Initialize | |
| this.init(); | |
| } | |
| async init() { | |
| console.log('🚀 Clarke Player Ultra Initializing...'); | |
| // Hide initial overlay after 2 seconds | |
| setTimeout(() => { | |
| this.elements.initialLoadOverlay.style.display = 'none'; | |
| this.updateStatus('Ready'); | |
| }, 2000); | |
| // Setup event listeners | |
| this.setupEventListeners(); | |
| this.setupKeyboardControls(); | |
| // Load default playlist if URL exists | |
| if (this.elements.playlistUrl.value) { | |
| setTimeout(() => { | |
| this.loadPlaylistFromUrl(this.elements.playlistUrl.value); | |
| }, 500); | |
| } | |
| // Start stats update loop | |
| this.updateStatsLoop(); | |
| // Set initial volume | |
| this.elements.video.volume = this.elements.volumeSlider.value / 100; | |
| } | |
| setupEventListeners() { | |
| // Load playlist from URL button | |
| this.elements.loadPlaylistBtn.addEventListener('click', () => { | |
| this.loadPlaylistFromUrl(this.elements.playlistUrl.value); | |
| }); | |
| // File input change | |
| this.elements.fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| this.handleFileSelect(e.target.files[0]); | |
| } | |
| }); | |
| // Drag and drop for file upload | |
| this.elements.fileUploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| e.currentTarget.classList.add('dragover'); | |
| }); | |
| this.elements.fileUploadArea.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('dragover'); | |
| }); | |
| this.elements.fileUploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length > 0) { | |
| const file = e.dataTransfer.files[0]; | |
| if (file.name.endsWith('.m3u') || file.name.endsWith('.m3u8')) { | |
| this.handleFileSelect(file); | |
| } else { | |
| this.showNotification('Please select a .m3u or .m3u8 file', 'error'); | |
| } | |
| } | |
| }); | |
| // Clear button | |
| this.elements.clearBtn.addEventListener('click', () => { | |
| this.clearPlaylist(); | |
| }); | |
| // 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(url); | |
| }); | |
| }); | |
| // Filter buttons | |
| document.querySelectorAll('.filter-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const filter = e.currentTarget.dataset.filter; | |
| this.setFilter(filter); | |
| // Update active state | |
| document.querySelectorAll('.filter-btn').forEach(b => { | |
| b.classList.remove('active'); | |
| }); | |
| e.currentTarget.classList.add('active'); | |
| }); | |
| }); | |
| // Favorites button | |
| this.elements.favoritesBtn.addEventListener('click', () => { | |
| this.setFilter('fav'); | |
| this.elements.favoritesBtn.classList.toggle('active'); | |
| // Update filter button | |
| document.querySelectorAll('.filter-btn').forEach(b => { | |
| b.classList.toggle('active', b.dataset.filter === 'fav'); | |
| }); | |
| }); | |
| // Search input | |
| this.elements.channelSearch.addEventListener('input', (e) => { | |
| this.state.searchQuery = e.target.value.toLowerCase(); | |
| 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(); | |
| }); | |
| // Volume control | |
| this.elements.volumeSlider.addEventListener('input', (e) => { | |
| this.elements.video.volume = e.target.value / 100; | |
| }); | |
| // Fullscreen | |
| this.elements.fullscreenBtn.addEventListener('click', () => { | |
| this.toggleFullscreen(); | |
| }); | |
| // Sidebar toggle | |
| this.elements.sidebarToggle.addEventListener('click', () => { | |
| this.elements.channelsSidebar.style.display = | |
| this.elements.channelsSidebar.style.display === 'none' ? 'flex' : 'none'; | |
| }); | |
| // 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(); | |
| }); | |
| this.elements.video.addEventListener('error', (e) => { | |
| console.error('Video error:', e); | |
| this.updateStatus('Playback error'); | |
| this.showLoading(false); | |
| }); | |
| // 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); | |
| }); | |
| // Settings button | |
| this.elements.settingsBtn.addEventListener('click', () => { | |
| this.showSettings(); | |
| }); | |
| } | |
| 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(this.elements.playlistUrl.value); | |
| break; | |
| case 'escape': | |
| if (document.fullscreenElement) { | |
| document.exitFullscreen(); | |
| } | |
| break; | |
| } | |
| }); | |
| } | |
| handleFileSelect(file) { | |
| if (!file.name.endsWith('.m3u') && !file.name.endsWith('.m3u8')) { | |
| this.showNotification('Please select a .m3u or .m3u8 file', 'error'); | |
| return; | |
| } | |
| this.state.currentFile = file; | |
| this.elements.fileName.textContent = file.name; | |
| this.updateStatus('File selected: ' + file.name); | |
| // Auto-load the file | |
| this.loadPlaylistFromFile(file); | |
| } | |
| async loadPlaylistFromFile(file) { | |
| this.updateStatus('Loading playlist file...'); | |
| this.showLoading(true); | |
| try { | |
| const text = await file.text(); | |
| await this.parsePlaylist(text, file.name); | |
| this.showNotification(`Playlist loaded: ${file.name}`, 'success'); | |
| } catch (error) { | |
| console.error('Failed to load file:', error); | |
| this.showNotification('Failed to load playlist file', 'error'); | |
| this.updateStatus('Load failed'); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async loadPlaylistFromUrl(url) { | |
| 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'); | |
| } 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 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', | |
| region: source.includes('us') ? 'US' : source.includes('gb') ? 'UK' : 'INT', | |
| number: channelNumber, | |
| url: '', | |
| category: this.detectCategory(titleMatch ? titleMatch[1] : '') | |
| }; | |
| channelNumber++; | |
| } | |
| else if (line.startsWith('http')) { | |
| currentChannel.url = line; | |
| channels.push({...currentChannel}); | |
| } | |
| } | |
| this.state.channels = channels; | |
| this.setFilter('all'); | |
| 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.channelsList; | |
| if (this.state.filteredChannels.length === 0) { | |
| container.innerHTML = ` | |
| <div class="empty-state"> | |
| <div class="empty-icon"> | |
| <i class="fas fa-tv"></i> | |
| </div> | |
| <h3>No Channels Found</h3> | |
| <p>Try a different filter or search term</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-item ${isActive ? 'active' : ''}" | |
| data-index="${index}" | |
| data-id="${channel.id}"> | |
| <div class="channel-number">${channel.number}</div> | |
| <div class="channel-info"> | |
| <div class="channel-title">${channel.title}</div> | |
| <div class="channel-category">${channel.category}</div> | |
| </div> | |
| <div class="channel-actions"> | |
| <button class="fav-btn ${isFavorite ? 'active' : ''}" | |
| data-id="${channel.id}"> | |
| <i class="fas fa-star"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| container.innerHTML = html; | |
| // Add event listeners | |
| container.querySelectorAll('.channel-item').forEach(item => { | |
| item.addEventListener('click', (e) => { | |
| if (!e.target.closest('.fav-btn')) { | |
| const index = parseInt(item.dataset.index); | |
| this.selectChannel(index); | |
| } | |
| }); | |
| }); | |
| container.querySelectorAll('.fav-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const channelId = btn.dataset.id; | |
| this.toggleFavorite(channelId); | |
| btn.classList.toggle('active'); | |
| }); | |
| }); | |
| } | |
| setFilter(filter) { | |
| this.state.currentFilter = filter; | |
| this.filterChannels(); | |
| } | |
| filterChannels() { | |
| let filtered = this.state.channels; | |
| // Apply filter | |
| if (this.state.currentFilter === 'us') { | |
| filtered = filtered.filter(ch => ch.region === 'US'); | |
| } else if (this.state.currentFilter === 'gb') { | |
| filtered = filtered.filter(ch => ch.region === 'UK'); | |
| } else if (this.state.currentFilter === 'fav') { | |
| filtered = filtered.filter(ch => this.state.favorites[ch.id]); | |
| } | |
| // Apply search | |
| if (this.state.searchQuery) { | |
| filtered = filtered.filter(ch => | |
| ch.title.toLowerCase().includes(this.state.searchQuery) || | |
| ch.category.toLowerCase().includes(this.state.searchQuery) | |
| ); | |
| } | |
| this.state.filteredChannels = filtered; | |
| this.renderChannels(); | |
| this.elements.channelCount.textContent = filtered.length; | |
| } | |
| 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.currentRegion.textContent = channel.region; | |
| this.elements.currentQuality.textContent = 'HD'; | |
| // Update active state | |
| document.querySelectorAll('.channel-item').forEach(item => { | |
| item.classList.remove('active'); | |
| }); | |
| const selectedItem = document.querySelector(`.channel-item[data-index="${index}"]`); | |
| if (selectedItem) { | |
| selectedItem.classList.add('active'); | |
| selectedItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| // Play 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 | |
| }); | |
| 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.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('Stream error', 'error'); | |
| break; | |
| } | |
| } | |
| }); | |
| this.state.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => { | |
| const level = this.state.hls.levels[data.level]; | |
| if (level) { | |
| this.state.stats.bitrate = Math.round(level.bitrate / 1000); | |
| this.elements.bitrateStat.textContent = this.state.stats.bitrate; | |
| } | |
| }); | |
| } 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.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| }); | |
| } else { | |
| this.showLoading(false); | |
| this.showNotification('Browser not supported', '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_ultra_favs', JSON.stringify(this.state.favorites)); | |
| // Refresh if in favorites filter | |
| if (this.state.currentFilter === 'fav') { | |
| this.filterChannels(); | |
| } | |
| } | |
| 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; | |
| } | |
| 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 | |
| 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}%`; | |
| this.state.stats.buffer = Math.round(bufferedEnd - current); | |
| this.elements.bufferStat.textContent = `${this.state.stats.buffer}s`; | |
| } | |
| } | |
| updateStatsLoop() { | |
| // Update health based on buffering | |
| if (this.state.hls && this.elements.video.buffered.length > 0) { | |
| const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1); | |
| const bufferTime = bufferedEnd - this.elements.video.currentTime; | |
| if (bufferTime > 5) { | |
| this.state.stats.health = 100; | |
| } else if (bufferTime > 2) { | |
| this.state.stats.health = 80; | |
| } else if (bufferTime > 0) { | |
| this.state.stats.health = 60; | |
| } else { | |
| this.state.stats.health = 40; | |
| } | |
| this.elements.healthStat.textContent = `${this.state.stats.health}%`; | |
| } | |
| // Continue loop | |
| setTimeout(() => this.updateStatsLoop(), 1000); | |
| } | |
| clearPlaylist() { | |
| this.state.channels = []; | |
| this.state.filteredChannels = []; | |
| this.state.currentChannel = null; | |
| this.state.currentFile = null; | |
| if (this.state.hls) { | |
| this.state.hls.destroy(); | |
| this.state.hls = null; | |
| } | |
| this.elements.video.pause(); | |
| this.elements.video.src = ''; | |
| this.elements.fileName.textContent = ''; | |
| this.elements.fileInput.value = ''; | |
| this.renderChannels(); | |
| this.elements.channelCount.textContent = '0'; | |
| this.updateStatus('Playlist cleared'); | |
| this.elements.currentChannel.textContent = 'Welcome to Clarke Ultra'; | |
| this.elements.currentRegion.textContent = 'TV'; | |
| this.elements.currentQuality.textContent = '4K READY'; | |
| this.showNotification('Playlist cleared', 'info'); | |
| } | |
| showLoading(show) { | |
| this.elements.loadingOverlay.style.display = show ? 'flex' : 'none'; | |
| } | |
| showSettings() { | |
| const settings = ` | |
| <div class="overlay-content" style="max-width: 500px;"> | |
| <div class="overlay-icon"> | |
| <i class="fas fa-cog"></i> | |
| </div> | |
| <h2 class="overlay-title">Settings</h2> | |
| <div style="text-align: left; margin-bottom: 2rem;"> | |
| <div style="margin-bottom: 1rem;"> | |
| <label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">Video Quality</label> | |
| <select style="width: 100%; padding: 0.75rem; background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary);"> | |
| <option>Auto</option> | |
| <option>1080p</option> | |
| <option>720p</option> | |
| <option>480p</option> | |
| </select> | |
| </div> | |
| <div style="margin-bottom: 1rem;"> | |
| <label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">Buffer Size</label> | |
| <select style="width: 100%; padding: 0.75rem; background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary);"> | |
| <option>30 seconds</option> | |
| <option>60 seconds</option> | |
| <option>90 seconds</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="overlay-buttons"> | |
| <button class="tv-btn tv-btn-primary" style="padding: 0.75rem 2rem;"> | |
| Save | |
| </button> | |
| <button class="tv-btn tv-btn-secondary" style="padding: 0.75rem 2rem;"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| this.showOverlay(settings); | |
| } | |
| showNotification(message, type = 'info') { | |
| // Create notification | |
| const notification = document.createElement('div'); | |
| notification.className = 'tv-overlay'; | |
| notification.style.display = 'flex'; | |
| notification.style.zIndex = '3000'; | |
| notification.style.animation = 'fadeIn 0.3s ease'; | |
| notification.innerHTML = ` | |
| <div class="overlay-content" style="max-width: 400px;"> | |
| <div class="overlay-icon"> | |
| <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> | |
| </div> | |
| <p class="overlay-message">${message}</p> | |
| <div class="overlay-buttons"> | |
| <button class="tv-btn tv-btn-primary" style="padding: 0.75rem 2rem;"> | |
| OK | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(notification); | |
| // Add click to close | |
| notification.querySelector('button').addEventListener('click', () => { | |
| notification.style.animation = 'fadeIn 0.3s ease reverse'; | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 300); | |
| }); | |
| // Auto-close after 3 seconds | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.style.animation = 'fadeIn 0.3s ease reverse'; | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 300); | |
| } | |
| }, 3000); | |
| } | |
| showOverlay(content) { | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'tv-overlay'; | |
| overlay.style.display = 'flex'; | |
| overlay.innerHTML = content; | |
| document.body.appendChild(overlay); | |
| // Add close button functionality | |
| overlay.addEventListener('click', (e) => { | |
| if (e.target === overlay || e.target.closest('.tv-btn-secondary')) { | |
| overlay.style.animation = 'fadeIn 0.3s ease reverse'; | |
| setTimeout(() => { | |
| if (overlay.parentNode) { | |
| overlay.parentNode.removeChild(overlay); | |
| } | |
| }, 300); | |
| } | |
| }); | |
| } | |
| } | |
| // Initialize Clarke Player Ultra | |
| let player; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| player = new ClarkePlayerUltra(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |