@innovui/react-native 0.3.1 → 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.
- package/dist/components/_shared/media-icons.js +5 -23
- package/dist/components/_shared/media-icons.js.map +1 -1
- package/dist/components/audio-player/audio-player.js +1 -1
- package/dist/components/audio-player/audio-player.js.map +1 -1
- package/dist/components/video-player/video-player.d.ts +4 -7
- package/dist/components/video-player/video-player.js +77 -104
- package/dist/components/video-player/video-player.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,6 @@ const strokeProps = {
|
|
|
8
8
|
strokeLinejoin: "round"
|
|
9
9
|
};
|
|
10
10
|
const playPath = "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z";
|
|
11
|
-
const speakerPath = "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z";
|
|
12
11
|
function Icon({ color, size = 20, children, ...props }) {
|
|
13
12
|
return /* @__PURE__ */ jsx(Svg, {
|
|
14
13
|
width: size,
|
|
@@ -29,8 +28,11 @@ function PlayIcon({ filled, color, ...props }) {
|
|
|
29
28
|
children: /* @__PURE__ */ jsx(Path, { d: playPath })
|
|
30
29
|
});
|
|
31
30
|
}
|
|
32
|
-
|
|
31
|
+
/** Duas barras de pausar; `filled` preenche com `color` (botão central do vídeo). */
|
|
32
|
+
function PauseIcon({ filled, color, ...props }) {
|
|
33
33
|
return /* @__PURE__ */ jsxs(Icon, {
|
|
34
|
+
color,
|
|
35
|
+
fill: filled ? color : "none",
|
|
34
36
|
...props,
|
|
35
37
|
children: [/* @__PURE__ */ jsx(Rect, {
|
|
36
38
|
x: "14",
|
|
@@ -47,26 +49,6 @@ function PauseIcon(props) {
|
|
|
47
49
|
})]
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
|
-
function VolumeIcon(props) {
|
|
51
|
-
return /* @__PURE__ */ jsxs(Icon, {
|
|
52
|
-
...props,
|
|
53
|
-
children: [
|
|
54
|
-
/* @__PURE__ */ jsx(Path, { d: speakerPath }),
|
|
55
|
-
/* @__PURE__ */ jsx(Path, { d: "M16 9a5 5 0 0 1 0 6" }),
|
|
56
|
-
/* @__PURE__ */ jsx(Path, { d: "M19.364 18.364a9 9 0 0 0 0-12.728" })
|
|
57
|
-
]
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function VolumeMutedIcon(props) {
|
|
61
|
-
return /* @__PURE__ */ jsxs(Icon, {
|
|
62
|
-
...props,
|
|
63
|
-
children: [
|
|
64
|
-
/* @__PURE__ */ jsx(Path, { d: speakerPath }),
|
|
65
|
-
/* @__PURE__ */ jsx(Path, { d: "M22 9l-6 6" }),
|
|
66
|
-
/* @__PURE__ */ jsx(Path, { d: "M16 9l6 6" })
|
|
67
|
-
]
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
52
|
function MaximizeIcon(props) {
|
|
71
53
|
return /* @__PURE__ */ jsxs(Icon, {
|
|
72
54
|
...props,
|
|
@@ -120,6 +102,6 @@ function NextIcon(props) {
|
|
|
120
102
|
});
|
|
121
103
|
}
|
|
122
104
|
//#endregion
|
|
123
|
-
export { ExpandIcon, MaximizeIcon, MinimizeIcon, NextIcon, PauseIcon, PlayIcon, PreviousIcon
|
|
105
|
+
export { ExpandIcon, MaximizeIcon, MinimizeIcon, NextIcon, PauseIcon, PlayIcon, PreviousIcon };
|
|
124
106
|
|
|
125
107
|
//# sourceMappingURL=media-icons.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-icons.js","names":[],"sources":["../../../src/components/_shared/media-icons.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport Svg, { Path, Rect, type SvgProps } from 'react-native-svg'\n\ntype IconProps = Omit<SvgProps, 'stroke' | 'fill' | 'width' | 'height'> & {\n color?: string\n /** Lado em px. Padrão: 20. */\n size?: number\n}\n\n// Ícones do Lucide (viewBox 24); com traço 2.4 saem com 2px aos 20px, como no Figma.\nconst strokeProps = {\n fill: 'none',\n strokeWidth: 2.4,\n strokeLinecap: 'round',\n strokeLinejoin: 'round',\n} as const\n\nconst playPath =\n 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z'\
|
|
1
|
+
{"version":3,"file":"media-icons.js","names":[],"sources":["../../../src/components/_shared/media-icons.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport Svg, { Path, Rect, type SvgProps } from 'react-native-svg'\n\ntype IconProps = Omit<SvgProps, 'stroke' | 'fill' | 'width' | 'height'> & {\n color?: string\n /** Lado em px. Padrão: 20. */\n size?: number\n}\n\n// Ícones do Lucide (viewBox 24); com traço 2.4 saem com 2px aos 20px, como no Figma.\nconst strokeProps = {\n fill: 'none',\n strokeWidth: 2.4,\n strokeLinecap: 'round',\n strokeLinejoin: 'round',\n} as const\n\nconst playPath =\n 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z'\n\nfunction Icon({\n color,\n size = 20,\n children,\n ...props\n}: IconProps & { fill?: string; children: React.ReactNode }) {\n return (\n <Svg width={size} height={size} viewBox=\"0 0 24 24\" stroke={color} {...strokeProps} {...props}>\n {children}\n </Svg>\n )\n}\n\n/** Triângulo de reproduzir; `filled` preenche com `color` (botão central do vídeo). */\nexport function PlayIcon({ filled, color, ...props }: IconProps & { filled?: boolean }) {\n return (\n <Icon color={color} fill={filled ? color : 'none'} {...props}>\n <Path d={playPath} />\n </Icon>\n )\n}\n\n/** Duas barras de pausar; `filled` preenche com `color` (botão central do vídeo). */\nexport function PauseIcon({ filled, color, ...props }: IconProps & { filled?: boolean }) {\n return (\n <Icon color={color} fill={filled ? color : 'none'} {...props}>\n <Rect x=\"14\" y=\"3\" width=\"5\" height=\"18\" rx=\"1\" />\n <Rect x=\"5\" y=\"3\" width=\"5\" height=\"18\" rx=\"1\" />\n </Icon>\n )\n}\n\nexport function MaximizeIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n <Path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n <Path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n <Path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n </Icon>\n )\n}\n\nexport function MinimizeIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"M8 3v3a2 2 0 0 1-2 2H3\" />\n <Path d=\"M21 8h-3a2 2 0 0 1-2-2V3\" />\n <Path d=\"M3 16h3a2 2 0 0 1 2 2v3\" />\n <Path d=\"M16 21v-3a2 2 0 0 1 2-2h3\" />\n </Icon>\n )\n}\n\n/** Quatro setas para fora: o ícone de ampliar sobre a miniatura. */\nexport function ExpandIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"m15 15 6 6\" />\n <Path d=\"m15 9 6-6\" />\n <Path d=\"M21 16v5h-5\" />\n <Path d=\"M21 8V3h-5\" />\n <Path d=\"M3 16v5h5\" />\n <Path d=\"m3 21 6-6\" />\n <Path d=\"M3 8V3h5\" />\n <Path d=\"M9 9 3 3\" />\n </Icon>\n )\n}\n\n/** ‹ de 20px (setas do carrossel e do lightbox). */\nexport function PreviousIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"m15 18-6-6 6-6\" />\n </Icon>\n )\n}\n\n/** › de 20px (setas do carrossel e do lightbox). */\nexport function NextIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"m9 18 6-6-6-6\" />\n </Icon>\n )\n}\n"],"mappings":";;;AAWA,MAAM,cAAc;CAClB,MAAM;CACN,aAAa;CACb,eAAe;CACf,gBAAgB;AAClB;AAEA,MAAM,WACJ;AAEF,SAAS,KAAK,EACZ,OACA,OAAO,IACP,UACA,GAAG,SACwD;CAC3D,OACE,oBAAC,KAAD;EAAK,OAAO;EAAM,QAAQ;EAAM,SAAQ;EAAY,QAAQ;EAAO,GAAI;EAAa,GAAI;EACrF;CACE,CAAA;AAET;;AAGA,SAAgB,SAAS,EAAE,QAAQ,OAAO,GAAG,SAA2C;CACtF,OACE,oBAAC,MAAD;EAAa;EAAO,MAAM,SAAS,QAAQ;EAAQ,GAAI;EACrD,UAAA,oBAAC,MAAD,EAAM,GAAG,SAAW,CAAA;CAChB,CAAA;AAEV;;AAGA,SAAgB,UAAU,EAAE,QAAQ,OAAO,GAAG,SAA2C;CACvF,OACE,qBAAC,MAAD;EAAa;EAAO,MAAM,SAAS,QAAQ;EAAQ,GAAI;EAAvD,UAAA,CACE,oBAAC,MAAD;GAAM,GAAE;GAAK,GAAE;GAAI,OAAM;GAAI,QAAO;GAAK,IAAG;EAAK,CAAA,GACjD,oBAAC,MAAD;GAAM,GAAE;GAAI,GAAE;GAAI,OAAM;GAAI,QAAO;GAAK,IAAG;EAAK,CAAA,CAC5C;;AAEV;AAEA,SAAgB,aAAa,OAAkB;CAC7C,OACE,qBAAC,MAAD;EAAM,GAAI;EAAV,UAAA;GACE,oBAAC,MAAD,EAAM,GAAE,yBAA0B,CAAA;GAClC,oBAAC,MAAD,EAAM,GAAE,2BAA4B,CAAA;GACpC,oBAAC,MAAD,EAAM,GAAE,0BAA2B,CAAA;GACnC,oBAAC,MAAD,EAAM,GAAE,4BAA6B,CAAA;EACjC;;AAEV;AAEA,SAAgB,aAAa,OAAkB;CAC7C,OACE,qBAAC,MAAD;EAAM,GAAI;EAAV,UAAA;GACE,oBAAC,MAAD,EAAM,GAAE,yBAA0B,CAAA;GAClC,oBAAC,MAAD,EAAM,GAAE,2BAA4B,CAAA;GACpC,oBAAC,MAAD,EAAM,GAAE,0BAA2B,CAAA;GACnC,oBAAC,MAAD,EAAM,GAAE,4BAA6B,CAAA;EACjC;;AAEV;;AAGA,SAAgB,WAAW,OAAkB;CAC3C,OACE,qBAAC,MAAD;EAAM,GAAI;EAAV,UAAA;GACE,oBAAC,MAAD,EAAM,GAAE,aAAc,CAAA;GACtB,oBAAC,MAAD,EAAM,GAAE,YAAa,CAAA;GACrB,oBAAC,MAAD,EAAM,GAAE,cAAe,CAAA;GACvB,oBAAC,MAAD,EAAM,GAAE,aAAc,CAAA;GACtB,oBAAC,MAAD,EAAM,GAAE,YAAa,CAAA;GACrB,oBAAC,MAAD,EAAM,GAAE,YAAa,CAAA;GACrB,oBAAC,MAAD,EAAM,GAAE,WAAY,CAAA;GACpB,oBAAC,MAAD,EAAM,GAAE,WAAY,CAAA;EAChB;;AAEV;;AAGA,SAAgB,aAAa,OAAkB;CAC7C,OACE,oBAAC,MAAD;EAAM,GAAI;EACR,UAAA,oBAAC,MAAD,EAAM,GAAE,iBAAkB,CAAA;CACtB,CAAA;AAEV;;AAGA,SAAgB,SAAS,OAAkB;CACzC,OACE,oBAAC,MAAD;EAAM,GAAI;EACR,UAAA,oBAAC,MAAD,EAAM,GAAE,gBAAiB,CAAA;CACrB,CAAA;AAEV"}
|
|
@@ -60,7 +60,7 @@ function AudioPlayer({ className, label = "Áudio", playing = false, position =
|
|
|
60
60
|
className: cn("h-[34px] min-w-0 justify-center", stacked ? "w-full" : "flex-1"),
|
|
61
61
|
children: /* @__PURE__ */ jsx(View, {
|
|
62
62
|
pointerEvents: "none",
|
|
63
|
-
className: "h-full flex-row items-center gap-0.5",
|
|
63
|
+
className: "h-full flex-row items-center gap-0.5 overflow-hidden",
|
|
64
64
|
children: peaks.map((peak, i) => /* @__PURE__ */ jsx(View, {
|
|
65
65
|
className: cn("min-w-0.5 flex-1 rounded-full", i < played ? "bg-button-primary-bg" : "bg-media-wave-rest"),
|
|
66
66
|
style: { height: BAR_MIN + peak * BAR_RANGE }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-player.js","names":[],"sources":["../../../src/components/audio-player/audio-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport { Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek } from '../_shared/media'\nimport { PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\n// Alturas das barras no Figma: de 6px (silêncio) a 34px (pico).\nconst BAR_MIN = 6\nconst BAR_RANGE = 28\n\nexport type AudioPlayerProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Velocidade atual (controlado). Sem ela, o player guarda a escolhida. */\n rate?: number\n /** Velocidades do botão (cada toque passa para a próxima). Padrão: 1x, 1.5x e 2x. */\n speeds?: number[]\n /** Picos da onda, de 0 a 1. Sem eles, usa uma onda padrão. */\n waveform?: number[]\n /** Quantidade de barras da onda. Padrão: 52. */\n bars?: number\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na onda. */\n onSeek?: (seconds: number) => void\n /** Chamado com a nova velocidade quando o botão é tocado. */\n onRateChange?: (rate: number) => void\n}\n\n/**\n * Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o\n * estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e\n * `onRateChange` ao player de verdade.\n */\nexport function AudioPlayer({\n className,\n label = 'Áudio',\n playing = false,\n position = 0,\n duration = 0,\n rate,\n speeds = [1, 1.5, 2],\n waveform,\n bars = 52,\n onPlayPause,\n onSeek,\n onRateChange,\n onLayout,\n ...props\n}: AudioPlayerProps) {\n const [chosen, setChosen] = useState(speeds[0] ?? 1)\n const [cardWidth, setCardWidth] = useState(0)\n // abaixo de 416px (26rem) o tempo desce para baixo da onda, que ganha a largura toda\n const stacked = cardWidth > 0 && cardWidth < 416\n const speed = rate ?? chosen\n const iconColor = useToken('--color-media-on-surface')\n const seek = useSeek({ position, duration, label: 'Posição do áudio', onSeek })\n // Em telas estreitas a onda perde barras (2px + 2px de espaço), em vez de invadir o tempo.\n const shown = seek.width ? Math.min(bars, Math.max(1, Math.floor((seek.width + 2) / 4))) : bars\n const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, shown)\n const played = duration ? Math.round((position / duration) * shown) : 0\n\n function nextSpeed() {\n const next = speeds[(speeds.indexOf(speed) + 1) % speeds.length] ?? 1\n setChosen(next)\n onRateChange?.(next)\n }\n\n return (\n <View\n accessibilityLabel={label}\n onLayout={(event) => {\n setCardWidth(event.nativeEvent.layout.width)\n onLayout?.(event)\n }}\n className={cn(\n 'w-full max-w-[480px] flex-row items-center gap-3 rounded-lg border border-border-default bg-surface-elevated p-4 shadow-sm',\n className,\n )}\n {...props}\n >\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={onPlayPause}\n className=\"size-11 shrink-0 items-center justify-center rounded-full bg-button-primary-bg active:bg-button-primary-bg-active\"\n >\n {playing ? <PauseIcon color={iconColor} /> : <PlayIcon color={iconColor} />}\n </Pressable>\n <View\n className={cn(\n 'min-w-0 flex-1',\n stacked ? 'flex-col items-stretch gap-1' : 'flex-row items-center gap-2',\n )}\n >\n <View\n {...seek.props}\n className={cn('h-[34px] min-w-0 justify-center', stacked ? 'w-full' : 'flex-1')}\n >\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à onda. */}\n <View
|
|
1
|
+
{"version":3,"file":"audio-player.js","names":[],"sources":["../../../src/components/audio-player/audio-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport { Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek } from '../_shared/media'\nimport { PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\n// Alturas das barras no Figma: de 6px (silêncio) a 34px (pico).\nconst BAR_MIN = 6\nconst BAR_RANGE = 28\n\nexport type AudioPlayerProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Velocidade atual (controlado). Sem ela, o player guarda a escolhida. */\n rate?: number\n /** Velocidades do botão (cada toque passa para a próxima). Padrão: 1x, 1.5x e 2x. */\n speeds?: number[]\n /** Picos da onda, de 0 a 1. Sem eles, usa uma onda padrão. */\n waveform?: number[]\n /** Quantidade de barras da onda. Padrão: 52. */\n bars?: number\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na onda. */\n onSeek?: (seconds: number) => void\n /** Chamado com a nova velocidade quando o botão é tocado. */\n onRateChange?: (rate: number) => void\n}\n\n/**\n * Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o\n * estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e\n * `onRateChange` ao player de verdade.\n */\nexport function AudioPlayer({\n className,\n label = 'Áudio',\n playing = false,\n position = 0,\n duration = 0,\n rate,\n speeds = [1, 1.5, 2],\n waveform,\n bars = 52,\n onPlayPause,\n onSeek,\n onRateChange,\n onLayout,\n ...props\n}: AudioPlayerProps) {\n const [chosen, setChosen] = useState(speeds[0] ?? 1)\n const [cardWidth, setCardWidth] = useState(0)\n // abaixo de 416px (26rem) o tempo desce para baixo da onda, que ganha a largura toda\n const stacked = cardWidth > 0 && cardWidth < 416\n const speed = rate ?? chosen\n const iconColor = useToken('--color-media-on-surface')\n const seek = useSeek({ position, duration, label: 'Posição do áudio', onSeek })\n // Em telas estreitas a onda perde barras (2px + 2px de espaço), em vez de invadir o tempo.\n const shown = seek.width ? Math.min(bars, Math.max(1, Math.floor((seek.width + 2) / 4))) : bars\n const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, shown)\n const played = duration ? Math.round((position / duration) * shown) : 0\n\n function nextSpeed() {\n const next = speeds[(speeds.indexOf(speed) + 1) % speeds.length] ?? 1\n setChosen(next)\n onRateChange?.(next)\n }\n\n return (\n <View\n accessibilityLabel={label}\n onLayout={(event) => {\n setCardWidth(event.nativeEvent.layout.width)\n onLayout?.(event)\n }}\n className={cn(\n 'w-full max-w-[480px] flex-row items-center gap-3 rounded-lg border border-border-default bg-surface-elevated p-4 shadow-sm',\n className,\n )}\n {...props}\n >\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={onPlayPause}\n className=\"size-11 shrink-0 items-center justify-center rounded-full bg-button-primary-bg active:bg-button-primary-bg-active\"\n >\n {playing ? <PauseIcon color={iconColor} /> : <PlayIcon color={iconColor} />}\n </Pressable>\n <View\n className={cn(\n 'min-w-0 flex-1',\n stacked ? 'flex-col items-stretch gap-1' : 'flex-row items-center gap-2',\n )}\n >\n <View\n {...seek.props}\n className={cn('h-[34px] min-w-0 justify-center', stacked ? 'w-full' : 'flex-1')}\n >\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à onda. */}\n <View\n pointerEvents=\"none\"\n className=\"h-full flex-row items-center gap-0.5 overflow-hidden\"\n >\n {peaks.map((peak, i) => (\n <View\n // biome-ignore lint/suspicious/noArrayIndexKey: as barras são posicionais e fixas\n key={i}\n className={cn(\n 'min-w-0.5 flex-1 rounded-full',\n i < played ? 'bg-button-primary-bg' : 'bg-media-wave-rest',\n )}\n style={{ height: BAR_MIN + peak * BAR_RANGE }}\n />\n ))}\n </View>\n </View>\n <Text\n accessible={false}\n className=\"shrink-0 text-xs font-medium leading-4 text-text-secondary\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n </View>\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`Velocidade de reprodução: ${speed}x`}\n onPress={nextSpeed}\n className=\"h-8 w-16 shrink-0 items-center justify-center rounded-full border border-border-default bg-surface-elevated active:bg-bg-tertiary\"\n >\n <Text className=\"text-sm font-medium leading-5 text-text-secondary\">{speed}x</Text>\n </Pressable>\n </View>\n )\n}\n"],"mappings":";;;;;;;;AAUA,MAAM,UAAU;AAChB,MAAM,YAAY;;;;;;AAgClB,SAAgB,YAAY,EAC1B,WACA,QAAQ,SACR,UAAU,OACV,WAAW,GACX,WAAW,GACX,MACA,SAAS;CAAC;CAAG;CAAK;AAAC,GACnB,UACA,OAAO,IACP,aACA,QACA,cACA,UACA,GAAG,SACgB;CACnB,MAAM,CAAC,QAAQ,aAAa,SAAS,OAAO,MAAM,CAAC;CACnD,MAAM,CAAC,WAAW,gBAAgB,SAAS,CAAC;CAE5C,MAAM,UAAU,YAAY,KAAK,YAAY;CAC7C,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,0BAA0B;CACrD,MAAM,OAAO,QAAQ;EAAE;EAAU;EAAU,OAAO;EAAoB;CAAO,CAAC;CAE9E,MAAM,QAAQ,KAAK,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,KAAK,CAAC,CAAC,CAAC,IAAI;CAC3F,MAAM,QAAQ,cAAc,YAAY,kBAAkB,KAAK;CAC/D,MAAM,SAAS,WAAW,KAAK,MAAO,WAAW,WAAY,KAAK,IAAI;CAEtE,SAAS,YAAY;EACnB,MAAM,OAAO,QAAQ,OAAO,QAAQ,KAAK,IAAI,KAAK,OAAO,WAAW;EACpE,UAAU,IAAI;EACd,eAAe,IAAI;CACrB;CAEA,OACE,qBAAC,MAAD;EACE,oBAAoB;EACpB,WAAW,UAAU;GACnB,aAAa,MAAM,YAAY,OAAO,KAAK;GAC3C,WAAW,KAAK;EAClB;EACA,WAAW,GACT,8HACA,SACF;EACA,GAAI;EAVN,UAAA;GAYE,oBAAC,WAAD;IACE,mBAAkB;IAClB,oBAAoB,UAAU,WAAW;IACzC,SAAS;IACT,WAAU;IAET,UAAA,UAAU,oBAAC,WAAD,EAAW,OAAO,UAAY,CAAA,IAAI,oBAAC,UAAD,EAAU,OAAO,UAAY,CAAA;GACjE,CAAA;GACX,qBAAC,MAAD;IACE,WAAW,GACT,kBACA,UAAU,iCAAiC,6BAC7C;IAJF,UAAA,CAME,oBAAC,MAAD;KACE,GAAI,KAAK;KACT,WAAW,GAAG,mCAAmC,UAAU,WAAW,QAAQ;KAG9E,UAAA,oBAAC,MAAD;MACE,eAAc;MACd,WAAU;MAET,UAAA,MAAM,KAAK,MAAM,MAChB,oBAAC,MAAD;OAGE,WAAW,GACT,iCACA,IAAI,SAAS,yBAAyB,oBACxC;OACA,OAAO,EAAE,QAAQ,UAAU,OAAO,UAAU;MAC7C,GANM,CAMN,CACF;KACG,CAAA;IACF,CAAA,GACN,qBAAC,MAAD;KACE,YAAY;KACZ,WAAU;KACV,OAAO,EAAE,aAAa,CAAC,cAAc,EAAE;KAHzC,UAAA;MAKG,WAAW,QAAQ;MAAE;MAAI,WAAW,QAAQ;KACzC;IACF,CAAA,CAAA;;GACN,oBAAC,WAAD;IACE,mBAAkB;IAClB,oBAAoB,6BAA6B,MAAM;IACvD,SAAS;IACT,WAAU;IAEV,UAAA,qBAAC,MAAD;KAAM,WAAU;KAAhB,UAAA,CAAqE,OAAM,GAAO;;GACzE,CAAA;EACP;;AAEV"}
|
|
@@ -13,25 +13,22 @@ export type VideoPlayerProps = Omit<ViewProps, 'style'> & {
|
|
|
13
13
|
position?: number;
|
|
14
14
|
/** Duração total, em segundos. */
|
|
15
15
|
duration?: number;
|
|
16
|
-
/** Sem som: mostra o ícone de volume cortado. */
|
|
17
|
-
muted?: boolean;
|
|
18
16
|
/** Em tela cheia: mostra o ícone de sair. Só vale com `onToggleFullscreen`. */
|
|
19
17
|
fullscreen?: boolean;
|
|
20
18
|
onPlayPause?: () => void;
|
|
21
19
|
/** Chamado com a posição (em segundos) tocada ou arrastada na barra. */
|
|
22
20
|
onSeek?: (seconds: number) => void;
|
|
23
|
-
onToggleMute?: () => void;
|
|
24
21
|
/** Sem ele, o botão de tela cheia não aparece. */
|
|
25
22
|
onToggleFullscreen?: () => void;
|
|
26
23
|
/** Tempo sem interação, em ms, até esconder os controles durante a reprodução. Padrão: 2500. */
|
|
27
24
|
hideControlsAfter?: number;
|
|
28
25
|
};
|
|
29
26
|
/**
|
|
30
|
-
* Casca de player de vídeo: play central, posição, tempo
|
|
27
|
+
* Casca de player de vídeo: play/pause central, posição, tempo e tela cheia sobre o seu vídeo.
|
|
31
28
|
* Não toca nada: quem usa guarda o estado (por exemplo com `expo-video`) e o passa por props,
|
|
32
|
-
* ligando os `on…` ao player de verdade. Tocar no vídeo
|
|
33
|
-
*
|
|
29
|
+
* ligando os `on…` ao player de verdade. Tocar no vídeo mostra ou esconde os controles, que também
|
|
30
|
+
* somem sozinhos depois de um tempo sem toques enquanto toca (com leitor de tela ligado, ficam).
|
|
34
31
|
*/
|
|
35
|
-
export declare function VideoPlayer({ className, children, label, playing, position, duration,
|
|
32
|
+
export declare function VideoPlayer({ className, children, label, playing, position, duration, fullscreen, onPlayPause, onSeek, onToggleFullscreen, hideControlsAfter, ...props }: VideoPlayerProps): import("react").JSX.Element;
|
|
36
33
|
//#endregion
|
|
37
34
|
//# sourceMappingURL=video-player.d.ts.map
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import { cn } from "../../utils/cn.js";
|
|
2
2
|
import { useToken } from "../_shared/use-token.js";
|
|
3
3
|
import { formatTime, useSeek } from "../_shared/media.js";
|
|
4
|
-
import { MaximizeIcon, MinimizeIcon, PauseIcon, PlayIcon
|
|
4
|
+
import { MaximizeIcon, MinimizeIcon, PauseIcon, PlayIcon } from "../_shared/media-icons.js";
|
|
5
5
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
-
import { AccessibilityInfo, Pressable,
|
|
7
|
-
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
|
|
6
|
+
import { AccessibilityInfo, Pressable, Text, View } from "react-native";
|
|
8
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
8
|
//#region src/components/video-player/video-player.tsx
|
|
10
|
-
const button = "size-
|
|
9
|
+
const button = "size-11 shrink-0 items-center justify-center rounded-full active:bg-media-on-surface/15";
|
|
11
10
|
/**
|
|
12
|
-
* Casca de player de vídeo: play central, posição, tempo
|
|
11
|
+
* Casca de player de vídeo: play/pause central, posição, tempo e tela cheia sobre o seu vídeo.
|
|
13
12
|
* Não toca nada: quem usa guarda o estado (por exemplo com `expo-video`) e o passa por props,
|
|
14
|
-
* ligando os `on…` ao player de verdade. Tocar no vídeo
|
|
15
|
-
*
|
|
13
|
+
* ligando os `on…` ao player de verdade. Tocar no vídeo mostra ou esconde os controles, que também
|
|
14
|
+
* somem sozinhos depois de um tempo sem toques enquanto toca (com leitor de tela ligado, ficam).
|
|
16
15
|
*/
|
|
17
|
-
function VideoPlayer({ className, children, label = "Vídeo", playing = false, position = 0, duration = 0,
|
|
16
|
+
function VideoPlayer({ className, children, label = "Vídeo", playing = false, position = 0, duration = 0, fullscreen = false, onPlayPause, onSeek, onToggleFullscreen, hideControlsAfter = 2500, ...props }) {
|
|
18
17
|
const [active, setActive] = useState(true);
|
|
19
18
|
const [screenReader, setScreenReader] = useState(false);
|
|
20
19
|
const timer = useRef(void 0);
|
|
21
20
|
const onSurface = useToken("--color-media-on-surface");
|
|
22
21
|
const accent = useToken("--color-interactive-primary");
|
|
23
|
-
const scrim = useToken("--color-media-surface") ?? "#000000";
|
|
24
22
|
const seek = useSeek({
|
|
25
23
|
position,
|
|
26
24
|
duration,
|
|
@@ -29,11 +27,16 @@ function VideoPlayer({ className, children, label = "Vídeo", playing = false, p
|
|
|
29
27
|
});
|
|
30
28
|
const visible = active || !playing || screenReader;
|
|
31
29
|
const progress = duration ? Math.min(position / duration, 1) * 100 : 0;
|
|
30
|
+
const ended = !playing && duration > 0 && position >= duration - .1;
|
|
32
31
|
useEffect(() => {
|
|
33
32
|
AccessibilityInfo.isScreenReaderEnabled().then(setScreenReader);
|
|
34
33
|
const subscription = AccessibilityInfo.addEventListener("screenReaderChanged", setScreenReader);
|
|
35
34
|
return () => subscription.remove();
|
|
36
35
|
}, []);
|
|
36
|
+
const hide = () => {
|
|
37
|
+
clearTimeout(timer.current);
|
|
38
|
+
setActive(false);
|
|
39
|
+
};
|
|
37
40
|
const show = useCallback(() => {
|
|
38
41
|
setActive(true);
|
|
39
42
|
clearTimeout(timer.current);
|
|
@@ -44,6 +47,10 @@ function VideoPlayer({ className, children, label = "Vídeo", playing = false, p
|
|
|
44
47
|
else setActive(true);
|
|
45
48
|
return () => clearTimeout(timer.current);
|
|
46
49
|
}, [playing, show]);
|
|
50
|
+
const playPause = () => {
|
|
51
|
+
if (ended) onSeek?.(0);
|
|
52
|
+
onPlayPause?.();
|
|
53
|
+
};
|
|
47
54
|
return /* @__PURE__ */ jsxs(View, {
|
|
48
55
|
accessibilityLabel: label,
|
|
49
56
|
className: cn("aspect-video w-full overflow-hidden rounded-lg bg-media-surface shadow-lg", className),
|
|
@@ -57,117 +64,83 @@ function VideoPlayer({ className, children, label = "Vídeo", playing = false, p
|
|
|
57
64
|
/* @__PURE__ */ jsx(Pressable, {
|
|
58
65
|
accessible: false,
|
|
59
66
|
onPress: () => {
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
if (!playing) return;
|
|
68
|
+
if (visible) hide();
|
|
69
|
+
else show();
|
|
62
70
|
},
|
|
63
71
|
className: "absolute inset-0",
|
|
64
|
-
children:
|
|
72
|
+
children: visible && /* @__PURE__ */ jsx(View, {
|
|
65
73
|
pointerEvents: "none",
|
|
66
|
-
className: "absolute inset-0 bg-media-surface/
|
|
67
|
-
})
|
|
68
|
-
}),
|
|
69
|
-
!playing && /* @__PURE__ */ jsx(View, {
|
|
70
|
-
pointerEvents: "box-none",
|
|
71
|
-
className: "absolute inset-0 items-center justify-center",
|
|
72
|
-
children: /* @__PURE__ */ jsx(Pressable, {
|
|
73
|
-
accessible: false,
|
|
74
|
-
importantForAccessibility: "no",
|
|
75
|
-
onPress: onPlayPause,
|
|
76
|
-
className: "size-16 items-center justify-center rounded-full bg-media-on-surface shadow-lg",
|
|
77
|
-
children: /* @__PURE__ */ jsx(PlayIcon, {
|
|
78
|
-
filled: true,
|
|
79
|
-
size: 28,
|
|
80
|
-
color: accent
|
|
81
|
-
})
|
|
74
|
+
className: "absolute inset-0 bg-media-surface/40"
|
|
82
75
|
})
|
|
83
76
|
}),
|
|
84
77
|
/* @__PURE__ */ jsxs(View, {
|
|
85
78
|
pointerEvents: visible ? "box-none" : "none",
|
|
86
79
|
importantForAccessibility: visible ? "auto" : "no-hide-descendants",
|
|
87
80
|
accessibilityElementsHidden: !visible,
|
|
88
|
-
className: cn("absolute inset-
|
|
89
|
-
children: [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
81
|
+
className: cn("absolute inset-0", !visible && "opacity-0"),
|
|
82
|
+
children: [/* @__PURE__ */ jsx(View, {
|
|
83
|
+
pointerEvents: "box-none",
|
|
84
|
+
className: "absolute inset-0 items-center justify-center",
|
|
85
|
+
children: /* @__PURE__ */ jsx(Pressable, {
|
|
86
|
+
accessibilityRole: "button",
|
|
87
|
+
accessibilityLabel: playing ? "Pausar" : "Reproduzir",
|
|
88
|
+
onPress: () => {
|
|
89
|
+
playPause();
|
|
90
|
+
show();
|
|
91
|
+
},
|
|
92
|
+
className: "size-14 items-center justify-center rounded-full bg-media-on-surface shadow-lg active:opacity-80",
|
|
93
|
+
children: playing ? /* @__PURE__ */ jsx(PauseIcon, {
|
|
94
|
+
filled: true,
|
|
95
|
+
size: 24,
|
|
96
|
+
color: accent
|
|
97
|
+
}) : /* @__PURE__ */ jsx(PlayIcon, {
|
|
98
|
+
filled: true,
|
|
99
|
+
size: 24,
|
|
100
|
+
color: accent
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
}), /* @__PURE__ */ jsxs(View, {
|
|
104
|
+
pointerEvents: "box-none",
|
|
105
|
+
className: "absolute inset-x-0 bottom-0",
|
|
106
|
+
children: [/* @__PURE__ */ jsxs(View, {
|
|
107
|
+
pointerEvents: "box-none",
|
|
108
|
+
className: "h-11 flex-row items-center justify-between pl-4 pr-1",
|
|
109
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
110
|
+
accessible: false,
|
|
111
|
+
className: "text-xs font-medium leading-4 text-media-on-surface",
|
|
112
|
+
style: { fontVariant: ["tabular-nums"] },
|
|
113
|
+
children: [
|
|
114
|
+
formatTime(position),
|
|
115
|
+
" / ",
|
|
116
|
+
formatTime(duration)
|
|
117
|
+
]
|
|
118
|
+
}), onToggleFullscreen && /* @__PURE__ */ jsx(Pressable, {
|
|
119
|
+
accessibilityRole: "button",
|
|
120
|
+
accessibilityLabel: fullscreen ? "Sair da tela cheia" : "Tela cheia",
|
|
121
|
+
onPress: onToggleFullscreen,
|
|
122
|
+
className: button,
|
|
123
|
+
children: fullscreen ? /* @__PURE__ */ jsx(MinimizeIcon, { color: onSurface }) : /* @__PURE__ */ jsx(MaximizeIcon, { color: onSurface })
|
|
113
124
|
})]
|
|
114
|
-
}),
|
|
115
|
-
/* @__PURE__ */ jsx(View, {
|
|
125
|
+
}), /* @__PURE__ */ jsx(View, {
|
|
116
126
|
...seek.props,
|
|
117
|
-
className: "
|
|
118
|
-
children: /* @__PURE__ */
|
|
127
|
+
className: "-mt-2 mx-4 h-8 justify-center",
|
|
128
|
+
children: /* @__PURE__ */ jsxs(View, {
|
|
119
129
|
pointerEvents: "none",
|
|
120
130
|
className: "h-1 w-full rounded-full bg-media-on-surface/30",
|
|
121
|
-
children: /* @__PURE__ */ jsx(View, {
|
|
131
|
+
children: [/* @__PURE__ */ jsx(View, {
|
|
122
132
|
className: "h-full rounded-full bg-media-accent",
|
|
123
133
|
style: { width: `${progress}%` }
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
className: "flex-row items-center gap-2",
|
|
131
|
-
children: [/* @__PURE__ */ jsx(Pressable, {
|
|
132
|
-
accessibilityRole: "button",
|
|
133
|
-
accessibilityLabel: playing ? "Pausar" : "Reproduzir",
|
|
134
|
-
onPress: () => {
|
|
135
|
-
onPlayPause?.();
|
|
136
|
-
show();
|
|
137
|
-
},
|
|
138
|
-
className: button,
|
|
139
|
-
children: playing ? /* @__PURE__ */ jsx(PauseIcon, { color: onSurface }) : /* @__PURE__ */ jsx(PlayIcon, { color: onSurface })
|
|
140
|
-
}), /* @__PURE__ */ jsxs(Text, {
|
|
141
|
-
accessible: false,
|
|
142
|
-
className: "text-xs font-medium leading-4 text-media-on-surface",
|
|
143
|
-
style: { fontVariant: ["tabular-nums"] },
|
|
144
|
-
children: [
|
|
145
|
-
formatTime(position),
|
|
146
|
-
" / ",
|
|
147
|
-
formatTime(duration)
|
|
148
|
-
]
|
|
149
|
-
})]
|
|
150
|
-
}), /* @__PURE__ */ jsxs(View, {
|
|
151
|
-
className: "flex-row items-center gap-2",
|
|
152
|
-
children: [/* @__PURE__ */ jsx(Pressable, {
|
|
153
|
-
accessibilityRole: "button",
|
|
154
|
-
accessibilityLabel: muted ? "Ativar o som" : "Silenciar",
|
|
155
|
-
onPress: () => {
|
|
156
|
-
onToggleMute?.();
|
|
157
|
-
show();
|
|
158
|
-
},
|
|
159
|
-
className: button,
|
|
160
|
-
children: muted ? /* @__PURE__ */ jsx(VolumeMutedIcon, { color: onSurface }) : /* @__PURE__ */ jsx(VolumeIcon, { color: onSurface })
|
|
161
|
-
}), onToggleFullscreen && /* @__PURE__ */ jsx(Pressable, {
|
|
162
|
-
accessibilityRole: "button",
|
|
163
|
-
accessibilityLabel: fullscreen ? "Sair da tela cheia" : "Tela cheia",
|
|
164
|
-
onPress: onToggleFullscreen,
|
|
165
|
-
className: button,
|
|
166
|
-
children: fullscreen ? /* @__PURE__ */ jsx(MinimizeIcon, { color: onSurface }) : /* @__PURE__ */ jsx(MaximizeIcon, { color: onSurface })
|
|
134
|
+
}), /* @__PURE__ */ jsx(View, {
|
|
135
|
+
className: "absolute -top-1 size-3 rounded-full bg-media-accent",
|
|
136
|
+
style: {
|
|
137
|
+
left: `${progress}%`,
|
|
138
|
+
marginLeft: -6
|
|
139
|
+
}
|
|
167
140
|
})]
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
]
|
|
141
|
+
})
|
|
142
|
+
})]
|
|
143
|
+
})]
|
|
171
144
|
})
|
|
172
145
|
]
|
|
173
146
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-player.js","names":[],"sources":["../../../src/components/video-player/video-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'\nimport { AccessibilityInfo, Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native'\nimport Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg'\nimport { cn } from '../../utils/cn'\nimport { formatTime, useSeek } from '../_shared/media'\nimport {\n MaximizeIcon,\n MinimizeIcon,\n PauseIcon,\n PlayIcon,\n VolumeIcon,\n VolumeMutedIcon,\n} from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\nexport type VideoPlayerProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** A superfície de vídeo (ex.: o `VideoView` do `expo-video`), que preenche o player. */\n children: ReactNode\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar e esconde o play central. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Sem som: mostra o ícone de volume cortado. */\n muted?: boolean\n /** Em tela cheia: mostra o ícone de sair. Só vale com `onToggleFullscreen`. */\n fullscreen?: boolean\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na barra. */\n onSeek?: (seconds: number) => void\n onToggleMute?: () => void\n /** Sem ele, o botão de tela cheia não aparece. */\n onToggleFullscreen?: () => void\n /** Tempo sem interação, em ms, até esconder os controles durante a reprodução. Padrão: 2500. */\n hideControlsAfter?: number\n}\n\nconst button =\n 'size-9 shrink-0 items-center justify-center rounded-full active:bg-media-on-surface/15'\n\n/**\n * Casca de player de vídeo: play central, posição, tempo, mudo e tela cheia sobre o seu vídeo.\n * Não toca nada: quem usa guarda o estado (por exemplo com `expo-video`) e o passa por props,\n * ligando os `on…` ao player de verdade. Tocar no vídeo alterna play/pause e mostra os controles,\n * que somem depois de um tempo sem toques enquanto toca (com leitor de tela ligado, ficam).\n */\nexport function VideoPlayer({\n className,\n children,\n label = 'Vídeo',\n playing = false,\n position = 0,\n duration = 0,\n muted = false,\n fullscreen = false,\n onPlayPause,\n onSeek,\n onToggleMute,\n onToggleFullscreen,\n hideControlsAfter = 2500,\n ...props\n}: VideoPlayerProps) {\n const [active, setActive] = useState(true)\n const [screenReader, setScreenReader] = useState(false)\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const onSurface = useToken('--color-media-on-surface')\n const accent = useToken('--color-interactive-primary')\n const scrim = useToken('--color-media-surface') ?? '#000000'\n const seek = useSeek({ position, duration, label: 'Posição do vídeo', onSeek })\n const visible = active || !playing || screenReader\n const progress = duration ? Math.min(position / duration, 1) * 100 : 0\n\n useEffect(() => {\n AccessibilityInfo.isScreenReaderEnabled().then(setScreenReader)\n const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setScreenReader)\n return () => subscription.remove()\n }, [])\n\n const show = useCallback(() => {\n setActive(true)\n clearTimeout(timer.current)\n timer.current = setTimeout(() => setActive(false), hideControlsAfter)\n }, [hideControlsAfter])\n\n useEffect(() => {\n if (playing) show()\n else setActive(true)\n return () => clearTimeout(timer.current)\n }, [playing, show])\n\n return (\n <View\n accessibilityLabel={label}\n className={cn(\n 'aspect-video w-full overflow-hidden rounded-lg bg-media-surface shadow-lg',\n className,\n )}\n {...props}\n >\n <View pointerEvents=\"none\" className=\"absolute inset-0\">\n {children}\n </View>\n <Pressable\n accessible={false}\n onPress={() => {\n onPlayPause?.()\n show()\n }}\n className=\"absolute inset-0\"\n >\n {!playing && <View pointerEvents=\"none\" className=\"absolute inset-0 bg-media-surface/30\" />}\n </Pressable>\n {!playing && (\n <View pointerEvents=\"box-none\" className=\"absolute inset-0 items-center justify-center\">\n <Pressable\n accessible={false}\n importantForAccessibility=\"no\"\n onPress={onPlayPause}\n className=\"size-16 items-center justify-center rounded-full bg-media-on-surface shadow-lg\"\n >\n <PlayIcon filled size={28} color={accent} />\n </Pressable>\n </View>\n )}\n <View\n pointerEvents={visible ? 'box-none' : 'none'}\n importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}\n accessibilityElementsHidden={!visible}\n className={cn('absolute inset-x-0 bottom-0 px-4 pb-3 pt-[30px]', !visible && 'opacity-0')}\n >\n <Svg pointerEvents=\"none\" style={StyleSheet.absoluteFill} preserveAspectRatio=\"none\">\n <Defs>\n <LinearGradient id=\"scrim\" x1=\"0\" y1=\"1\" x2=\"0\" y2=\"0\">\n <Stop offset=\"0\" stopColor={scrim} stopOpacity={0.65} />\n <Stop offset=\"1\" stopColor={scrim} stopOpacity={0} />\n </LinearGradient>\n </Defs>\n <Rect width=\"100%\" height=\"100%\" fill=\"url(#scrim)\" />\n </Svg>\n <View {...seek.props} className=\"mb-1 h-4 w-full justify-center\">\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à barra. */}\n <View pointerEvents=\"none\" className=\"h-1 w-full rounded-full bg-media-on-surface/30\">\n <View\n className=\"h-full rounded-full bg-media-accent\"\n style={{ width: `${progress}%` }}\n />\n </View>\n </View>\n <View className=\"h-9 flex-row items-center justify-between\">\n <View className=\"flex-row items-center gap-2\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={() => {\n onPlayPause?.()\n show()\n }}\n className={button}\n >\n {playing ? <PauseIcon color={onSurface} /> : <PlayIcon color={onSurface} />}\n </Pressable>\n <Text\n accessible={false}\n className=\"text-xs font-medium leading-4 text-media-on-surface\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n </View>\n <View className=\"flex-row items-center gap-2\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={muted ? 'Ativar o som' : 'Silenciar'}\n onPress={() => {\n onToggleMute?.()\n show()\n }}\n className={button}\n >\n {muted ? <VolumeMutedIcon color={onSurface} /> : <VolumeIcon color={onSurface} />}\n </Pressable>\n {onToggleFullscreen && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={fullscreen ? 'Sair da tela cheia' : 'Tela cheia'}\n onPress={onToggleFullscreen}\n className={button}\n >\n {fullscreen ? (\n <MinimizeIcon color={onSurface} />\n ) : (\n <MaximizeIcon color={onSurface} />\n )}\n </Pressable>\n )}\n </View>\n </View>\n </View>\n </View>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"video-player.js","names":[],"sources":["../../../src/components/video-player/video-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'\nimport { AccessibilityInfo, Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { formatTime, useSeek } from '../_shared/media'\nimport { MaximizeIcon, MinimizeIcon, PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\nexport type VideoPlayerProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** A superfície de vídeo (ex.: o `VideoView` do `expo-video`), que preenche o player. */\n children: ReactNode\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar e esconde o play central. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Em tela cheia: mostra o ícone de sair. Só vale com `onToggleFullscreen`. */\n fullscreen?: boolean\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na barra. */\n onSeek?: (seconds: number) => void\n /** Sem ele, o botão de tela cheia não aparece. */\n onToggleFullscreen?: () => void\n /** Tempo sem interação, em ms, até esconder os controles durante a reprodução. Padrão: 2500. */\n hideControlsAfter?: number\n}\n\n// 44px: o mínimo de área de toque recomendado no iOS (Apple HIG) e perto dos 48dp do Android.\nconst button =\n 'size-11 shrink-0 items-center justify-center rounded-full active:bg-media-on-surface/15'\n\n/**\n * Casca de player de vídeo: play/pause central, posição, tempo e tela cheia sobre o seu vídeo.\n * Não toca nada: quem usa guarda o estado (por exemplo com `expo-video`) e o passa por props,\n * ligando os `on…` ao player de verdade. Tocar no vídeo mostra ou esconde os controles, que também\n * somem sozinhos depois de um tempo sem toques enquanto toca (com leitor de tela ligado, ficam).\n */\nexport function VideoPlayer({\n className,\n children,\n label = 'Vídeo',\n playing = false,\n position = 0,\n duration = 0,\n fullscreen = false,\n onPlayPause,\n onSeek,\n onToggleFullscreen,\n hideControlsAfter = 2500,\n ...props\n}: VideoPlayerProps) {\n const [active, setActive] = useState(true)\n const [screenReader, setScreenReader] = useState(false)\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const onSurface = useToken('--color-media-on-surface')\n const accent = useToken('--color-interactive-primary')\n const seek = useSeek({ position, duration, label: 'Posição do vídeo', onSeek })\n const visible = active || !playing || screenReader\n const progress = duration ? Math.min(position / duration, 1) * 100 : 0\n const ended = !playing && duration > 0 && position >= duration - 0.1\n\n useEffect(() => {\n AccessibilityInfo.isScreenReaderEnabled().then(setScreenReader)\n const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setScreenReader)\n return () => subscription.remove()\n }, [])\n\n const hide = () => {\n clearTimeout(timer.current)\n setActive(false)\n }\n\n const show = useCallback(() => {\n setActive(true)\n clearTimeout(timer.current)\n timer.current = setTimeout(() => setActive(false), hideControlsAfter)\n }, [hideControlsAfter])\n\n useEffect(() => {\n if (playing) show()\n else setActive(true)\n return () => clearTimeout(timer.current)\n }, [playing, show])\n\n // No expo-video, `play()` no fim do vídeo não faz nada: volta ao começo antes.\n const playPause = () => {\n if (ended) onSeek?.(0)\n onPlayPause?.()\n }\n\n return (\n <View\n accessibilityLabel={label}\n className={cn(\n 'aspect-video w-full overflow-hidden rounded-lg bg-media-surface shadow-lg',\n className,\n )}\n {...props}\n >\n <View pointerEvents=\"none\" className=\"absolute inset-0\">\n {children}\n </View>\n {/* Tocar no vídeo só mostra/esconde os controles: pausar sem querer é o erro mais comum no celular. */}\n <Pressable\n accessible={false}\n onPress={() => {\n if (!playing) return\n if (visible) hide()\n else show()\n }}\n className=\"absolute inset-0\"\n >\n {visible && <View pointerEvents=\"none\" className=\"absolute inset-0 bg-media-surface/40\" />}\n </Pressable>\n <View\n pointerEvents={visible ? 'box-none' : 'none'}\n importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}\n accessibilityElementsHidden={!visible}\n className={cn('absolute inset-0', !visible && 'opacity-0')}\n >\n <View pointerEvents=\"box-none\" className=\"absolute inset-0 items-center justify-center\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={() => {\n playPause()\n show()\n }}\n className=\"size-14 items-center justify-center rounded-full bg-media-on-surface shadow-lg active:opacity-80\"\n >\n {playing ? (\n <PauseIcon filled size={24} color={accent} />\n ) : (\n <PlayIcon filled size={24} color={accent} />\n )}\n </Pressable>\n </View>\n <View pointerEvents=\"box-none\" className=\"absolute inset-x-0 bottom-0\">\n <View\n pointerEvents=\"box-none\"\n className=\"h-11 flex-row items-center justify-between pl-4 pr-1\"\n >\n <Text\n accessible={false}\n className=\"text-xs font-medium leading-4 text-media-on-surface\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n {onToggleFullscreen && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={fullscreen ? 'Sair da tela cheia' : 'Tela cheia'}\n onPress={onToggleFullscreen}\n className={button}\n >\n {fullscreen ? (\n <MinimizeIcon color={onSurface} />\n ) : (\n <MaximizeIcon color={onSurface} />\n )}\n </Pressable>\n )}\n </View>\n {/* 32px de altura para o dedo acertar a barra de 4px; sobe 8px sobre a folga do botão acima. */}\n <View {...seek.props} className=\"-mt-2 mx-4 h-8 justify-center\">\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à barra. */}\n <View pointerEvents=\"none\" className=\"h-1 w-full rounded-full bg-media-on-surface/30\">\n <View\n className=\"h-full rounded-full bg-media-accent\"\n style={{ width: `${progress}%` }}\n />\n <View\n className=\"absolute -top-1 size-3 rounded-full bg-media-accent\"\n style={{ left: `${progress}%`, marginLeft: -6 }}\n />\n </View>\n </View>\n </View>\n </View>\n </View>\n )\n}\n"],"mappings":";;;;;;;;AAiCA,MAAM,SACJ;;;;;;;AAQF,SAAgB,YAAY,EAC1B,WACA,UACA,QAAQ,SACR,UAAU,OACV,WAAW,GACX,WAAW,GACX,aAAa,OACb,aACA,QACA,oBACA,oBAAoB,MACpB,GAAG,SACgB;CACnB,MAAM,CAAC,QAAQ,aAAa,SAAS,IAAI;CACzC,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,QAAQ,OAAkD,KAAA,CAAS;CACzE,MAAM,YAAY,SAAS,0BAA0B;CACrD,MAAM,SAAS,SAAS,6BAA6B;CACrD,MAAM,OAAO,QAAQ;EAAE;EAAU;EAAU,OAAO;EAAoB;CAAO,CAAC;CAC9E,MAAM,UAAU,UAAU,CAAC,WAAW;CACtC,MAAM,WAAW,WAAW,KAAK,IAAI,WAAW,UAAU,CAAC,IAAI,MAAM;CACrE,MAAM,QAAQ,CAAC,WAAW,WAAW,KAAK,YAAY,WAAW;CAEjE,gBAAgB;EACd,kBAAkB,sBAAsB,CAAC,CAAC,KAAK,eAAe;EAC9D,MAAM,eAAe,kBAAkB,iBAAiB,uBAAuB,eAAe;EAC9F,aAAa,aAAa,OAAO;CACnC,GAAG,CAAC,CAAC;CAEL,MAAM,aAAa;EACjB,aAAa,MAAM,OAAO;EAC1B,UAAU,KAAK;CACjB;CAEA,MAAM,OAAO,kBAAkB;EAC7B,UAAU,IAAI;EACd,aAAa,MAAM,OAAO;EAC1B,MAAM,UAAU,iBAAiB,UAAU,KAAK,GAAG,iBAAiB;CACtE,GAAG,CAAC,iBAAiB,CAAC;CAEtB,gBAAgB;EACd,IAAI,SAAS,KAAK;OACb,UAAU,IAAI;EACnB,aAAa,aAAa,MAAM,OAAO;CACzC,GAAG,CAAC,SAAS,IAAI,CAAC;CAGlB,MAAM,kBAAkB;EACtB,IAAI,OAAO,SAAS,CAAC;EACrB,cAAc;CAChB;CAEA,OACE,qBAAC,MAAD;EACE,oBAAoB;EACpB,WAAW,GACT,6EACA,SACF;EACA,GAAI;EANN,UAAA;GAQE,oBAAC,MAAD;IAAM,eAAc;IAAO,WAAU;IAClC;GACG,CAAA;GAEN,oBAAC,WAAD;IACE,YAAY;IACZ,eAAe;KACb,IAAI,CAAC,SAAS;KACd,IAAI,SAAS,KAAK;UACb,KAAK;IACZ;IACA,WAAU;IAET,UAAA,WAAW,oBAAC,MAAD;KAAM,eAAc;KAAO,WAAU;IAAwC,CAAA;GAChF,CAAA;GACX,qBAAC,MAAD;IACE,eAAe,UAAU,aAAa;IACtC,2BAA2B,UAAU,SAAS;IAC9C,6BAA6B,CAAC;IAC9B,WAAW,GAAG,oBAAoB,CAAC,WAAW,WAAW;IAJ3D,UAAA,CAME,oBAAC,MAAD;KAAM,eAAc;KAAW,WAAU;KACvC,UAAA,oBAAC,WAAD;MACE,mBAAkB;MAClB,oBAAoB,UAAU,WAAW;MACzC,eAAe;OACb,UAAU;OACV,KAAK;MACP;MACA,WAAU;MAET,UAAA,UACC,oBAAC,WAAD;OAAW,QAAA;OAAO,MAAM;OAAI,OAAO;MAAS,CAAA,IAE5C,oBAAC,UAAD;OAAU,QAAA;OAAO,MAAM;OAAI,OAAO;MAAS,CAAA;KAEpC,CAAA;IACP,CAAA,GACN,qBAAC,MAAD;KAAM,eAAc;KAAW,WAAU;KAAzC,UAAA,CACE,qBAAC,MAAD;MACE,eAAc;MACd,WAAU;MAFZ,UAAA,CAIE,qBAAC,MAAD;OACE,YAAY;OACZ,WAAU;OACV,OAAO,EAAE,aAAa,CAAC,cAAc,EAAE;OAHzC,UAAA;QAKG,WAAW,QAAQ;QAAE;QAAI,WAAW,QAAQ;OACzC;MACL,CAAA,GAAA,sBACC,oBAAC,WAAD;OACE,mBAAkB;OAClB,oBAAoB,aAAa,uBAAuB;OACxD,SAAS;OACT,WAAW;OAEV,UAAA,aACC,oBAAC,cAAD,EAAc,OAAO,UAAY,CAAA,IAEjC,oBAAC,cAAD,EAAc,OAAO,UAAY,CAAA;MAE1B,CAAA,CAET;KAEN,CAAA,GAAA,oBAAC,MAAD;MAAM,GAAI,KAAK;MAAO,WAAU;MAE9B,UAAA,qBAAC,MAAD;OAAM,eAAc;OAAO,WAAU;OAArC,UAAA,CACE,oBAAC,MAAD;QACE,WAAU;QACV,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG;OAChC,CAAA,GACD,oBAAC,MAAD;QACE,WAAU;QACV,OAAO;SAAE,MAAM,GAAG,SAAS;SAAI,YAAY;QAAG;OAC/C,CAAA,CACG;;KACF,CAAA,CACF;IACF,CAAA,CAAA;;EACF;;AAEV"}
|