@moviie/player-expo 0.4.0

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/app.plugin.cjs +3 -0
  4. package/dist/cast.cjs +100 -0
  5. package/dist/cast.cjs.map +1 -0
  6. package/dist/cast.d.cts +15 -0
  7. package/dist/cast.d.ts +15 -0
  8. package/dist/cast.mjs +92 -0
  9. package/dist/cast.mjs.map +1 -0
  10. package/dist/chunk-67DJ7NOB.mjs +294 -0
  11. package/dist/chunk-67DJ7NOB.mjs.map +1 -0
  12. package/dist/chunk-7U2LKIGU.mjs +12 -0
  13. package/dist/chunk-7U2LKIGU.mjs.map +1 -0
  14. package/dist/chunk-BJTO5JO5.mjs +10 -0
  15. package/dist/chunk-BJTO5JO5.mjs.map +1 -0
  16. package/dist/index.cjs +3934 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +450 -0
  19. package/dist/index.d.ts +450 -0
  20. package/dist/index.mjs +3571 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/layout.cjs +217 -0
  23. package/dist/layout.cjs.map +1 -0
  24. package/dist/layout.d.cts +20 -0
  25. package/dist/layout.d.ts +20 -0
  26. package/dist/layout.mjs +4 -0
  27. package/dist/layout.mjs.map +1 -0
  28. package/dist/moviie-cast-adapter-DmSU2u3j.d.cts +53 -0
  29. package/dist/moviie-cast-adapter-DmSU2u3j.d.ts +53 -0
  30. package/dist/plugin/with-moviie.cjs +88 -0
  31. package/dist/plugin/with-moviie.cjs.map +1 -0
  32. package/dist/plugin/with-moviie.d.cts +52 -0
  33. package/dist/plugin/with-moviie.d.ts +52 -0
  34. package/dist/plugin/with-moviie.mjs +76 -0
  35. package/dist/plugin/with-moviie.mjs.map +1 -0
  36. package/package.json +134 -0
  37. package/plugin/validate-options.ts +31 -0
  38. package/plugin/with-moviie-types.ts +21 -0
  39. package/plugin/with-moviie.ts +84 -0
  40. package/src/apply-expo-moviie-endpoints.ts +47 -0
  41. package/src/cast/google-cast-adapter.ts +111 -0
  42. package/src/cast/index.ts +12 -0
  43. package/src/components/controls/moviie-bottom-timeline.tsx +477 -0
  44. package/src/components/controls/moviie-cast-buttons.tsx +96 -0
  45. package/src/components/controls/moviie-chrome-edge-gradients.tsx +162 -0
  46. package/src/components/controls/moviie-controls.tsx +585 -0
  47. package/src/components/controls/moviie-skin-chrome-context.tsx +374 -0
  48. package/src/components/controls/moviie-skin-smart-progress.tsx +157 -0
  49. package/src/components/icons/embed-media-icons.tsx +282 -0
  50. package/src/components/icons/moviie-embed-brand-mark.tsx +58 -0
  51. package/src/components/moviie-error-boundary.tsx +80 -0
  52. package/src/components/moviie-player-error-shell.tsx +143 -0
  53. package/src/components/moviie-player-loading-shell.tsx +59 -0
  54. package/src/components/moviie-skin-custom-fullscreen-modal.tsx +232 -0
  55. package/src/components/moviie-video-props.ts +134 -0
  56. package/src/components/moviie-video.tsx +568 -0
  57. package/src/components/moviie-video.web.tsx +167 -0
  58. package/src/components/overlays/moviie-watermark.tsx +53 -0
  59. package/src/constants.ts +374 -0
  60. package/src/hooks/use-moviie-event.ts +103 -0
  61. package/src/hooks/use-moviie-playback-ended.ts +14 -0
  62. package/src/hooks/use-moviie-playback-resume-persistence.ts +76 -0
  63. package/src/hooks/use-moviie-playback.ts +99 -0
  64. package/src/hooks/use-moviie-player-types.ts +55 -0
  65. package/src/hooks/use-moviie-player.ts +236 -0
  66. package/src/hooks/use-moviie-player.web.ts +57 -0
  67. package/src/hooks/use-moviie-telemetry.ts +133 -0
  68. package/src/index.ts +90 -0
  69. package/src/layout.ts +4 -0
  70. package/src/lib/add-moviie-playback-ended-listener.ts +16 -0
  71. package/src/lib/build-moviie-branding-marketing-url.ts +14 -0
  72. package/src/lib/build-moviie-embed-brand-home-url.ts +16 -0
  73. package/src/lib/build-moviie-skin-layout-metrics.ts +166 -0
  74. package/src/lib/build-moviie-video-presentation.ts +42 -0
  75. package/src/lib/build-moviie-watch-embed-url.ts +30 -0
  76. package/src/lib/cast-adapter-registry.ts +13 -0
  77. package/src/lib/clamp-unit-interval.ts +3 -0
  78. package/src/lib/compute-custom-fullscreen-stage-dimensions.ts +39 -0
  79. package/src/lib/compute-moviie-playback-ended.ts +31 -0
  80. package/src/lib/compute-playback-buffering.ts +41 -0
  81. package/src/lib/compute-playback-progress-ratio.ts +33 -0
  82. package/src/lib/compute-timeline-scrub-commit-time.ts +40 -0
  83. package/src/lib/custom-fullscreen-native-orientation.ts +88 -0
  84. package/src/lib/format-playback-clock.ts +27 -0
  85. package/src/lib/jsx-native-bridge.ts +45 -0
  86. package/src/lib/map-smart-progress-display-ratio.ts +43 -0
  87. package/src/lib/moviie-cast-adapter.ts +53 -0
  88. package/src/lib/moviie-error-display.ts +149 -0
  89. package/src/lib/moviie-shell-tokens.ts +27 -0
  90. package/src/lib/moviie-telemetry-callbacks.ts +76 -0
  91. package/src/lib/optional-status-bar.ts +48 -0
  92. package/src/lib/partition-moviie-video-host-style.ts +54 -0
  93. package/src/lib/remember-playback-position-storage.ts +98 -0
  94. package/src/lib/resolve-moviie-skin-layout-scale.ts +17 -0
  95. package/src/lib/resolve-orientation-lock-for-rotate-z-deg.ts +21 -0
  96. package/src/lib/resolve-skin-accent-color.ts +10 -0
  97. package/src/lib/resolve-web-embed-iframe-src.ts +17 -0
  98. package/src/lib/warn-once.ts +23 -0
  99. package/src/platform/native-application-id.ts +10 -0
  100. package/src/platform/platform-client-info.ts +31 -0
  101. package/src/playback/fetch-playback-fresh.ts +27 -0
  102. package/src/playback/playback-memory-cache.ts +52 -0
  103. package/src/provider/moviie-provider.tsx +99 -0
  104. package/src/secure-store-viewer-token-store.ts +80 -0
@@ -0,0 +1,477 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from "react"
2
+ import { StyleSheet, View, type LayoutChangeEvent } from "react-native"
3
+ import { Gesture, GestureDetector } from "react-native-gesture-handler"
4
+ import {
5
+ runOnJS,
6
+ useDerivedValue,
7
+ useAnimatedStyle,
8
+ useSharedValue,
9
+ } from "react-native-reanimated"
10
+
11
+ import {
12
+ MOVIIE_SKIN_CHROME_FOREGROUND_HEX,
13
+ MOVIIE_SKIN_TIMELINE_BUFFER_LAYER_RGBA,
14
+ MOVIIE_SKIN_TIMELINE_RAIL_BACKGROUND_RGBA,
15
+ MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_BOTTOM_DP,
16
+ MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_HORIZONTAL_DP,
17
+ MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_TOP_DP,
18
+ MOVIIE_SKIN_TIMELINE_THUMB_BORDER_RGBA,
19
+ MOVIIE_SKIN_TIMELINE_THUMB_DRAG_DIAMETER_MULTIPLIER,
20
+ MOVIIE_SKIN_TIMELINE_TOUCH_EXPANSION_VERTICAL_DP,
21
+ } from "../../constants"
22
+ import type { MoviieSkinLayoutMetrics } from "../../lib/build-moviie-skin-layout-metrics"
23
+ import { clampUnitInterval } from "../../lib/clamp-unit-interval"
24
+ import { computePlaybackProgressRatio } from "../../lib/compute-playback-progress-ratio"
25
+ import { computeTimelineScrubCommitTime } from "../../lib/compute-timeline-scrub-commit-time"
26
+ import { ReanimatedView } from "../../lib/jsx-native-bridge"
27
+ import { resolveSkinAccentColor } from "../../lib/resolve-skin-accent-color"
28
+
29
+ import { useMoviieSkinChrome } from "./moviie-skin-chrome-context"
30
+
31
+ function createMoviieSkinBottomTimelineStyles(layout: MoviieSkinLayoutMetrics) {
32
+ return StyleSheet.create({
33
+ root: {
34
+ bottom: 0,
35
+ left: 0,
36
+ overflow: "visible",
37
+ pointerEvents: "box-none",
38
+ position: "absolute",
39
+ right: 0,
40
+ zIndex: 24,
41
+ },
42
+ touchExpansion: {
43
+ overflow: "visible",
44
+ paddingBottom: 0,
45
+ paddingTop:
46
+ layout.timelineTouchPaddingVerticalPx +
47
+ MOVIIE_SKIN_TIMELINE_TOUCH_EXPANSION_VERTICAL_DP,
48
+ position: "relative",
49
+ },
50
+ trackStack: {
51
+ overflow: "visible",
52
+ width: "100%",
53
+ },
54
+ thumbBase: {
55
+ borderColor: MOVIIE_SKIN_TIMELINE_THUMB_BORDER_RGBA,
56
+ borderWidth: 1,
57
+ elevation: 8,
58
+ position: "absolute",
59
+ zIndex: 10,
60
+ },
61
+ unifiedBarSlot: {
62
+ overflow: "visible",
63
+ position: "relative",
64
+ width: "100%",
65
+ },
66
+ railLayer: {
67
+ backgroundColor: MOVIIE_SKIN_TIMELINE_RAIL_BACKGROUND_RGBA,
68
+ bottom: 0,
69
+ height: layout.timelineBarHeightPx,
70
+ left: 0,
71
+ position: "absolute",
72
+ right: 0,
73
+ zIndex: 0,
74
+ },
75
+ bufferLayer: {
76
+ backgroundColor: MOVIIE_SKIN_TIMELINE_BUFFER_LAYER_RGBA,
77
+ bottom: 0,
78
+ height: layout.timelineBarHeightPx,
79
+ left: 0,
80
+ position: "absolute",
81
+ zIndex: 1,
82
+ },
83
+ progressLayer: {
84
+ bottom: 0,
85
+ height: layout.timelineBarHeightPx,
86
+ left: 0,
87
+ position: "absolute",
88
+ zIndex: 2,
89
+ },
90
+ })
91
+ }
92
+
93
+ export function MoviieSkinBottomTimeline() {
94
+ const {
95
+ playback,
96
+ chromeVisible,
97
+ layout,
98
+ player,
99
+ setTimelineScrubClockSeconds,
100
+ ui,
101
+ } = useMoviieSkinChrome()
102
+
103
+ const styles = useMemo(
104
+ () => createMoviieSkinBottomTimelineStyles(layout),
105
+ [layout]
106
+ )
107
+
108
+ const { controls } = playback
109
+ const accent = resolveSkinAccentColor(playback.branding.primaryColor)
110
+ const foreground = MOVIIE_SKIN_CHROME_FOREGROUND_HEX
111
+
112
+ const durationOk =
113
+ controls.showProgress &&
114
+ Number.isFinite(ui.duration) &&
115
+ ui.duration > 0 &&
116
+ !player.isLive
117
+
118
+ const progressRatioLive = computePlaybackProgressRatio({
119
+ currentTime: ui.currentTime,
120
+ duration: ui.duration,
121
+ durationOk,
122
+ playing: ui.playing,
123
+ })
124
+
125
+ const bufferRatioLive =
126
+ durationOk &&
127
+ Number.isFinite(ui.bufferedPosition) &&
128
+ ui.bufferedPosition >= 0
129
+ ? clampUnitInterval(ui.bufferedPosition / ui.duration)
130
+ : 0
131
+
132
+ // ── Shared values (UI thread) ─────────────────────────────────────────────
133
+ const isDraggingSV = useSharedValue(false)
134
+ const awaitingUnlockSV = useSharedValue(false)
135
+ const scrubRatioSV = useSharedValue(0)
136
+ const progressRatioSV = useSharedValue(progressRatioLive)
137
+ const bufferRatioSV = useSharedValue(bufferRatioLive)
138
+ const trackWidthSV = useSharedValue(0)
139
+ const durationSV = useSharedValue(ui.duration)
140
+ const chromeVisibleSV = useSharedValue(chromeVisible)
141
+ const timelineAccentEngagedSV = useSharedValue(false)
142
+ const thumbBaseDiamSV = useSharedValue(layout.timelineThumbDiameterPx)
143
+ const barHeightSV = useSharedValue(layout.timelineBarHeightPx)
144
+
145
+ // ── JS refs ────────────────────────────────────────────────────────────────
146
+ const isDraggingRef = useRef(false)
147
+ // shouldResumeRef permanece true até playingChange confirmar que o player retomou.
148
+ // Isso permite retry via statusChange caso player.play() seja chamado enquanto o seek
149
+ // ainda está em curso (expo-video pode ignorar play() durante loading).
150
+ const shouldResumeRef = useRef(false)
151
+ const progressRatioLiveRef = useRef(progressRatioLive)
152
+ progressRatioLiveRef.current = progressRatioLive
153
+ const chaptersRef = useRef(playback.chapters)
154
+ chaptersRef.current = playback.chapters
155
+ const chromeVisibleRef = useRef(chromeVisible)
156
+ chromeVisibleRef.current = chromeVisible
157
+
158
+ // ── Sync JS → shared values ────────────────────────────────────────────────
159
+ useEffect(() => {
160
+ if (!isDraggingRef.current) {
161
+ progressRatioSV.value = progressRatioLive
162
+ }
163
+ }, [progressRatioLive, progressRatioSV])
164
+
165
+ useEffect(() => {
166
+ bufferRatioSV.value = bufferRatioLive
167
+ }, [bufferRatioLive, bufferRatioSV])
168
+
169
+ useEffect(() => {
170
+ durationSV.value = ui.duration
171
+ }, [ui.duration, durationSV])
172
+
173
+ useEffect(() => {
174
+ thumbBaseDiamSV.value = layout.timelineThumbDiameterPx
175
+ barHeightSV.value = layout.timelineBarHeightPx
176
+ }, [
177
+ layout.timelineThumbDiameterPx,
178
+ layout.timelineBarHeightPx,
179
+ thumbBaseDiamSV,
180
+ barHeightSV,
181
+ ])
182
+
183
+ useEffect(() => {
184
+ chromeVisibleSV.value = chromeVisible
185
+ if (chromeVisible) {
186
+ timelineAccentEngagedSV.value = false
187
+ awaitingUnlockSV.value = false
188
+ }
189
+ }, [
190
+ chromeVisible,
191
+ chromeVisibleSV,
192
+ timelineAccentEngagedSV,
193
+ awaitingUnlockSV,
194
+ ])
195
+
196
+ // Retry em cada readyToPlay enquanto !dragging + shouldResume.
197
+ // Sem live seeks, este evento só pode vir do seek do commit → sem ambiguidade.
198
+ useEffect(() => {
199
+ const sub = player.addListener("statusChange", ({ status }) => {
200
+ if (shouldResumeRef.current && status === "readyToPlay" && !isDraggingRef.current) {
201
+ player.play()
202
+ }
203
+ })
204
+ return () => sub.remove()
205
+ }, [player])
206
+
207
+ // playingChange com isPlaying true é a confirmação autoritativa que o play() funcionou.
208
+ useEffect(() => {
209
+ const sub = player.addListener("playingChange", ({ isPlaying }) => {
210
+ if (isPlaying && !isDraggingRef.current) {
211
+ shouldResumeRef.current = false
212
+ }
213
+ })
214
+ return () => sub.remove()
215
+ }, [player])
216
+
217
+ // ── Derived values ─────────────────────────────────────────────────────────
218
+ const effectiveRatioSV = useDerivedValue(() =>
219
+ isDraggingSV.value ? scrubRatioSV.value : progressRatioSV.value
220
+ )
221
+
222
+ const showAccentSV = useDerivedValue(
223
+ () =>
224
+ chromeVisibleSV.value ||
225
+ isDraggingSV.value ||
226
+ timelineAccentEngagedSV.value
227
+ )
228
+
229
+ const showThumbSV = useDerivedValue(
230
+ () =>
231
+ chromeVisibleSV.value ||
232
+ isDraggingSV.value ||
233
+ timelineAccentEngagedSV.value
234
+ )
235
+
236
+ const thumbDiamSV = useDerivedValue(() =>
237
+ isDraggingSV.value
238
+ ? Math.round(
239
+ thumbBaseDiamSV.value * MOVIIE_SKIN_TIMELINE_THUMB_DRAG_DIAMETER_MULTIPLIER
240
+ )
241
+ : thumbBaseDiamSV.value
242
+ )
243
+
244
+ // ── Animated styles ────────────────────────────────────────────────────────
245
+ const progressBarStyle = useAnimatedStyle(() => ({
246
+ backgroundColor: showAccentSV.value ? accent : foreground,
247
+ width: `${effectiveRatioSV.value * 100}%`,
248
+ }))
249
+
250
+ const bufferBarStyle = useAnimatedStyle(() => ({
251
+ width: `${bufferRatioSV.value * 100}%`,
252
+ }))
253
+
254
+ const thumbStyle = useAnimatedStyle(() => {
255
+ if (!showThumbSV.value) {
256
+ return { opacity: 0 }
257
+ }
258
+ const diameter = thumbDiamSV.value
259
+ const radius = diameter / 2
260
+ const w = trackWidthSV.value
261
+ const left =
262
+ w > 0
263
+ ? Math.min(w - diameter, Math.max(0, effectiveRatioSV.value * w - radius))
264
+ : 0
265
+ const bh = barHeightSV.value
266
+ return {
267
+ opacity: 1,
268
+ backgroundColor: showAccentSV.value ? accent : foreground,
269
+ left,
270
+ width: diameter,
271
+ height: diameter,
272
+ borderRadius: radius,
273
+ bottom: bh / 2 - radius,
274
+ }
275
+ })
276
+
277
+ // ── JS callbacks ───────────────────────────────────────────────────────────
278
+ const onScrubBegin = useCallback(() => {
279
+ isDraggingRef.current = true
280
+ // Herda shouldResume de um commit anterior se o player ainda não retomou,
281
+ // senão usa o estado actual do player.
282
+ if (!shouldResumeRef.current) {
283
+ shouldResumeRef.current = player.playing
284
+ }
285
+ }, [player])
286
+
287
+ const onScrubUpdate = useCallback(
288
+ (ratio: number) => {
289
+ // Sem live seek: a barra anima via Reanimated (UI thread), o frame do vídeo
290
+ // só é actualizado no commit. Evita seeks concorrentes que interferem com o resume.
291
+ setTimelineScrubClockSeconds(ratio * durationSV.value)
292
+ },
293
+ [setTimelineScrubClockSeconds, durationSV]
294
+ )
295
+
296
+ const onScrubCommit = useCallback(
297
+ (ratio: number) => {
298
+ isDraggingRef.current = false
299
+ setTimelineScrubClockSeconds(null)
300
+ const dur = durationSV.value
301
+ if (!Number.isFinite(dur) || dur <= 0) return
302
+ const targetTime = computeTimelineScrubCommitTime({
303
+ ratio,
304
+ durationSeconds: dur,
305
+ chapters: chaptersRef.current,
306
+ })
307
+ player.currentTime = targetTime
308
+ // player.play() imediato pode ser ignorado se o seek ainda está em curso.
309
+ // O listener statusChange(readyToPlay) faz o retry enquanto shouldResumeRef=true.
310
+ // playingChange com isPlaying confirma e zera shouldResumeRef.
311
+ if (shouldResumeRef.current) {
312
+ player.play()
313
+ }
314
+ },
315
+ [setTimelineScrubClockSeconds, player, durationSV]
316
+ )
317
+
318
+ const onScrubCancel = useCallback(() => {
319
+ isDraggingRef.current = false
320
+ setTimelineScrubClockSeconds(null)
321
+ progressRatioSV.value = progressRatioLiveRef.current
322
+ // Cancel não faz seek — shouldResumeRef não muda.
323
+ // Se shouldResume=true (estava a tocar), o playingChange listener vai limpar quando
324
+ // o player retomar normalmente (ou o utilizador faz outro scrub).
325
+ }, [setTimelineScrubClockSeconds, progressRatioSV])
326
+
327
+ const onAccentEngage = useCallback(() => {
328
+ timelineAccentEngagedSV.value = true
329
+ }, [timelineAccentEngagedSV])
330
+
331
+ const onAccentDisengageCancel = useCallback(() => {
332
+ timelineAccentEngagedSV.value = false
333
+ }, [timelineAccentEngagedSV])
334
+
335
+ // ── Gesture ────────────────────────────────────────────────────────────────
336
+ // activeOffsetX: ativa só após 8 dp horizontal — evita conflito com ScrollView vertical.
337
+ // failOffsetY: cancela se o utilizador scrollar mais de 15 dp — devolve gesto ao ScrollView.
338
+ const panGesture = useMemo(
339
+ () =>
340
+ Gesture.Pan()
341
+ .activeOffsetX([-8, 8])
342
+ .failOffsetY([-15, 15])
343
+ .enabled(durationOk)
344
+ .onBegin(() => {
345
+ "worklet"
346
+ if (!chromeVisibleSV.value && !timelineAccentEngagedSV.value) {
347
+ awaitingUnlockSV.value = true
348
+ runOnJS(onAccentEngage)()
349
+ }
350
+ })
351
+ .onStart((e) => {
352
+ "worklet"
353
+ if (awaitingUnlockSV.value) {
354
+ // Primeira activação após accent-engage — aguarda onUpdate
355
+ return
356
+ }
357
+ isDraggingSV.value = true
358
+ runOnJS(onScrubBegin)()
359
+ const w = trackWidthSV.value
360
+ if (w <= 0) return
361
+ const ratio = Math.max(0, Math.min(1, e.x / w))
362
+ scrubRatioSV.value = ratio
363
+ runOnJS(onScrubUpdate)(ratio)
364
+ })
365
+ .onUpdate((e) => {
366
+ "worklet"
367
+ const w = trackWidthSV.value
368
+ if (w <= 0) return
369
+ const ratio = Math.max(0, Math.min(1, e.x / w))
370
+ if (awaitingUnlockSV.value) {
371
+ awaitingUnlockSV.value = false
372
+ isDraggingSV.value = true
373
+ runOnJS(onScrubBegin)()
374
+ }
375
+ if (!isDraggingSV.value) return
376
+ scrubRatioSV.value = ratio
377
+ runOnJS(onScrubUpdate)(ratio)
378
+ })
379
+ .onEnd(() => {
380
+ "worklet"
381
+ if (awaitingUnlockSV.value) {
382
+ awaitingUnlockSV.value = false
383
+ runOnJS(onAccentDisengageCancel)()
384
+ return
385
+ }
386
+ if (!isDraggingSV.value) return
387
+ const committed = scrubRatioSV.value
388
+ // Actualiza progressRatioSV ANTES de desactivar isDragging para evitar
389
+ // que effectiveRatioSV salte de volta para a posição antiga (visual "pause").
390
+ progressRatioSV.value = committed
391
+ isDraggingSV.value = false
392
+ scrubRatioSV.value = 0
393
+ runOnJS(onScrubCommit)(committed)
394
+ })
395
+ .onFinalize(() => {
396
+ "worklet"
397
+ if (awaitingUnlockSV.value) {
398
+ awaitingUnlockSV.value = false
399
+ runOnJS(onAccentDisengageCancel)()
400
+ return
401
+ }
402
+ if (isDraggingSV.value) {
403
+ isDraggingSV.value = false
404
+ scrubRatioSV.value = 0
405
+ runOnJS(onScrubCancel)()
406
+ }
407
+ }),
408
+ [
409
+ durationOk,
410
+ isDraggingSV,
411
+ awaitingUnlockSV,
412
+ scrubRatioSV,
413
+ progressRatioSV,
414
+ trackWidthSV,
415
+ chromeVisibleSV,
416
+ timelineAccentEngagedSV,
417
+ onAccentEngage,
418
+ onAccentDisengageCancel,
419
+ onScrubBegin,
420
+ onScrubUpdate,
421
+ onScrubCommit,
422
+ onScrubCancel,
423
+ ]
424
+ )
425
+
426
+ const onTrackLayout = useCallback(
427
+ (event: LayoutChangeEvent) => {
428
+ trackWidthSV.value = event.nativeEvent.layout.width
429
+ },
430
+ [trackWidthSV]
431
+ )
432
+
433
+ if (!durationOk) {
434
+ return null
435
+ }
436
+
437
+ const unifiedSlotHeightPx = Math.max(
438
+ layout.timelineThumbDiameterPx,
439
+ Math.round(
440
+ layout.timelineThumbDiameterPx * MOVIIE_SKIN_TIMELINE_THUMB_DRAG_DIAMETER_MULTIPLIER
441
+ )
442
+ )
443
+
444
+ const scrubHitSlop = {
445
+ top: MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_TOP_DP,
446
+ bottom: MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_BOTTOM_DP,
447
+ left: MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_HORIZONTAL_DP,
448
+ right: MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_HORIZONTAL_DP,
449
+ }
450
+
451
+ return (
452
+ <View pointerEvents="box-none" style={styles.root}>
453
+ <GestureDetector gesture={panGesture}>
454
+ <View hitSlop={scrubHitSlop} pointerEvents="auto" style={styles.touchExpansion}>
455
+ <View style={styles.trackStack}>
456
+ <View
457
+ accessibilityRole="progressbar"
458
+ accessibilityValue={{
459
+ max: 1,
460
+ min: 0,
461
+ now: progressRatioLive,
462
+ }}
463
+ collapsable={false}
464
+ style={[styles.unifiedBarSlot, { height: unifiedSlotHeightPx }]}
465
+ onLayout={onTrackLayout}
466
+ >
467
+ <View style={styles.railLayer} />
468
+ <ReanimatedView style={[styles.bufferLayer, bufferBarStyle]} />
469
+ <ReanimatedView style={[styles.progressLayer, progressBarStyle]} />
470
+ <ReanimatedView style={[styles.thumbBase, thumbStyle]} />
471
+ </View>
472
+ </View>
473
+ </View>
474
+ </GestureDetector>
475
+ </View>
476
+ )
477
+ }
@@ -0,0 +1,96 @@
1
+ import type { PressableProps, ViewStyle } from 'react-native'
2
+ import type { ReactNode } from 'react'
3
+ import { Pressable, View } from 'react-native'
4
+
5
+ import {
6
+ MOVIIE_SKIN_CHROME_CONTROL_PRESSED_OPACITY,
7
+ MOVIIE_SKIN_CHROME_CONTROL_REST_OPACITY,
8
+ MOVIIE_SKIN_CHROME_FOREGROUND_HEX,
9
+ MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX,
10
+ MOVIIE_SKIN_TOP_ICON_CAST_VISUAL_SCALE,
11
+ } from '../../constants'
12
+ import type { MoviieSkinLayoutMetrics } from '../../lib/build-moviie-skin-layout-metrics'
13
+ import { EmbedMediaIconCast, EmbedMediaIconCastConnected } from '../icons/embed-media-icons'
14
+
15
+ function pressOpacity(pressed: boolean): Pick<ViewStyle, 'opacity'> {
16
+ return {
17
+ opacity: pressed
18
+ ? MOVIIE_SKIN_CHROME_CONTROL_PRESSED_OPACITY
19
+ : MOVIIE_SKIN_CHROME_CONTROL_REST_OPACITY,
20
+ }
21
+ }
22
+
23
+ function ScrimPad(props: {
24
+ accessibilityLabel: string
25
+ hitSlop?: PressableProps['hitSlop']
26
+ onPress: () => void
27
+ chromeScrimBaseStyle: object
28
+ style: object
29
+ children: ReactNode
30
+ selected: boolean
31
+ }) {
32
+ return (
33
+ <Pressable
34
+ accessibilityLabel={props.accessibilityLabel}
35
+ accessibilityRole="button"
36
+ accessibilityState={{ selected: props.selected }}
37
+ hitSlop={props.hitSlop}
38
+ onPress={props.onPress}
39
+ style={(state) => [props.chromeScrimBaseStyle, props.style, pressOpacity(state.pressed)]}
40
+ >
41
+ {props.children}
42
+ </Pressable>
43
+ )
44
+ }
45
+
46
+ export function MoviieSkinCastCircleButton(props: {
47
+ castConnected: boolean
48
+ chromeScrimBaseStyle: object
49
+ circleScrimStyle: object
50
+ layout: MoviieSkinLayoutMetrics
51
+ onPress: () => void
52
+ }) {
53
+ const Icon = props.castConnected ? EmbedMediaIconCastConnected : EmbedMediaIconCast
54
+ return (
55
+ <ScrimPad
56
+ accessibilityLabel={props.castConnected ? 'Encerrar transmissão' : 'Transmitir'}
57
+ chromeScrimBaseStyle={props.chromeScrimBaseStyle}
58
+ hitSlop={MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX}
59
+ onPress={props.onPress}
60
+ selected={props.castConnected}
61
+ style={props.circleScrimStyle}
62
+ >
63
+ <Icon
64
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
65
+ size={props.layout.floatingHudFullscreenIconPx}
66
+ />
67
+ </ScrimPad>
68
+ )
69
+ }
70
+
71
+ export function MoviieSkinCastTopBareButton(props: {
72
+ castConnected: boolean
73
+ layout: MoviieSkinLayoutMetrics
74
+ onPress: () => void
75
+ }) {
76
+ const Icon = props.castConnected ? EmbedMediaIconCastConnected : EmbedMediaIconCast
77
+ return (
78
+ <Pressable
79
+ accessibilityLabel={props.castConnected ? 'Encerrar transmissão' : 'Transmitir'}
80
+ accessibilityRole="button"
81
+ accessibilityState={{ selected: props.castConnected }}
82
+ hitSlop={MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX}
83
+ onPress={props.onPress}
84
+ style={(state) => pressOpacity(state.pressed)}
85
+ >
86
+ <View>
87
+ <Icon
88
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
89
+ size={
90
+ props.layout.floatingHudFullscreenTopIconPx * MOVIIE_SKIN_TOP_ICON_CAST_VISUAL_SCALE
91
+ }
92
+ />
93
+ </View>
94
+ </Pressable>
95
+ )
96
+ }