mega-stream-proxy 0.1.9

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/android/build.gradle +31 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/dist/Player/native.d.ts +3 -0
  6. package/dist/Player/native.js +3 -0
  7. package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.d.ts +47 -0
  8. package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +624 -0
  9. package/dist/Player/playerAndroid/PlayerControlsAndroid.d.ts +54 -0
  10. package/dist/Player/playerAndroid/PlayerControlsAndroid.js +370 -0
  11. package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +21 -0
  12. package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +55 -0
  13. package/dist/Player/playerAndroid/index.d.ts +3 -0
  14. package/dist/Player/playerAndroid/index.js +3 -0
  15. package/dist/Player/playerweb/MegaVideoPlayer.d.ts +92 -0
  16. package/dist/Player/playerweb/MegaVideoPlayer.js +368 -0
  17. package/dist/Player/playerweb/PlayerControls.d.ts +52 -0
  18. package/dist/Player/playerweb/PlayerControls.js +346 -0
  19. package/dist/Player/playerweb/SubtitleDisplay.d.ts +15 -0
  20. package/dist/Player/playerweb/SubtitleDisplay.js +32 -0
  21. package/dist/Player/playerweb/index.d.ts +3 -0
  22. package/dist/Player/playerweb/index.js +3 -0
  23. package/dist/Player/web.d.ts +3 -0
  24. package/dist/Player/web.js +3 -0
  25. package/dist/accounts/async-storage.d.ts +10 -0
  26. package/dist/accounts/async-storage.js +46 -0
  27. package/dist/accounts/index.d.ts +4 -0
  28. package/dist/accounts/index.js +3 -0
  29. package/dist/accounts/local-storage.d.ts +3 -0
  30. package/dist/accounts/local-storage.js +63 -0
  31. package/dist/accounts/manager.d.ts +39 -0
  32. package/dist/accounts/manager.js +149 -0
  33. package/dist/accounts/types.d.ts +35 -0
  34. package/dist/accounts/types.js +1 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +4 -0
  37. package/dist/megaFile.d.ts +7 -0
  38. package/dist/megaFile.js +31 -0
  39. package/dist/proxy/native.d.ts +25 -0
  40. package/dist/proxy/native.js +336 -0
  41. package/dist/proxy/sw/bundle.d.ts +21 -0
  42. package/dist/proxy/sw/bundle.js +32 -0
  43. package/dist/proxy/sw/megajs.browser.js +27 -0
  44. package/dist/proxy/sw/sw.js +212 -0
  45. package/dist/proxy/types.d.ts +15 -0
  46. package/dist/proxy/types.js +2 -0
  47. package/dist/proxy/web.d.ts +24 -0
  48. package/dist/proxy/web.js +231 -0
  49. package/dist/ranges.d.ts +30 -0
  50. package/dist/ranges.js +63 -0
  51. package/dist/stream.d.ts +9 -0
  52. package/dist/stream.js +100 -0
  53. package/dist/subtitles/index.d.ts +9 -0
  54. package/dist/subtitles/index.js +2 -0
  55. package/dist/subtitles/load.d.ts +18 -0
  56. package/dist/subtitles/load.js +58 -0
  57. package/dist/subtitles/parse.d.ts +19 -0
  58. package/dist/subtitles/parse.js +95 -0
  59. package/dist/swDefine.d.ts +5 -0
  60. package/dist/swDefine.js +5 -0
  61. package/package.json +112 -0
  62. package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +825 -0
  63. package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +824 -0
  64. package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +80 -0
  65. package/src/Player/playerAndroid/index.ts +7 -0
  66. package/src/Player/playerweb/MegaVideoPlayer.tsx +551 -0
  67. package/src/Player/playerweb/PlayerControls.tsx +798 -0
  68. package/src/Player/playerweb/SubtitleDisplay.tsx +48 -0
  69. package/src/Player/playerweb/index.ts +3 -0
  70. package/src/accounts/async-storage.ts +55 -0
  71. package/src/accounts/index.ts +9 -0
  72. package/src/accounts/local-storage.ts +66 -0
  73. package/src/accounts/manager.ts +174 -0
  74. package/src/accounts/types.ts +38 -0
  75. package/src/index.ts +10 -0
  76. package/src/megaFile.ts +48 -0
  77. package/src/proxy/native.ts +396 -0
  78. package/src/proxy/sw/bundle.ts +37 -0
  79. package/src/proxy/sw/megajs.browser.js +27 -0
  80. package/src/proxy/sw/sw.js +212 -0
  81. package/src/proxy/types.ts +17 -0
  82. package/src/proxy/web.ts +266 -0
  83. package/src/ranges.ts +72 -0
  84. package/src/stream.ts +117 -0
  85. package/src/subtitles/index.ts +15 -0
  86. package/src/subtitles/load.ts +83 -0
  87. package/src/subtitles/parse.ts +105 -0
  88. package/src/swDefine.ts +5 -0
@@ -0,0 +1,798 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ PanResponder,
5
+ StyleSheet,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ } from "react-native";
10
+ import { Ionicons } from "@expo/vector-icons";
11
+ import { SubtitleDisplayWeb } from "./SubtitleDisplay.js";
12
+
13
+ export type PlayerControlsWebProps = {
14
+ playing: boolean;
15
+ buffering: boolean;
16
+ currentTime: number;
17
+ duration: number;
18
+ muted: boolean;
19
+ title?: string;
20
+ visible: boolean;
21
+ subtitlesOn: boolean;
22
+ volume: number;
23
+ /** subtitle language labels (when >1 the CC switcher is shown) */
24
+ subtitleTracks?: string[];
25
+ /** currently active subtitle language */
26
+ subtitleTrack?: string;
27
+ /** select a subtitle language from the list directly */
28
+ onSelectSubtitleTrack?: (lang: string) => void;
29
+ /** current subtitle font size (px) */
30
+ subtitleFontSize?: number;
31
+ /** adjust the subtitle font size (px) */
32
+ onChangeSubtitleFontSize?: (px: number) => void;
33
+ /** 0..1 — how much of the whole merged episode has been downloaded. Renders
34
+ * a second, thinner line under the playhead track. */
35
+ loadProgress?: number;
36
+ /** [start, end] seconds (merged timeline) of the intro — highlighted on the
37
+ * track + shows a "Skip Intro" button while the playhead is inside it. */
38
+ introRegion?: [number, number];
39
+ /** [start, end] seconds (merged timeline) of the outro */
40
+ outroRegion?: [number, number];
41
+ onSkipIntro?: () => void;
42
+ onSkipOutro?: () => void;
43
+ /** resolution labels in display order — the FIRST is the default source
44
+ * (the episode URL), e.g. ["Default", "1080p", "720p", "480p"]. */
45
+ resolutions?: string[];
46
+ /** currently active resolution label */
47
+ resolution?: string;
48
+ width?: number;
49
+ height?: number;
50
+ cue?: any;
51
+ enabled?: any;
52
+ fontSize?: any;
53
+ /** select a resolution — the parent resolves the label to its URL */
54
+ onSelectResolution?: (label: string) => void;
55
+ onToggleVisible: () => void;
56
+ onPlayPause: () => void;
57
+ onSkip: (deltaSeconds: number) => void;
58
+ onSeek: (seconds: number) => void;
59
+ onToggleMute: () => void;
60
+ onToggleSubtitles: () => void;
61
+ onVolume: (v: number) => void;
62
+ onFullscreen: () => void;
63
+ };
64
+
65
+ const fmt = (s: number) => {
66
+ if (!Number.isFinite(s) || s < 0) s = 0;
67
+ const h = Math.floor(s / 3600);
68
+ const m = Math.floor((s % 3600) / 60);
69
+ const sec = Math.floor(s % 60);
70
+ return `${h.toString().padStart(2, "0")}:${m
71
+ .toString()
72
+ .padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
73
+ };
74
+
75
+ const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
76
+
77
+ export function PlayerControlsWeb(props: PlayerControlsWebProps) {
78
+ const {
79
+ playing,
80
+ buffering,
81
+ currentTime,
82
+ duration,
83
+ muted,
84
+ title,
85
+ visible,
86
+ subtitlesOn,
87
+ volume,
88
+ subtitleTracks,
89
+ subtitleTrack,
90
+ onSelectSubtitleTrack,
91
+ subtitleFontSize,
92
+ onChangeSubtitleFontSize,
93
+ loadProgress,
94
+ introRegion,
95
+ outroRegion,
96
+ onSkipIntro,
97
+ onSkipOutro,
98
+ resolutions,
99
+ resolution,
100
+ onSelectResolution,
101
+ onToggleVisible,
102
+ onPlayPause,
103
+ onSkip,
104
+ onSeek,
105
+ onToggleMute,
106
+ onToggleSubtitles,
107
+ onVolume,
108
+ onFullscreen,
109
+ width,
110
+ height,
111
+ cue,
112
+ enabled,
113
+ fontSize,
114
+ } = props;
115
+
116
+ const hasSubs = (subtitleTracks?.length ?? 0) > 0;
117
+ const activeRes = resolution ?? resolutions?.[0] ?? "";
118
+
119
+ const [trackW, setTrackW] = useState(0);
120
+ const [scrubPct, setScrubPct] = useState<number | null>(null);
121
+ const [volW, setVolW] = useState(0);
122
+ const [sizeW, setSizeW] = useState(0);
123
+ const [settingsOpen, setSettingsOpen] = useState(false);
124
+
125
+ const pct = duration > 0 ? clamp01(currentTime / duration) : 0;
126
+ const volPct = clamp01(volume);
127
+ const inIntro =
128
+ !!introRegion &&
129
+ introRegion[1] > introRegion[0] &&
130
+ currentTime >= introRegion[0] &&
131
+ currentTime <= introRegion[1] &&
132
+ !!onSkipIntro;
133
+ const inOutro =
134
+ !!outroRegion &&
135
+ outroRegion[1] > outroRegion[0] &&
136
+ currentTime >= outroRegion[0] &&
137
+ currentTime <= outroRegion[1] &&
138
+ !!onSkipOutro;
139
+
140
+ const pan = useMemo(
141
+ () =>
142
+ PanResponder.create({
143
+ onStartShouldSetPanResponder: () => true,
144
+ onMoveShouldSetPanResponder: () => true,
145
+ onPanResponderGrant: (e) =>
146
+ setScrubPct(
147
+ trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0,
148
+ ),
149
+ onPanResponderMove: (e) =>
150
+ setScrubPct(
151
+ trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0,
152
+ ),
153
+ onPanResponderRelease: (e) => {
154
+ const p = trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0;
155
+ setScrubPct(null);
156
+ onSeek(p * duration);
157
+ },
158
+ }),
159
+ [trackW, duration, onSeek],
160
+ );
161
+
162
+ const volPan = useMemo(
163
+ () =>
164
+ PanResponder.create({
165
+ onStartShouldSetPanResponder: () => true,
166
+ onMoveShouldSetPanResponder: () => true,
167
+ onPanResponderGrant: (e) =>
168
+ onVolume(volW > 0 ? clamp01(e.nativeEvent.locationX / volW) : 1),
169
+ onPanResponderMove: (e) =>
170
+ onVolume(volW > 0 ? clamp01(e.nativeEvent.locationX / volW) : 1),
171
+ }),
172
+ [volW, onVolume],
173
+ );
174
+
175
+ const SIZE_MIN = 10;
176
+ const SIZE_MAX = 34;
177
+ const currentSize = subtitleFontSize ?? 14;
178
+ const sizePct = clamp01((currentSize - SIZE_MIN) / (SIZE_MAX - SIZE_MIN));
179
+ const sizePan = useMemo(
180
+ () =>
181
+ PanResponder.create({
182
+ onStartShouldSetPanResponder: () => true,
183
+ onMoveShouldSetPanResponder: () => true,
184
+ onPanResponderGrant: (e) => {
185
+ if (!onChangeSubtitleFontSize) return;
186
+ const p = sizeW > 0 ? clamp01(e.nativeEvent.locationX / sizeW) : 0;
187
+ onChangeSubtitleFontSize?.(
188
+ Math.round(SIZE_MIN + p * (SIZE_MAX - SIZE_MIN)),
189
+ );
190
+ },
191
+ onPanResponderMove: (e) => {
192
+ if (!onChangeSubtitleFontSize) return;
193
+ const p = sizeW > 0 ? clamp01(e.nativeEvent.locationX / sizeW) : 0;
194
+ onChangeSubtitleFontSize?.(
195
+ Math.round(SIZE_MIN + p * (SIZE_MAX - SIZE_MIN)),
196
+ );
197
+ },
198
+ }),
199
+ [sizeW, onChangeSubtitleFontSize],
200
+ );
201
+
202
+ const shownPct = scrubPct ?? pct;
203
+ const volIcon =
204
+ muted || volume === 0
205
+ ? "volume-mute"
206
+ : volume < 0.5
207
+ ? "volume-low"
208
+ : "volume-high";
209
+
210
+ const showSubsSection = hasSubs || !!onChangeSubtitleFontSize;
211
+
212
+ return (
213
+ <View
214
+ style={[
215
+ styles.root,
216
+ StyleSheet.absoluteFill,
217
+ { width: width, height: height },
218
+ ]}
219
+ >
220
+ {/* tap surface toggles the chrome */}
221
+ <TouchableOpacity
222
+ style={StyleSheet.absoluteFill}
223
+ activeOpacity={1}
224
+ onPress={onToggleVisible}
225
+ />
226
+
227
+ {buffering && (
228
+ <View
229
+ style={[StyleSheet.absoluteFill, styles.centerBadge]}
230
+ pointerEvents="none"
231
+ >
232
+ <ActivityIndicator color="#e50914" size="large" />
233
+ </View>
234
+ )}
235
+
236
+ {visible && (
237
+ <View
238
+ style={[
239
+ StyleSheet.absoluteFill,
240
+ { width: "100%", height: "100%", position: "absolute" },
241
+ ]}
242
+ pointerEvents="box-none"
243
+ >
244
+ {/* top gradient-ish bar */}
245
+ <View style={[styles.bar, styles.top]} pointerEvents="box-none">
246
+ {!!title && (
247
+ <Text numberOfLines={1} style={styles.title}>
248
+ {title}
249
+ </Text>
250
+ )}
251
+ </View>
252
+
253
+ {/* center buttons */}
254
+ <View
255
+ style={[styles.centerRow, { flex: 1 }]}
256
+ pointerEvents="box-none"
257
+ >
258
+ <TouchableOpacity
259
+ style={styles.skipBtn}
260
+ onPress={() => onSkip(-10)}
261
+ >
262
+ <Ionicons name="play-back" size={30} color="#fff" />
263
+ </TouchableOpacity>
264
+ <TouchableOpacity style={styles.ppBtn} onPress={onPlayPause}>
265
+ <Ionicons
266
+ name={
267
+ buffering ? "ellipsis-horizontal" : playing ? "pause" : "play"
268
+ }
269
+ size={38}
270
+ color="#fff"
271
+ />
272
+ </TouchableOpacity>
273
+ <TouchableOpacity style={styles.skipBtn} onPress={() => onSkip(10)}>
274
+ <Ionicons name="play-forward" size={30} color="#fff" />
275
+ </TouchableOpacity>
276
+ </View>
277
+
278
+ {/* bottom bar: time — timeline — duration — settings — fullscreen */}
279
+ <View style={[styles.bar, styles.bottom]} pointerEvents="box-none">
280
+ <Text style={styles.time}>
281
+ {fmt(scrubPct !== null ? scrubPct * duration : currentTime)}
282
+ </Text>
283
+
284
+ <View
285
+ style={styles.trackWrap}
286
+ onLayout={(e) => setTrackW(e.nativeEvent.layout.width)}
287
+ {...pan.panHandlers}
288
+ >
289
+ <View style={styles.track}>
290
+ {loadProgress != null && loadProgress > 0 && (
291
+ <View
292
+ style={[
293
+ styles.loadLine,
294
+ { width: `${clamp01(loadProgress) * 100}%` },
295
+ ]}
296
+ />
297
+ )}
298
+ {introRegion &&
299
+ duration > 0 &&
300
+ introRegion[1] > introRegion[0] && (
301
+ <View
302
+ style={[
303
+ styles.introHighlight,
304
+ {
305
+ left: `${clamp01(introRegion[0] / duration) * 100}%`,
306
+ width: `${clamp01((introRegion[1] - introRegion[0]) / duration) * 100}%`,
307
+ },
308
+ ]}
309
+ />
310
+ )}
311
+ {outroRegion &&
312
+ duration > 0 &&
313
+ outroRegion[1] > outroRegion[0] && (
314
+ <View
315
+ style={[
316
+ styles.outroHighlight,
317
+ {
318
+ left: `${clamp01(outroRegion[0] / duration) * 100}%`,
319
+ width: `${clamp01((outroRegion[1] - outroRegion[0]) / duration) * 100}%`,
320
+ },
321
+ ]}
322
+ />
323
+ )}
324
+ <View style={[styles.fill, { width: `${shownPct * 100}%` }]} />
325
+ </View>
326
+ <View
327
+ style={[
328
+ styles.thumb,
329
+ { left: `${shownPct * 100}%` },
330
+ scrubPct === null && styles.thumbIdle,
331
+ ]}
332
+ />
333
+ </View>
334
+
335
+ <Text style={styles.time}>{fmt(duration)}</Text>
336
+
337
+ {/* settings dropdown: subtitles + volume + resolutions */}
338
+ <View style={styles.settingsWrap}>
339
+ <TouchableOpacity
340
+ style={[styles.iconBtn, settingsOpen && styles.iconBtnOn]}
341
+ onPress={() => setSettingsOpen((v) => !v)}
342
+ hitSlop={6}
343
+ >
344
+ <Ionicons
345
+ name="settings-outline"
346
+ size={17}
347
+ color={settingsOpen ? "#7dd3fc" : "#fff"}
348
+ />
349
+ </TouchableOpacity>
350
+
351
+ {settingsOpen ? (
352
+ <View style={styles.settingsMenu}>
353
+ <Text style={styles.settingsTitle}>Settings</Text>
354
+
355
+ {/* ---- Subtitles ---- */}
356
+ {showSubsSection ? (
357
+ <View style={styles.section}>
358
+ <View style={styles.sectionHead}>
359
+ <Ionicons
360
+ name="chatbox-ellipses-outline"
361
+ size={15}
362
+ color="#9aa0aa"
363
+ />
364
+ <Text style={styles.sectionLabel}>Subtitles</Text>
365
+ <TouchableOpacity
366
+ onPress={onToggleSubtitles}
367
+ hitSlop={6}
368
+ style={{ marginLeft: "auto" }}
369
+ >
370
+ <View
371
+ style={[
372
+ styles.switchTrack,
373
+ subtitlesOn && styles.switchTrackOn,
374
+ ]}
375
+ >
376
+ <View
377
+ style={[
378
+ styles.switchThumb,
379
+ subtitlesOn && styles.switchThumbOn,
380
+ ]}
381
+ />
382
+ </View>
383
+ </TouchableOpacity>
384
+ </View>
385
+
386
+ {hasSubs && (
387
+ <View style={styles.menuList}>
388
+ {subtitleTracks?.map((lang) => {
389
+ const active = lang === subtitleTrack;
390
+ return (
391
+ <TouchableOpacity
392
+ key={lang}
393
+ style={[
394
+ styles.menuItem,
395
+ active && styles.menuItemOn,
396
+ ]}
397
+ onPress={() => {
398
+ setSettingsOpen(false);
399
+ onSelectSubtitleTrack?.(lang);
400
+ }}
401
+ >
402
+ <Text
403
+ style={[
404
+ styles.menuItemText,
405
+ active && styles.menuItemTextOn,
406
+ ]}
407
+ >
408
+ {lang}
409
+ </Text>
410
+ {active ? (
411
+ <Ionicons
412
+ name="checkmark"
413
+ size={14}
414
+ color="#fff"
415
+ />
416
+ ) : null}
417
+ </TouchableOpacity>
418
+ );
419
+ })}
420
+ </View>
421
+ )}
422
+
423
+ {!!onChangeSubtitleFontSize && (
424
+ <View style={styles.sliderRow}>
425
+ <TouchableOpacity
426
+ style={styles.minusBtn}
427
+ onPress={() =>
428
+ onChangeSubtitleFontSize?.(
429
+ Math.max(
430
+ SIZE_MIN,
431
+ Math.round((subtitleFontSize ?? 14) - 2),
432
+ ),
433
+ )
434
+ }
435
+ >
436
+ <Ionicons name="remove" size={16} color="#fff" />
437
+ </TouchableOpacity>
438
+
439
+ <View
440
+ style={styles.sliderTrack}
441
+ onLayout={(e) =>
442
+ setSizeW(e.nativeEvent.layout.width)
443
+ }
444
+ {...sizePan.panHandlers}
445
+ >
446
+ <View
447
+ style={[
448
+ styles.sliderFill,
449
+ { width: `${sizePct * 100}%` },
450
+ ]}
451
+ />
452
+ </View>
453
+
454
+ <TouchableOpacity
455
+ style={styles.minusBtn}
456
+ onPress={() =>
457
+ onChangeSubtitleFontSize?.(
458
+ Math.min(
459
+ SIZE_MAX,
460
+ Math.round((subtitleFontSize ?? 14) + 2),
461
+ ),
462
+ )
463
+ }
464
+ >
465
+ <Ionicons name="add" size={16} color="#fff" />
466
+ </TouchableOpacity>
467
+ <Text style={styles.sliderValue}>
468
+ {currentSize}px
469
+ </Text>
470
+ </View>
471
+ )}
472
+ </View>
473
+ ) : null}
474
+
475
+ {/* ---- Volume ---- */}
476
+ <View style={styles.section}>
477
+ <View style={styles.sectionHead}>
478
+ <Ionicons
479
+ name="volume-medium-outline"
480
+ size={15}
481
+ color="#9aa0aa"
482
+ />
483
+ <Text style={styles.sectionLabel}>Volume</Text>
484
+ <TouchableOpacity
485
+ onPress={onToggleMute}
486
+ hitSlop={6}
487
+ style={{ marginLeft: "auto" }}
488
+ >
489
+ <Ionicons
490
+ name={volIcon as any}
491
+ size={17}
492
+ color={muted ? "#e50914" : "#fff"}
493
+ />
494
+ </TouchableOpacity>
495
+ <Text style={styles.sliderValue}>
496
+ {Math.round(volPct * 100)}
497
+ </Text>
498
+ </View>
499
+ <View style={styles.sliderRow}>
500
+ <View
501
+ style={styles.sliderTrack}
502
+ onLayout={(e) => setVolW(e.nativeEvent.layout.width)}
503
+ {...volPan.panHandlers}
504
+ >
505
+ <View
506
+ style={[
507
+ styles.sliderFill,
508
+ { width: `${volPct * 100}%` },
509
+ ]}
510
+ />
511
+ </View>
512
+ </View>
513
+ </View>
514
+
515
+ {/* ---- Resolution ---- */}
516
+ {(resolutions?.length ?? 0) > 0 ? (
517
+ <View style={styles.section}>
518
+ <View style={styles.sectionHead}>
519
+ <Ionicons
520
+ name="resize-outline"
521
+ size={15}
522
+ color="#9aa0aa"
523
+ />
524
+ <Text style={styles.sectionLabel}>Resolution</Text>
525
+ </View>
526
+ <View style={styles.menuList}>
527
+ {resolutions?.map((label) => {
528
+ const active = label === activeRes;
529
+ return (
530
+ <TouchableOpacity
531
+ key={label}
532
+ style={[
533
+ styles.menuItem,
534
+ active && styles.menuItemOn,
535
+ ]}
536
+ onPress={() => {
537
+ setSettingsOpen(false);
538
+ onSelectResolution?.(label);
539
+ }}
540
+ >
541
+ <Text
542
+ style={[
543
+ styles.menuItemText,
544
+ active && styles.menuItemTextOn,
545
+ ]}
546
+ >
547
+ {label}
548
+ </Text>
549
+ {active ? (
550
+ <Ionicons
551
+ name="checkmark"
552
+ size={14}
553
+ color="#fff"
554
+ />
555
+ ) : null}
556
+ </TouchableOpacity>
557
+ );
558
+ })}
559
+ </View>
560
+ </View>
561
+ ) : null}
562
+ </View>
563
+ ) : null}
564
+ </View>
565
+
566
+ <TouchableOpacity onPress={onFullscreen} hitSlop={8}>
567
+ <Ionicons name="expand" size={18} color="#fff" />
568
+ </TouchableOpacity>
569
+ </View>
570
+ </View>
571
+ )}
572
+ <SubtitleDisplayWeb
573
+ cue={cue}
574
+ enabled={enabled}
575
+ fontSize={fontSize}
576
+ />
577
+ {visible && inIntro && (
578
+ <TouchableOpacity style={styles.skipRegionBtn} onPress={onSkipIntro}>
579
+ <Text style={styles.skipRegionText}>Skip Intro ›</Text>
580
+ </TouchableOpacity>
581
+ )}
582
+ {visible && inOutro && (
583
+ <TouchableOpacity style={styles.skipRegionBtn} onPress={onSkipOutro}>
584
+ <Text style={styles.skipRegionText}>Skip Outro ›</Text>
585
+ </TouchableOpacity>
586
+ )}
587
+ </View>
588
+ );
589
+ }
590
+
591
+ const styles = StyleSheet.create({
592
+ root: { zIndex: 2 },
593
+
594
+ centerBadge: {
595
+ justifyContent: "center",
596
+ alignItems: "center",
597
+ width: "100%",
598
+ height: "100%",
599
+ },
600
+
601
+ bar: {
602
+ position: "absolute",
603
+ left: 0,
604
+ right: 0,
605
+ flexDirection: "row",
606
+ alignItems: "center",
607
+ paddingHorizontal: 12,
608
+ backgroundColor: "rgba(0,0,0,0.45)",
609
+ gap: 8,
610
+ },
611
+ top: { top: 0, height: 42 },
612
+ bottom: { bottom: 0, height: 44 },
613
+
614
+ title: { color: "#fff", fontWeight: "700", fontSize: 13, flex: 1 },
615
+
616
+ centerRow: {
617
+ justifyContent: "center",
618
+ alignItems: "center",
619
+ flexDirection: "row",
620
+ gap: 34,
621
+ },
622
+ ppBtn: {
623
+ width: 64,
624
+ height: 64,
625
+ borderRadius: 32,
626
+ backgroundColor: "rgba(229,9,20,0.92)",
627
+ justifyContent: "center",
628
+ alignItems: "center",
629
+ },
630
+ skipBtn: { opacity: 0.9 },
631
+
632
+ time: {
633
+ color: "#ddd",
634
+ fontSize: 11,
635
+ fontVariant: ["tabular-nums"],
636
+ minWidth: 58,
637
+ },
638
+ trackWrap: { flex: 1, paddingVertical: 12, justifyContent: "center" },
639
+ track: {
640
+ height: 4,
641
+ borderRadius: 2,
642
+ backgroundColor: "#3a3f47",
643
+ overflow: "hidden",
644
+ },
645
+ fill: { height: 4, borderRadius: 2, backgroundColor: "#e50914" },
646
+ loadLine: {
647
+ position: "absolute",
648
+ left: 0,
649
+ top: 0,
650
+ height: 4,
651
+ borderRadius: 2,
652
+ backgroundColor: "rgba(255,255,255,0.35)",
653
+ },
654
+ introHighlight: {
655
+ position: "absolute",
656
+ top: 0,
657
+ height: 4,
658
+ backgroundColor: "rgba(0,229,255,0.55)",
659
+ },
660
+ outroHighlight: {
661
+ position: "absolute",
662
+ top: 0,
663
+ height: 4,
664
+ backgroundColor: "rgba(255,0,200,0.55)",
665
+ },
666
+ skipRegionBtn: {
667
+ position: "absolute",
668
+ right: 16,
669
+ bottom: 60,
670
+ flexDirection: "row",
671
+ alignItems: "center",
672
+ backgroundColor: "rgba(0,0,0,0.6)",
673
+ borderWidth: 1,
674
+ borderColor: "rgba(255,255,255,0.25)",
675
+ borderRadius: 20,
676
+ paddingHorizontal: 14,
677
+ paddingVertical: 8,
678
+ },
679
+ skipRegionText: { color: "#fff", fontWeight: "700", fontSize: 13 },
680
+ thumb: {
681
+ position: "absolute",
682
+ top: 12 - 7 + 2,
683
+ marginLeft: -6,
684
+ width: 12,
685
+ height: 12,
686
+ borderRadius: 6,
687
+ backgroundColor: "#e50914",
688
+ },
689
+ thumbIdle: { opacity: 0.85 },
690
+
691
+ iconBtn: {
692
+ width: 26,
693
+ height: 26,
694
+ borderRadius: 4,
695
+ alignItems: "center",
696
+ justifyContent: "center",
697
+ },
698
+ iconBtnOn: { backgroundColor: "rgba(125,211,252,0.18)" },
699
+
700
+ settingsWrap: { position: "relative", zIndex: 20 },
701
+ settingsMenu: {
702
+ position: "absolute",
703
+ bottom: 30,
704
+ right: 0,
705
+ zIndex: 30,
706
+ width: 250,
707
+ maxHeight: 420,
708
+ backgroundColor: "#14161c",
709
+ borderWidth: 1,
710
+ borderColor: "#2a2f3a",
711
+ borderRadius: 8,
712
+ overflow: "hidden",
713
+ paddingBottom: 6,
714
+ elevation: 12,
715
+ shadowColor: "#000",
716
+ shadowOpacity: 0.5,
717
+ shadowRadius: 10,
718
+ shadowOffset: { width: 0, height: 3 },
719
+ },
720
+ settingsTitle: {
721
+ color: "#9aa0aa",
722
+ fontSize: 11,
723
+ fontWeight: "800",
724
+ textTransform: "uppercase",
725
+ letterSpacing: 1,
726
+ paddingHorizontal: 12,
727
+ paddingTop: 10,
728
+ paddingBottom: 6,
729
+ borderBottomWidth: 1,
730
+ borderBottomColor: "#262a34",
731
+ },
732
+
733
+ section: { paddingHorizontal: 6, paddingTop: 8 },
734
+ sectionHead: {
735
+ flexDirection: "row",
736
+ alignItems: "center",
737
+ gap: 6,
738
+ paddingHorizontal: 6,
739
+ paddingBottom: 2,
740
+ },
741
+ sectionLabel: { color: "#cbd5e1", fontSize: 12, fontWeight: "700" },
742
+
743
+ menuList: { paddingVertical: 4 },
744
+ menuItem: {
745
+ flexDirection: "row",
746
+ alignItems: "center",
747
+ justifyContent: "space-between",
748
+ gap: 10,
749
+ paddingHorizontal: 14,
750
+ paddingVertical: 6,
751
+ borderRadius: 6,
752
+ },
753
+ menuItemOn: { backgroundColor: "rgba(125,211,252,0.15)" },
754
+ menuItemText: { color: "#cbd5e1", fontSize: 13, fontWeight: "600" },
755
+ menuItemTextOn: { color: "#7dd3fc", fontWeight: "800" },
756
+
757
+ sliderRow: {
758
+ flexDirection: "row",
759
+ alignItems: "center",
760
+ gap: 10,
761
+ paddingHorizontal: 10,
762
+ paddingVertical: 8,
763
+ },
764
+ minusBtn: {
765
+ width: 24,
766
+ height: 24,
767
+ borderRadius: 4,
768
+ alignItems: "center",
769
+ justifyContent: "center",
770
+ backgroundColor: "#2a2f3a",
771
+ },
772
+ sliderTrack: { flex: 1, height: 22, justifyContent: "center" },
773
+ sliderFill: { height: 4, borderRadius: 2, backgroundColor: "#7dd3fc" },
774
+ sliderValue: {
775
+ color: "#9aa0aa",
776
+ fontSize: 11,
777
+ fontWeight: "700",
778
+ minWidth: 32,
779
+ textAlign: "right",
780
+ },
781
+
782
+ switchTrack: {
783
+ width: 34,
784
+ height: 20,
785
+ borderRadius: 10,
786
+ backgroundColor: "#3a3f47",
787
+ justifyContent: "center",
788
+ paddingHorizontal: 2,
789
+ },
790
+ switchTrackOn: { backgroundColor: "rgba(229,9,20,0.85)" },
791
+ switchThumb: {
792
+ width: 16,
793
+ height: 16,
794
+ borderRadius: 8,
795
+ backgroundColor: "#fff",
796
+ },
797
+ switchThumbOn: { alignSelf: "flex-end" },
798
+ });