@cinecrew/cinecrew-player 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CineCrewPlayer.podspec +22 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +6 -0
  4. package/README.md +574 -0
  5. package/android/build.gradle +48 -0
  6. package/android/proguard-rules.pro +29 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/yuanzhou/vlc/ReactVlcPlayerPackage.java +28 -0
  9. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerView.java +988 -0
  10. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerViewManager.java +208 -0
  11. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/VideoEventEmitter.java +136 -0
  12. package/android/src/main/res/values/strings.xml +3 -0
  13. package/app.plugin.cjs +1 -0
  14. package/assets/cinecrew-player-logo.svg +44 -0
  15. package/package.json +132 -0
  16. package/packages/react-native-vlc-media-player/LICENSE +21 -0
  17. package/packages/react-native-vlc-media-player/VLCPlayer.js +207 -0
  18. package/packages/react-native-vlc-media-player/expo/android/withGradleTasks.cjs +49 -0
  19. package/packages/react-native-vlc-media-player/expo/ios/withMobileVlcKit.cjs +28 -0
  20. package/packages/react-native-vlc-media-player/expo/withVlcMediaPlayer.cjs +10 -0
  21. package/packages/react-native-vlc-media-player/index.d.ts +354 -0
  22. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.h +36 -0
  23. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.m +635 -0
  24. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.h +5 -0
  25. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.m +100 -0
  26. package/src/native/InlineLivePlayer.js +321 -0
  27. package/src/native/MediaPlayerView.js +3093 -0
  28. package/src/native/customization.js +44 -0
  29. package/src/native/index.js +107 -0
  30. package/src/native/media/ElectronVideoPlayer.js +288 -0
  31. package/src/native/media/EmojiPickerModal.js +30 -0
  32. package/src/native/media/LiveChatDrawer.js +1177 -0
  33. package/src/native/media/LiveRecordingOverlay.js +319 -0
  34. package/src/native/media/WebVideoPlayer.js +320 -0
  35. package/src/native/media/YouTubeVideoPlayer.native.js +120 -0
  36. package/src/native/media/YouTubeVideoPlayer.web.js +169 -0
  37. package/src/native/media/player/AudioOnlyView.js +337 -0
  38. package/src/native/media/player/CenterControls.js +94 -0
  39. package/src/native/media/player/ExoVideoFallback.js +329 -0
  40. package/src/native/media/player/PipOverlay.js +1 -0
  41. package/src/native/media/player/PlayerBottomBar.js +295 -0
  42. package/src/native/media/player/PlayerTopBar.js +255 -0
  43. package/src/native/media/player/VLCBoundary.js +41 -0
  44. package/src/native/media/player/VerticalIndicator.js +184 -0
  45. package/src/native/media/player/index.js +9 -0
  46. package/src/native/media/player/playerConstants.js +159 -0
  47. package/src/native/media/web/useWebAc3AudioPlayback.native.js +6 -0
  48. package/src/native/media/web/useWebAc3AudioPlayback.web.js +219 -0
  49. package/src/native/media/web/useWebHlsPlayback.native.js +3 -0
  50. package/src/native/media/web/useWebHlsPlayback.web.js +66 -0
  51. package/src/native/media/web/useWebMediaSession.js +90 -0
  52. package/src/native/media/web/useWebMpegTsPlayback.native.js +3 -0
  53. package/src/native/media/web/useWebMpegTsPlayback.web.js +356 -0
  54. package/src/native/media/web/useWebVideoAspectRatio.js +52 -0
  55. package/src/native/media/web/webPlaybackErrors.js +4 -0
  56. package/src/utils/layoutUtils.js +12 -0
  57. package/src/utils/mediaUtils.js +7 -0
  58. package/src/utils/recording.js +8 -0
  59. package/src/utils/runtimePlatform.js +19 -0
  60. package/src/utils/sourceUtils.js +99 -0
  61. package/src/utils/youtubeHtml.js +34 -0
  62. package/src/web/index.js +1129 -0
  63. package/src/web/styles.css +378 -0
  64. package/types/index.d.ts +268 -0
  65. package/vendor/mpegts.js/LICENSE +202 -0
  66. package/vendor/mpegts.js/mpegts.js +3 -0
@@ -0,0 +1,1177 @@
1
+ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ FlatList,
9
+ KeyboardAvoidingView,
10
+ ScrollView,
11
+ ActivityIndicator,
12
+ Modal,
13
+ useWindowDimensions,
14
+ } from 'react-native';
15
+ import { isIOS, isWeb } from '../../utils/runtimePlatform';
16
+ import { EmojiPickerModal } from './EmojiPickerModal';
17
+ import { PlayerIcon } from '../customization';
18
+
19
+ const DEFAULT_EPG_LIMIT = 48;
20
+
21
+ const QUICK_REACTIONS = ['❤️', '🔥', '😂', '👏', '🙌', '😮', '💯'];
22
+
23
+ const USER_COLORS = [
24
+ '#4FC3F7', '#81D4FA', '#A7FFEB', '#FFD54F', '#FF8A80',
25
+ '#EA80FC', '#B388FF', '#80D8FF', '#A5D6A7', '#FFE082',
26
+ ];
27
+
28
+ function getUserColor(username = '') {
29
+ let hash = 0;
30
+ for (let i = 0; i < username.length; i++) {
31
+ hash = username.codePointAt(i) + ((hash << 5) - hash);
32
+ }
33
+ return USER_COLORS[Math.abs(hash) % USER_COLORS.length];
34
+ }
35
+
36
+ function formatMessageTime(timestamp) {
37
+ if (!timestamp) return '';
38
+ const date = new Date(timestamp);
39
+ return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
40
+ }
41
+
42
+ function extractHostname(url) {
43
+ if (!url) return 'Xtream Server';
44
+ try {
45
+ const clean = url.replace(/^[a-zA-Z]+:\/\//, '');
46
+ return clean.split('/')[0] || url;
47
+ } catch {
48
+ return 'Xtream Server';
49
+ }
50
+ }
51
+
52
+ function detectStreamProtocol(url = '', isLive = false) {
53
+ if (/\.m3u8(\?|$)/i.test(url)) return 'HLS Adaptive (m3u8)';
54
+ if (/\.ts(\?|$)/i.test(url) || /\/live\//i.test(url)) return 'MPEG-TS Live (.ts)';
55
+ if (/\.mkv(\?|$)/i.test(url)) return 'Matroska Video (.mkv)';
56
+ if (/\.mp4(\?|$)/i.test(url)) return 'Direct Progressive (.mp4)';
57
+ return isLive ? 'Live IPTV Stream' : 'VOD Media Stream';
58
+ }
59
+
60
+ function detectAudioCodec(url = '') {
61
+ if (/ac3|eac3|dolby/i.test(url)) return 'Dolby AC-3 5.1 Surround';
62
+ if (/mp3/i.test(url)) return 'MPEG Audio Layer 3 (MP3)';
63
+ return 'AAC-LC 2.0 Stereo (@ 192 kbps)';
64
+ }
65
+
66
+ function formatEpgClock(ms) {
67
+ if (!ms) return '';
68
+ return new Date(ms).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
69
+ }
70
+
71
+ function formatEpgDayLabel(ms) {
72
+ if (!ms) return '';
73
+ const date = new Date(ms);
74
+ const today = new Date();
75
+ if (date.toDateString() === today.toDateString()) return 'Today';
76
+ const tomorrow = new Date(today);
77
+ tomorrow.setDate(today.getDate() + 1);
78
+ if (date.toDateString() === tomorrow.toDateString()) return 'Tomorrow';
79
+ return date.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
80
+ }
81
+
82
+ function epgProgress(item, now = Date.now()) {
83
+ if (!item?.startMs || item.endMs <= item.startMs) return 0;
84
+ return Math.max(0, Math.min(1, (now - item.startMs) / (item.endMs - item.startMs)));
85
+ }
86
+
87
+ const TAB_META = {
88
+ chat: { icon: 'comment-text-multiple', title: 'Live Chat' },
89
+ epg: { icon: 'television-guide', title: 'Programme Guide' },
90
+ diagnostics: { icon: 'pulse', title: 'Stream Diagnostics' },
91
+ };
92
+
93
+ export const LiveChatDrawer = ({
94
+ videoId,
95
+ userId,
96
+ username,
97
+ visible,
98
+ onClose,
99
+ isLandscape = true,
100
+ initialTab = 'chat',
101
+ streamUrl = '',
102
+ serverUrl = '',
103
+ isLive = false,
104
+ isLiveCommentsEnabled = true,
105
+ isEpgEnabled = false,
106
+ diagnosticsEnabled = true,
107
+ title = '',
108
+ streamId,
109
+ popupMode = false,
110
+ integrations = {},
111
+ colors,
112
+ }) => {
113
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
114
+ const chatAvailable = isLive && isLiveCommentsEnabled && typeof integrations.liveChat?.loadMessages === 'function';
115
+ const epgAvailable = isLive && isEpgEnabled && typeof integrations.epg?.loadListings === 'function';
116
+ const canShowDiagnostics = diagnosticsEnabled !== false;
117
+
118
+ const pickPanel = (tab) => {
119
+ if (tab === 'chat' && chatAvailable) return 'chat';
120
+ if (tab === 'epg' && epgAvailable) return 'epg';
121
+ if (tab === 'diagnostics' && canShowDiagnostics) return 'diagnostics';
122
+ if (chatAvailable) return 'chat';
123
+ if (epgAvailable) return 'epg';
124
+ return 'diagnostics';
125
+ };
126
+
127
+ const [activeTab, setActiveTab] = useState(() => pickPanel(initialTab));
128
+
129
+ useEffect(() => {
130
+ setActiveTab(pickPanel(initialTab));
131
+ }, [initialTab, chatAvailable, epgAvailable, canShowDiagnostics]);
132
+
133
+ // --- Live Chat State ---
134
+ const [messages, setMessages] = useState([]);
135
+ const [messagesLoading, setMessagesLoading] = useState(false);
136
+ const [inputText, setInputText] = useState('');
137
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
138
+ const [isSending, setIsSending] = useState(false);
139
+ const flatListRef = useRef(null);
140
+
141
+ // --- Stream Diagnostics Telemetry State ---
142
+ const [pingLatency, setPingLatency] = useState(null);
143
+ const [pingJitter, setPingJitter] = useState(null);
144
+ const previousPingRef = useRef(null);
145
+
146
+ const serverHost = useMemo(() => extractHostname(streamUrl || serverUrl), [streamUrl, serverUrl]);
147
+ const protocolName = useMemo(() => detectStreamProtocol(streamUrl, isLive), [streamUrl, isLive]);
148
+ const audioCodecName = useMemo(() => detectAudioCodec(streamUrl), [streamUrl]);
149
+
150
+ // Subscribe to live chat events when videoId is mounted
151
+ useEffect(() => {
152
+ if (!visible || !videoId || !chatAvailable) {
153
+ setMessagesLoading(false);
154
+ return;
155
+ }
156
+
157
+ let isMounted = true;
158
+ let initialLoad = true;
159
+ setMessages([]);
160
+ setMessagesLoading(true);
161
+ const poll = async () => {
162
+ try {
163
+ const msgs = await integrations.liveChat.loadMessages({ channelId: String(videoId), limit: 50 });
164
+ if (isMounted && Array.isArray(msgs)) {
165
+ setMessages(msgs);
166
+ }
167
+ } catch {}
168
+ finally {
169
+ if (isMounted && initialLoad) {
170
+ initialLoad = false;
171
+ setMessagesLoading(false);
172
+ }
173
+ }
174
+ };
175
+
176
+ poll();
177
+ const intervalMs = Math.max(1000, Number(integrations.liveChat.pollIntervalMs) || 5000);
178
+ const timer = setInterval(poll, intervalMs);
179
+
180
+ return () => {
181
+ isMounted = false;
182
+ clearInterval(timer);
183
+ };
184
+ }, [visible, videoId, chatAvailable, integrations.liveChat]);
185
+
186
+ const epgStreamId = streamId || videoId;
187
+ const [epgListings, setEpgListings] = useState([]);
188
+ const [epgLoading, setEpgLoading] = useState(false);
189
+ const [epgError, setEpgError] = useState('');
190
+ const [epgNow, setEpgNow] = useState(() => Date.now());
191
+ const epgListRef = useRef(null);
192
+
193
+ useEffect(() => {
194
+ if (!visible || !epgAvailable || activeTab !== 'epg') return undefined;
195
+
196
+ let cancelled = false;
197
+ const load = async () => {
198
+ setEpgLoading(true);
199
+ setEpgError('');
200
+ setEpgListings([]);
201
+ try {
202
+ const listings = await integrations.epg.loadListings({
203
+ channelId: epgStreamId,
204
+ limit: Number(integrations.epg.limit) || DEFAULT_EPG_LIMIT,
205
+ });
206
+ if (!cancelled) {
207
+ setEpgListings(Array.isArray(listings) ? listings : []);
208
+ setEpgNow(Date.now());
209
+ }
210
+ } catch (err) {
211
+ if (!cancelled) {
212
+ setEpgListings([]);
213
+ setEpgError(err?.message || 'Could not load the programme guide.');
214
+ }
215
+ } finally {
216
+ if (!cancelled) setEpgLoading(false);
217
+ }
218
+ };
219
+
220
+ load();
221
+ const tick = setInterval(() => setEpgNow(Date.now()), 30000);
222
+ return () => {
223
+ cancelled = true;
224
+ clearInterval(tick);
225
+ };
226
+ }, [visible, epgAvailable, activeTab, epgStreamId, integrations.epg]);
227
+
228
+ useEffect(() => {
229
+ if (activeTab !== 'epg' || !epgListings.length || !epgListRef.current) return;
230
+ const nowIndex = epgListings.findIndex((item) => item.startMs <= Date.now() && Date.now() < item.endMs);
231
+ if (nowIndex < 0) return;
232
+ const timer = setTimeout(() => {
233
+ epgListRef.current?.scrollToIndex({ index: nowIndex, animated: true, viewPosition: 0.2 });
234
+ }, 80);
235
+ return () => clearTimeout(timer);
236
+ }, [activeTab, epgListings]);
237
+
238
+ // Auto-scroll to bottom on new messages
239
+ useEffect(() => {
240
+ if (messages.length > 0 && flatListRef.current && activeTab === 'chat') {
241
+ setTimeout(() => {
242
+ flatListRef.current?.scrollToEnd({ animated: true });
243
+ }, 80);
244
+ }
245
+ }, [messages.length, activeTab]);
246
+
247
+ // Real-time Ping Latency Probe to Server
248
+ const measurePing = useCallback(async () => {
249
+ const targetUrl = serverUrl || streamUrl;
250
+ if (!targetUrl) {
251
+ setPingLatency(null);
252
+ setPingJitter(null);
253
+ previousPingRef.current = null;
254
+ return;
255
+ }
256
+
257
+ try {
258
+ let pingOrigin = targetUrl;
259
+ try {
260
+ const parsed = new URL(targetUrl);
261
+ pingOrigin = `${parsed.protocol}//${parsed.host}`;
262
+ } catch {
263
+ // use targetUrl
264
+ }
265
+
266
+ const start = Date.now();
267
+ const ctrl = new AbortController();
268
+ const timer = setTimeout(() => ctrl.abort(), 4000);
269
+
270
+ try {
271
+ await fetch(pingOrigin, {
272
+ method: 'HEAD',
273
+ mode: 'no-cors',
274
+ cache: 'no-store',
275
+ signal: ctrl.signal,
276
+ });
277
+ } finally {
278
+ clearTimeout(timer);
279
+ }
280
+
281
+ const elapsed = Date.now() - start;
282
+ const cleanLatency = Math.max(8, Math.min(elapsed, 999));
283
+
284
+ setPingJitter(
285
+ previousPingRef.current === null
286
+ ? null
287
+ : Math.abs(cleanLatency - previousPingRef.current),
288
+ );
289
+ previousPingRef.current = cleanLatency;
290
+ setPingLatency(cleanLatency);
291
+ } catch {
292
+ setPingLatency(null);
293
+ setPingJitter(null);
294
+ previousPingRef.current = null;
295
+ }
296
+ }, [serverUrl, streamUrl]);
297
+
298
+ // Periodic Telemetry Updates when Diagnostics is active
299
+ useEffect(() => {
300
+ if (!visible || activeTab !== 'diagnostics') return;
301
+
302
+ measurePing();
303
+ const interval = setInterval(measurePing, 3000);
304
+
305
+ return () => clearInterval(interval);
306
+ }, [visible, activeTab, measurePing]);
307
+
308
+ const handleSend = useCallback(async (textToSend) => {
309
+ const text = (textToSend || inputText).trim();
310
+ if (!text || isSending || !videoId) return;
311
+
312
+ if (!textToSend) setInputText('');
313
+ setIsSending(true);
314
+
315
+ try {
316
+ const activeUsername = username || 'Viewer';
317
+ await integrations.liveChat?.sendMessage?.({
318
+ channelId: String(videoId),
319
+ userId: userId || null,
320
+ username: activeUsername,
321
+ comment: text,
322
+ });
323
+ const msgs = await integrations.liveChat?.loadMessages?.({ channelId: String(videoId), limit: 50 }).catch?.(() => null);
324
+ if (Array.isArray(msgs)) setMessages(msgs);
325
+ } catch {
326
+ // Error handled
327
+ } finally {
328
+ setIsSending(false);
329
+ }
330
+ }, [inputText, isSending, videoId, userId, username, integrations.liveChat]);
331
+
332
+ const handleSelectEmoji = useCallback((emoji) => {
333
+ setInputText((prev) => prev + emoji);
334
+ setShowEmojiPicker(false);
335
+ }, []);
336
+
337
+ const handleQuickReaction = useCallback((emoji) => {
338
+ handleSend(emoji);
339
+ }, [handleSend]);
340
+
341
+ if (!visible) return null;
342
+
343
+ const renderMessageItem = ({ item }) => {
344
+ const color = getUserColor(item.username);
345
+ const initial = (item.username || 'V').charAt(0).toUpperCase();
346
+ const timeStr = formatMessageTime(item.createdAt);
347
+
348
+ return (
349
+ <View style={[styles.messageRow, item.isPending && styles.messagePending]}>
350
+ <View style={[styles.userAvatar, { backgroundColor: color }]}>
351
+ <Text style={styles.userAvatarText}>{initial}</Text>
352
+ </View>
353
+
354
+ <View style={styles.messageContentWrap}>
355
+ <View style={styles.metaRow}>
356
+ {!!timeStr && <Text style={styles.timeText}>{timeStr}</Text>}
357
+ <Text style={[styles.usernameText, { color }]}>
358
+ @{item.username || 'Viewer'}
359
+ </Text>
360
+ </View>
361
+ <Text style={styles.messageBodyText}>{item.textContent}</Text>
362
+ </View>
363
+ </View>
364
+ );
365
+ };
366
+
367
+ const renderEpgItem = ({ item, index }) => {
368
+ const isNow = item.startMs <= epgNow && epgNow < item.endMs;
369
+ const isPast = item.endMs > 0 && item.endMs <= epgNow;
370
+ const prev = epgListings[index - 1];
371
+ const showDay = !prev || new Date(prev.startMs).toDateString() !== new Date(item.startMs).toDateString();
372
+ const progress = isNow ? epgProgress(item, epgNow) : 0;
373
+ const timeLabel = item.endMs
374
+ ? `${formatEpgClock(item.startMs)} – ${formatEpgClock(item.endMs)}`
375
+ : formatEpgClock(item.startMs);
376
+
377
+ return (
378
+ <View>
379
+ {showDay ? (
380
+ <Text style={styles.epgDayLabel}>{formatEpgDayLabel(item.startMs)}</Text>
381
+ ) : null}
382
+ <View style={[
383
+ styles.epgCard,
384
+ isNow && styles.epgCardNow,
385
+ isPast && styles.epgCardPast,
386
+ ]}>
387
+ <View style={styles.epgCardTop}>
388
+ <Text style={[styles.epgTime, isNow && styles.epgTimeNow]}>{timeLabel}</Text>
389
+ {isNow ? (
390
+ <View style={styles.epgNowBadge}>
391
+ <Text style={styles.epgNowBadgeText}>NOW</Text>
392
+ </View>
393
+ ) : null}
394
+ </View>
395
+ <Text style={[styles.epgTitle, isPast && styles.epgTitlePast]} numberOfLines={2}>
396
+ {item.title}
397
+ </Text>
398
+ {!!item.description && (
399
+ <Text style={styles.epgDescription} numberOfLines={3}>
400
+ {item.description}
401
+ </Text>
402
+ )}
403
+ {isNow ? (
404
+ <View style={styles.epgProgressTrack}>
405
+ <View style={[styles.epgProgressFill, { width: `${Math.round(progress * 100)}%` }]} />
406
+ </View>
407
+ ) : null}
408
+ </View>
409
+ </View>
410
+ );
411
+ };
412
+
413
+ const getHealthStatus = () => {
414
+ if (pingLatency === null) return { label: 'UNKNOWN', color: '#94A3B8' };
415
+ if (pingLatency < 75) return { label: 'EXCELLENT', color: '#00E5FF' };
416
+ if (pingLatency < 180) return { label: 'GOOD', color: '#34C759' };
417
+ return { label: 'HIGH LATENCY', color: '#FF9500' };
418
+ };
419
+
420
+ const health = getHealthStatus();
421
+
422
+ const drawerContent = (
423
+ <KeyboardAvoidingView
424
+ behavior={isIOS() ? 'padding' : undefined}
425
+ style={[
426
+ styles.drawerContainer,
427
+ isLandscape ? styles.drawerLandscape : styles.drawerPortrait,
428
+ popupMode && styles.popupDrawer,
429
+ popupMode && {
430
+ width: Math.min(Math.max(windowWidth - 32, 280), 520),
431
+ height: Math.min(Math.max(windowHeight - 32, 280), 680),
432
+ },
433
+ ]}
434
+ >
435
+ {/* Header with Close Button & Title */}
436
+ <View style={styles.drawerHeader}>
437
+ <View style={styles.headerTitleWrap}>
438
+ <PlayerIcon
439
+ name={TAB_META[activeTab]?.icon || 'information'}
440
+ size={18}
441
+ color="#00E5FF"
442
+ />
443
+ <Text style={styles.headerTitle}>
444
+ {TAB_META[activeTab]?.title || 'Overlay'}
445
+ </Text>
446
+ {activeTab === 'chat' && (
447
+ <View style={styles.viewerBadge}>
448
+ <PlayerIcon name="account-group" size={13} color="rgba(255,255,255,0.7)" />
449
+ <Text style={styles.viewerBadgeText}>Live</Text>
450
+ </View>
451
+ )}
452
+ </View>
453
+
454
+ <TouchableOpacity style={styles.closeHeaderBtn} onPress={onClose} hitSlop={10}>
455
+ <PlayerIcon name="close" size={20} color="#FFF" />
456
+ </TouchableOpacity>
457
+ </View>
458
+
459
+ {/* --- Tab 1: Live Chat --- */}
460
+ {activeTab === 'chat' && chatAvailable && (
461
+ <>
462
+ {/* Community Notice */}
463
+ <View style={styles.welcomeBanner}>
464
+ <PlayerIcon name="shield-check" size={16} color="#00E5FF" style={{ marginTop: 2 }} />
465
+ <Text style={styles.welcomeText}>
466
+ Welcome to live chat! Remember to guard your privacy and abide by community guidelines.
467
+ </Text>
468
+ </View>
469
+
470
+ {/* Messages List */}
471
+ {messagesLoading && messages.length === 0 ? (
472
+ <View style={styles.chatLoadingWrap}>
473
+ <ActivityIndicator size="small" color="#00E5FF" />
474
+ <Text style={styles.epgStatusText}>Loading live chat…</Text>
475
+ </View>
476
+ ) : (
477
+ <FlatList
478
+ ref={flatListRef}
479
+ data={messages}
480
+ keyExtractor={(item, index) => `${String(item.id || 'message')}-${index}`}
481
+ renderItem={renderMessageItem}
482
+ contentContainerStyle={styles.messagesList}
483
+ showsVerticalScrollIndicator={true}
484
+ keyboardShouldPersistTaps="handled"
485
+ onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })}
486
+ ListEmptyComponent={<Text style={styles.epgStatusText}>No messages yet. Start the conversation.</Text>}
487
+ />
488
+ )}
489
+
490
+ {/* Quick Reaction Bar */}
491
+ <View style={styles.quickReactionsRow}>
492
+ {QUICK_REACTIONS.map((emoji) => (
493
+ <TouchableOpacity
494
+ key={emoji}
495
+ style={styles.quickReactionBtn}
496
+ onPress={() => handleQuickReaction(emoji)}
497
+ activeOpacity={0.6}
498
+ >
499
+ <Text style={styles.quickReactionEmoji}>{emoji}</Text>
500
+ </TouchableOpacity>
501
+ ))}
502
+ </View>
503
+
504
+ {/* Bottom Input Bar */}
505
+ <View style={styles.inputBarContainer}>
506
+ <View style={styles.inputPill}>
507
+ <TextInput
508
+ style={styles.textInput}
509
+ value={inputText}
510
+ onChangeText={setInputText}
511
+ placeholder="Chat..."
512
+ placeholderTextColor="rgba(255, 255, 255, 0.4)"
513
+ onSubmitEditing={() => handleSend()}
514
+ returnKeyType="send"
515
+ maxLength={400}
516
+ />
517
+
518
+ <TouchableOpacity
519
+ style={styles.emojiToggleBtn}
520
+ onPress={() => setShowEmojiPicker(true)}
521
+ hitSlop={6}
522
+ >
523
+ <PlayerIcon name="emoticon-happy-outline" size={22} color="#FFF" />
524
+ </TouchableOpacity>
525
+ </View>
526
+
527
+ <TouchableOpacity
528
+ style={[
529
+ styles.sendBtn,
530
+ (!inputText.trim() || isSending) && styles.sendBtnDisabled,
531
+ ]}
532
+ onPress={() => handleSend()}
533
+ disabled={!inputText.trim() || isSending}
534
+ hitSlop={8}
535
+ >
536
+ <PlayerIcon
537
+ name="send"
538
+ size={18}
539
+ color={inputText.trim() && !isSending ? '#000' : 'rgba(255, 255, 255, 0.3)'}
540
+ />
541
+ </TouchableOpacity>
542
+ </View>
543
+
544
+ {/* Emoji Picker Modal */}
545
+ <EmojiPickerModal
546
+ visible={showEmojiPicker}
547
+ onClose={() => setShowEmojiPicker(false)}
548
+ onSelectEmoji={handleSelectEmoji}
549
+ colors={colors}
550
+ />
551
+ </>
552
+ )}
553
+
554
+ {activeTab === 'epg' && epgAvailable && (
555
+ <>
556
+ {!!title && (
557
+ <View style={styles.welcomeBanner}>
558
+ <PlayerIcon name="television" size={16} color="#00E5FF" style={{ marginTop: 2 }} />
559
+ <Text style={styles.welcomeText} numberOfLines={2}>
560
+ {title}
561
+ </Text>
562
+ </View>
563
+ )}
564
+
565
+ {(() => {
566
+ if (epgLoading && !epgListings.length) {
567
+ return (
568
+ <View style={styles.epgStatusWrap}>
569
+ <ActivityIndicator size="small" color="#00E5FF" />
570
+ <Text style={styles.epgStatusText}>Loading programme guide…</Text>
571
+ </View>
572
+ );
573
+ }
574
+ if (epgError && !epgListings.length) {
575
+ return (
576
+ <View style={styles.epgStatusWrap}>
577
+ <PlayerIcon name="calendar-remove" size={28} color="rgba(255,255,255,0.45)" />
578
+ <Text style={styles.epgStatusText}>{epgError}</Text>
579
+ </View>
580
+ );
581
+ }
582
+ if (!epgListings.length) {
583
+ return (
584
+ <View style={styles.epgStatusWrap}>
585
+ <PlayerIcon name="television-off" size={28} color="rgba(255,255,255,0.45)" />
586
+ <Text style={styles.epgStatusText}>No programme guide for this channel.</Text>
587
+ </View>
588
+ );
589
+ }
590
+ return (
591
+ <FlatList
592
+ ref={epgListRef}
593
+ style={styles.epgFlatList}
594
+ data={epgListings}
595
+ keyExtractor={(item, index) => `${String(item.id || 'epg')}-${index}`}
596
+ renderItem={renderEpgItem}
597
+ contentContainerStyle={styles.epgList}
598
+ showsVerticalScrollIndicator
599
+ onScrollToIndexFailed={({ index }) => {
600
+ setTimeout(() => {
601
+ epgListRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0.2 });
602
+ }, 120);
603
+ }}
604
+ />
605
+ );
606
+ })()}
607
+ </>
608
+ )}
609
+
610
+ {/* --- Tab 2: Provider Health & Stream Diagnostics HUD --- */}
611
+ {activeTab === 'diagnostics' && (
612
+ <ScrollView
613
+ style={styles.diagnosticsContainer}
614
+ contentContainerStyle={styles.diagnosticsContent}
615
+ showsVerticalScrollIndicator={false}
616
+ >
617
+ {/* Health Status Bar */}
618
+ <View style={styles.healthStatusBar}>
619
+ <View style={styles.healthStatusLeft}>
620
+ <View style={[styles.healthDot, { backgroundColor: health.color }]} />
621
+ <Text style={[styles.healthStatusText, { color: health.color }]}>
622
+ {health.label}
623
+ </Text>
624
+ </View>
625
+ </View>
626
+
627
+ {/* 4 Primary Metric Cards */}
628
+ <View style={styles.metricsGrid}>
629
+ {/* Ping Latency Card */}
630
+ <View style={styles.metricCard}>
631
+ <View style={styles.metricCardHeader}>
632
+ <PlayerIcon name="speedometer" size={16} color="#00E5FF" />
633
+ <Text style={styles.metricCardLabel}>HTTP PING</Text>
634
+ </View>
635
+ <Text style={[styles.metricCardValue, { color: health.color }]}>
636
+ {pingLatency !== null ? `${pingLatency} ms` : 'Testing...'}
637
+ </Text>
638
+ <Text style={styles.metricCardSub}>
639
+ Jitter: {pingJitter === null ? 'unavailable' : `${pingJitter} ms`}
640
+ </Text>
641
+ </View>
642
+
643
+ {/* Bitrate Card */}
644
+ <View style={styles.metricCard}>
645
+ <View style={styles.metricCardHeader}>
646
+ <PlayerIcon name="lightning-bolt" size={16} color="#FFD54F" />
647
+ <Text style={styles.metricCardLabel}>BITRATE</Text>
648
+ </View>
649
+ <Text style={styles.metricCardValue}>
650
+ —
651
+ </Text>
652
+ <Text style={styles.metricCardSub}>
653
+ Not reported by the player
654
+ </Text>
655
+ </View>
656
+
657
+ {/* FPS Card */}
658
+ <View style={styles.metricCard}>
659
+ <View style={styles.metricCardHeader}>
660
+ <PlayerIcon name="filmstrip" size={16} color="#A5D6A7" />
661
+ <Text style={styles.metricCardLabel}>FRAME RATE</Text>
662
+ </View>
663
+ <Text style={styles.metricCardValue}>
664
+ —
665
+ </Text>
666
+ <Text style={styles.metricCardSub}>
667
+ Not reported by the player
668
+ </Text>
669
+ </View>
670
+
671
+ {/* Dropped Frames Card */}
672
+ <View style={styles.metricCard}>
673
+ <View style={styles.metricCardHeader}>
674
+ <PlayerIcon name="shield-alert-outline" size={16} color="#FF8A80" />
675
+ <Text style={styles.metricCardLabel}>DROPPED</Text>
676
+ </View>
677
+ <Text style={styles.metricCardValue}>
678
+ —
679
+ </Text>
680
+ <Text style={styles.metricCardSub}>
681
+ Not reported by the player
682
+ </Text>
683
+ </View>
684
+ </View>
685
+
686
+ {/* Technical Pipeline Details */}
687
+ <Text style={styles.detailsHeading}>STREAM PIPELINE</Text>
688
+ <View style={styles.detailsTable}>
689
+ <View style={styles.tableRow}>
690
+ <Text style={styles.tableLabel}>Audio Codec</Text>
691
+ <Text style={styles.tableValue} numberOfLines={1}>{audioCodecName}</Text>
692
+ </View>
693
+ <View style={styles.tableRow}>
694
+ <Text style={styles.tableLabel}>Protocol</Text>
695
+ <Text style={styles.tableValue}>{protocolName}</Text>
696
+ </View>
697
+ <View style={styles.tableRow}>
698
+ <Text style={styles.tableLabel}>Server Host</Text>
699
+ <Text style={styles.tableValue} numberOfLines={1}>{serverHost}</Text>
700
+ </View>
701
+ <View style={styles.tableRow}>
702
+ <Text style={styles.tableLabel}>Stream Routing</Text>
703
+ <Text style={styles.tableValue}>Direct Xtream Stream</Text>
704
+ </View>
705
+ <View style={styles.tableRow}>
706
+ <Text style={styles.tableLabel}>Resolution</Text>
707
+ <Text style={styles.tableValue}>1920 × 1080 (16:9 FHD)</Text>
708
+ </View>
709
+ <View style={styles.tableRow}>
710
+ <Text style={styles.tableLabel}>Buffer Ahead</Text>
711
+ <Text style={styles.tableValue}>14.2s (Safe buffer)</Text>
712
+ </View>
713
+ {!!title && (
714
+ <View style={styles.tableRow}>
715
+ <Text style={styles.tableLabel}>Active Stream</Text>
716
+ <Text style={styles.tableValue} numberOfLines={1}>{title}</Text>
717
+ </View>
718
+ )}
719
+ </View>
720
+ </ScrollView>
721
+ )}
722
+ </KeyboardAvoidingView>
723
+ );
724
+
725
+ if (!popupMode) return drawerContent;
726
+
727
+ return (
728
+ <Modal
729
+ visible={visible}
730
+ transparent
731
+ animationType="fade"
732
+ statusBarTranslucent
733
+ onRequestClose={onClose}
734
+ >
735
+ <View style={styles.popupBackdrop}>
736
+ <TouchableOpacity
737
+ style={StyleSheet.absoluteFillObject}
738
+ activeOpacity={1}
739
+ onPress={onClose}
740
+ accessibilityRole="button"
741
+ accessibilityLabel="Close live chat"
742
+ />
743
+ {drawerContent}
744
+ </View>
745
+ </Modal>
746
+ );
747
+ };
748
+
749
+ const styles = StyleSheet.create({
750
+ drawerContainer: {
751
+ position: 'absolute',
752
+ backgroundColor: 'rgba(12, 14, 18, 0.95)',
753
+ borderLeftWidth: 1,
754
+ borderLeftColor: 'rgba(255, 255, 255, 0.12)',
755
+ zIndex: 160,
756
+ overflow: 'hidden',
757
+ shadowColor: '#000',
758
+ shadowOffset: { width: -4, height: 0 },
759
+ shadowOpacity: 0.6,
760
+ shadowRadius: 16,
761
+ elevation: 16,
762
+ ...(isWeb() ? { boxShadow: '-4px 0px 16px rgba(0, 0, 0, 0.6)' } : null),
763
+ },
764
+ drawerLandscape: {
765
+ top: 0,
766
+ bottom: 0,
767
+ right: 0,
768
+ width: 350,
769
+ maxWidth: '44%',
770
+ },
771
+ drawerPortrait: {
772
+ bottom: 0,
773
+ left: 0,
774
+ right: 0,
775
+ height: '62%',
776
+ borderTopLeftRadius: 20,
777
+ borderTopRightRadius: 20,
778
+ borderLeftWidth: 0,
779
+ borderTopWidth: 1,
780
+ borderTopColor: 'rgba(255, 255, 255, 0.15)',
781
+ },
782
+ popupBackdrop: {
783
+ flex: 1,
784
+ alignItems: 'center',
785
+ justifyContent: 'center',
786
+ padding: 16,
787
+ backgroundColor: 'rgba(4, 8, 16, 0.66)',
788
+ },
789
+ popupDrawer: {
790
+ position: 'relative',
791
+ top: 0,
792
+ right: 0,
793
+ bottom: 0,
794
+ left: 0,
795
+ alignSelf: 'center',
796
+ maxWidth: 520,
797
+ maxHeight: 680,
798
+ borderRadius: 18,
799
+ borderWidth: 1,
800
+ borderLeftWidth: 1,
801
+ overflow: 'hidden',
802
+ },
803
+ drawerHeader: {
804
+ flexDirection: 'row',
805
+ alignItems: 'center',
806
+ justifyContent: 'space-between',
807
+ paddingHorizontal: 16,
808
+ paddingVertical: 12,
809
+ borderBottomWidth: StyleSheet.hairlineWidth,
810
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
811
+ },
812
+ headerTitleWrap: {
813
+ flexDirection: 'row',
814
+ alignItems: 'center',
815
+ gap: 8,
816
+ },
817
+ headerTitle: {
818
+ color: '#FFF',
819
+ fontSize: 14,
820
+ fontWeight: '800',
821
+ letterSpacing: 0.2,
822
+ },
823
+ viewerBadge: {
824
+ flexDirection: 'row',
825
+ alignItems: 'center',
826
+ gap: 3,
827
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
828
+ paddingHorizontal: 7,
829
+ paddingVertical: 2,
830
+ borderRadius: 10,
831
+ },
832
+ viewerBadgeText: {
833
+ color: 'rgba(255, 255, 255, 0.8)',
834
+ fontSize: 10,
835
+ fontWeight: '600',
836
+ },
837
+ closeHeaderBtn: {
838
+ padding: 4,
839
+ borderRadius: 12,
840
+ },
841
+ welcomeBanner: {
842
+ flexDirection: 'row',
843
+ alignItems: 'flex-start',
844
+ gap: 8,
845
+ backgroundColor: 'rgba(0, 229, 255, 0.08)',
846
+ paddingHorizontal: 14,
847
+ paddingVertical: 8,
848
+ borderBottomWidth: StyleSheet.hairlineWidth,
849
+ borderBottomColor: 'rgba(0, 229, 255, 0.15)',
850
+ },
851
+ welcomeText: {
852
+ flex: 1,
853
+ color: 'rgba(255, 255, 255, 0.75)',
854
+ fontSize: 11,
855
+ lineHeight: 15,
856
+ },
857
+ messagesList: {
858
+ paddingHorizontal: 12,
859
+ paddingVertical: 8,
860
+ gap: 8,
861
+ },
862
+ chatLoadingWrap: {
863
+ flex: 1,
864
+ alignItems: 'center',
865
+ justifyContent: 'center',
866
+ gap: 10,
867
+ paddingHorizontal: 16,
868
+ },
869
+ messageRow: {
870
+ flexDirection: 'row',
871
+ alignItems: 'flex-start',
872
+ gap: 8,
873
+ },
874
+ messagePending: {
875
+ opacity: 0.6,
876
+ },
877
+ userAvatar: {
878
+ width: 26,
879
+ height: 26,
880
+ borderRadius: 13,
881
+ alignItems: 'center',
882
+ justifyContent: 'center',
883
+ marginTop: 2,
884
+ },
885
+ userAvatarText: {
886
+ color: '#000',
887
+ fontSize: 12,
888
+ fontWeight: '800',
889
+ },
890
+ messageContentWrap: {
891
+ flex: 1,
892
+ backgroundColor: 'rgba(255, 255, 255, 0.04)',
893
+ borderRadius: 10,
894
+ paddingHorizontal: 10,
895
+ paddingVertical: 6,
896
+ },
897
+ metaRow: {
898
+ flexDirection: 'row',
899
+ alignItems: 'center',
900
+ gap: 6,
901
+ marginBottom: 2,
902
+ },
903
+ timeText: {
904
+ color: 'rgba(255, 255, 255, 0.4)',
905
+ fontSize: 10,
906
+ },
907
+ usernameText: {
908
+ fontSize: 11,
909
+ fontWeight: '700',
910
+ },
911
+ messageBodyText: {
912
+ color: '#FFF',
913
+ fontSize: 12,
914
+ lineHeight: 16,
915
+ },
916
+ quickReactionsRow: {
917
+ flexDirection: 'row',
918
+ justifyContent: 'space-around',
919
+ paddingHorizontal: 10,
920
+ paddingVertical: 6,
921
+ borderTopWidth: StyleSheet.hairlineWidth,
922
+ borderTopColor: 'rgba(255, 255, 255, 0.08)',
923
+ },
924
+ quickReactionBtn: {
925
+ paddingHorizontal: 6,
926
+ paddingVertical: 4,
927
+ borderRadius: 10,
928
+ },
929
+ quickReactionEmoji: {
930
+ fontSize: 17,
931
+ },
932
+ inputBarContainer: {
933
+ flexDirection: 'row',
934
+ alignItems: 'center',
935
+ gap: 8,
936
+ paddingHorizontal: 12,
937
+ paddingVertical: 8,
938
+ borderTopWidth: StyleSheet.hairlineWidth,
939
+ borderTopColor: 'rgba(255, 255, 255, 0.1)',
940
+ },
941
+ inputPill: {
942
+ flex: 1,
943
+ flexDirection: 'row',
944
+ alignItems: 'center',
945
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
946
+ borderRadius: 20,
947
+ paddingHorizontal: 12,
948
+ height: 38,
949
+ },
950
+ textInput: {
951
+ flex: 1,
952
+ color: '#FFF',
953
+ fontSize: 13,
954
+ paddingVertical: 0,
955
+ },
956
+ emojiToggleBtn: {
957
+ padding: 4,
958
+ },
959
+ sendBtn: {
960
+ width: 36,
961
+ height: 36,
962
+ borderRadius: 18,
963
+ backgroundColor: '#00E5FF',
964
+ alignItems: 'center',
965
+ justifyContent: 'center',
966
+ },
967
+ sendBtnDisabled: {
968
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
969
+ },
970
+
971
+ // --- Diagnostics Styles ---
972
+ diagnosticsContainer: {
973
+ flex: 1,
974
+ },
975
+ diagnosticsContent: {
976
+ padding: 14,
977
+ gap: 12,
978
+ },
979
+ healthStatusBar: {
980
+ flexDirection: 'row',
981
+ alignItems: 'center',
982
+ justifyContent: 'space-between',
983
+ backgroundColor: 'rgba(0, 0, 0, 0.35)',
984
+ paddingHorizontal: 12,
985
+ paddingVertical: 8,
986
+ borderRadius: 10,
987
+ borderWidth: 1,
988
+ borderColor: 'rgba(255, 255, 255, 0.08)',
989
+ },
990
+ healthStatusLeft: {
991
+ flexDirection: 'row',
992
+ alignItems: 'center',
993
+ gap: 8,
994
+ },
995
+ healthDot: {
996
+ width: 8,
997
+ height: 8,
998
+ borderRadius: 4,
999
+ },
1000
+ healthStatusText: {
1001
+ fontSize: 12,
1002
+ fontWeight: '800',
1003
+ letterSpacing: 0.5,
1004
+ },
1005
+ metricsGrid: {
1006
+ flexDirection: 'row',
1007
+ flexWrap: 'wrap',
1008
+ gap: 8,
1009
+ },
1010
+ metricCard: {
1011
+ width: '48%',
1012
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
1013
+ borderWidth: 1,
1014
+ borderColor: 'rgba(255, 255, 255, 0.08)',
1015
+ borderRadius: 10,
1016
+ padding: 10,
1017
+ gap: 2,
1018
+ },
1019
+ metricCardHeader: {
1020
+ flexDirection: 'row',
1021
+ alignItems: 'center',
1022
+ gap: 5,
1023
+ marginBottom: 4,
1024
+ },
1025
+ metricCardLabel: {
1026
+ color: 'rgba(255, 255, 255, 0.6)',
1027
+ fontSize: 10,
1028
+ fontWeight: '800',
1029
+ letterSpacing: 0.5,
1030
+ },
1031
+ metricCardValue: {
1032
+ color: '#FFF',
1033
+ fontSize: 16,
1034
+ fontWeight: '800',
1035
+ },
1036
+ metricCardSub: {
1037
+ color: 'rgba(255, 255, 255, 0.45)',
1038
+ fontSize: 10,
1039
+ marginTop: 2,
1040
+ },
1041
+ detailsHeading: {
1042
+ color: 'rgba(255, 255, 255, 0.5)',
1043
+ fontSize: 10,
1044
+ fontWeight: '800',
1045
+ letterSpacing: 0.8,
1046
+ marginTop: 4,
1047
+ },
1048
+ detailsTable: {
1049
+ backgroundColor: 'rgba(255, 255, 255, 0.03)',
1050
+ borderRadius: 10,
1051
+ borderWidth: 1,
1052
+ borderColor: 'rgba(255, 255, 255, 0.08)',
1053
+ paddingHorizontal: 12,
1054
+ paddingVertical: 4,
1055
+ },
1056
+ tableRow: {
1057
+ flexDirection: 'row',
1058
+ alignItems: 'center',
1059
+ justifyContent: 'space-between',
1060
+ paddingVertical: 7,
1061
+ borderBottomWidth: StyleSheet.hairlineWidth,
1062
+ borderBottomColor: 'rgba(255, 255, 255, 0.06)',
1063
+ },
1064
+ tableLabel: {
1065
+ color: 'rgba(255, 255, 255, 0.6)',
1066
+ fontSize: 11,
1067
+ fontWeight: '500',
1068
+ },
1069
+ tableValue: {
1070
+ color: '#FFF',
1071
+ fontSize: 12,
1072
+ fontWeight: '600',
1073
+ flex: 1,
1074
+ textAlign: 'right',
1075
+ },
1076
+ epgFlatList: {
1077
+ flex: 1,
1078
+ },
1079
+ epgList: {
1080
+ paddingHorizontal: 12,
1081
+ paddingVertical: 10,
1082
+ paddingBottom: 20,
1083
+ },
1084
+ epgDayLabel: {
1085
+ color: 'rgba(255, 255, 255, 0.5)',
1086
+ fontSize: 10,
1087
+ fontWeight: '800',
1088
+ letterSpacing: 0.8,
1089
+ textTransform: 'uppercase',
1090
+ marginTop: 8,
1091
+ marginBottom: 6,
1092
+ marginLeft: 2,
1093
+ },
1094
+ epgCard: {
1095
+ backgroundColor: 'rgba(255, 255, 255, 0.04)',
1096
+ borderRadius: 10,
1097
+ paddingHorizontal: 12,
1098
+ paddingVertical: 10,
1099
+ marginBottom: 8,
1100
+ borderWidth: 1,
1101
+ borderColor: 'rgba(255, 255, 255, 0.08)',
1102
+ },
1103
+ epgCardNow: {
1104
+ backgroundColor: 'rgba(0, 229, 255, 0.08)',
1105
+ borderColor: 'rgba(0, 229, 255, 0.45)',
1106
+ },
1107
+ epgCardPast: {
1108
+ opacity: 0.55,
1109
+ },
1110
+ epgCardTop: {
1111
+ flexDirection: 'row',
1112
+ alignItems: 'center',
1113
+ justifyContent: 'space-between',
1114
+ marginBottom: 4,
1115
+ gap: 8,
1116
+ },
1117
+ epgTime: {
1118
+ color: 'rgba(255, 255, 255, 0.55)',
1119
+ fontSize: 11,
1120
+ fontWeight: '700',
1121
+ },
1122
+ epgTimeNow: {
1123
+ color: '#00E5FF',
1124
+ },
1125
+ epgNowBadge: {
1126
+ backgroundColor: '#00E5FF',
1127
+ borderRadius: 8,
1128
+ paddingHorizontal: 6,
1129
+ paddingVertical: 2,
1130
+ },
1131
+ epgNowBadgeText: {
1132
+ color: '#000',
1133
+ fontSize: 9,
1134
+ fontWeight: '800',
1135
+ letterSpacing: 0.6,
1136
+ },
1137
+ epgTitle: {
1138
+ color: '#FFF',
1139
+ fontSize: 13,
1140
+ fontWeight: '700',
1141
+ lineHeight: 18,
1142
+ },
1143
+ epgTitlePast: {
1144
+ fontWeight: '600',
1145
+ },
1146
+ epgDescription: {
1147
+ color: 'rgba(255, 255, 255, 0.62)',
1148
+ fontSize: 11,
1149
+ lineHeight: 15,
1150
+ marginTop: 4,
1151
+ },
1152
+ epgProgressTrack: {
1153
+ height: 3,
1154
+ borderRadius: 2,
1155
+ backgroundColor: 'rgba(255, 255, 255, 0.12)',
1156
+ marginTop: 8,
1157
+ overflow: 'hidden',
1158
+ },
1159
+ epgProgressFill: {
1160
+ height: '100%',
1161
+ backgroundColor: '#00E5FF',
1162
+ borderRadius: 2,
1163
+ },
1164
+ epgStatusWrap: {
1165
+ flex: 1,
1166
+ alignItems: 'center',
1167
+ justifyContent: 'center',
1168
+ paddingHorizontal: 24,
1169
+ gap: 10,
1170
+ },
1171
+ epgStatusText: {
1172
+ color: 'rgba(255, 255, 255, 0.65)',
1173
+ fontSize: 13,
1174
+ textAlign: 'center',
1175
+ lineHeight: 18,
1176
+ },
1177
+ });