@exode-team/react-recorder 1.1.14 → 1.1.15
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/AudioRecorder.js +50 -0
- package/dist/components/AudioRecorder.js.map +1 -0
- package/dist/components/AudioRecorder.messages.js +47 -0
- package/dist/components/AudioRecorder.messages.js.map +1 -0
- package/dist/components/AudioRecorder.styled.js +16 -0
- package/dist/components/AudioRecorder.styled.js.map +1 -0
- package/dist/components/IdleControl/IdleControl.js +12 -0
- package/dist/components/IdleControl/IdleControl.js.map +1 -0
- package/dist/components/IdleControl/IdleControl.styled.js +68 -0
- package/dist/components/IdleControl/IdleControl.styled.js.map +1 -0
- package/dist/components/RecordingShell/RecordingShell.js +80 -0
- package/dist/components/RecordingShell/RecordingShell.js.map +1 -0
- package/dist/components/RecordingShell/RecordingShell.styled.js +159 -0
- package/dist/components/RecordingShell/RecordingShell.styled.js.map +1 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js +151 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js.map +1 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js +92 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js.map +1 -0
- package/dist/components/VoiceRecorderInput/PreviewBars.js +52 -0
- package/dist/components/VoiceRecorderInput/PreviewBars.js.map +1 -0
- package/dist/components/VoiceRecorderInput/TimelineBars.js +31 -0
- package/dist/components/VoiceRecorderInput/TimelineBars.js.map +1 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js +51 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js.map +1 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js +175 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js.map +1 -0
- package/dist/components/WaveformBars/WaveformBars.js +29 -0
- package/dist/components/WaveformBars/WaveformBars.js.map +1 -0
- package/dist/components/WaveformBars/WaveformBars.styled.js +20 -0
- package/dist/components/WaveformBars/WaveformBars.styled.js.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/hooks/useAudioBlobLoader.js +175 -0
- package/dist/hooks/useAudioBlobLoader.js.map +1 -0
- package/dist/hooks/useAudioPlayback.js +119 -0
- package/dist/hooks/useAudioPlayback.js.map +1 -0
- package/dist/hooks/useAudioRecorderHandlers.js +137 -0
- package/dist/hooks/useAudioRecorderHandlers.js.map +1 -0
- package/dist/hooks/useMediaRecorder.js +127 -0
- package/dist/hooks/useMediaRecorder.js.map +1 -0
- package/dist/hooks/useRecorderVisualization.js +94 -0
- package/dist/hooks/useRecorderVisualization.js.map +1 -0
- package/dist/hooks/useRecordingSender.js +53 -0
- package/dist/hooks/useRecordingSender.js.map +1 -0
- package/dist/hooks/useRecordingTiming.js +93 -0
- package/dist/hooks/useRecordingTiming.js.map +1 -0
- package/dist/hooks/useVoiceMessagePlayer.js +177 -0
- package/dist/hooks/useVoiceMessagePlayer.js.map +1 -0
- package/dist/hooks/useVoiceRecorder.js +153 -0
- package/dist/hooks/useVoiceRecorder.js.map +1 -0
- package/dist/hooks/useVoiceRecorderInput.js +163 -0
- package/dist/hooks/useVoiceRecorderInput.js.map +1 -0
- package/dist/index.js +13 -2659
- package/dist/index.js.map +1 -1
- package/dist/platform/animation-frame-adapter.js +14 -0
- package/dist/platform/animation-frame-adapter.js.map +1 -0
- package/dist/platform/audio-analyzer-adapter.js +74 -0
- package/dist/platform/audio-analyzer-adapter.js.map +1 -0
- package/dist/platform/audio-blob-fetcher.js +19 -0
- package/dist/platform/audio-blob-fetcher.js.map +1 -0
- package/dist/platform/audio-playback-adapter.js +114 -0
- package/dist/platform/audio-playback-adapter.js.map +1 -0
- package/dist/platform/audio-playback-manager.js +26 -0
- package/dist/platform/audio-playback-manager.js.map +1 -0
- package/dist/platform/media-recorder-adapter.js +23 -0
- package/dist/platform/media-recorder-adapter.js.map +1 -0
- package/dist/platform/microphone.js +24 -0
- package/dist/platform/microphone.js.map +1 -0
- package/dist/platform/mpeg-frame.js +60 -0
- package/dist/platform/mpeg-frame.js.map +1 -0
- package/dist/utils/format.js +13 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/math.js +16 -0
- package/dist/utils/math.js.map +1 -0
- package/dist/utils/pseudo-waveform.js +40 -0
- package/dist/utils/pseudo-waveform.js.map +1 -0
- package/dist/utils/voice-blob-cache.js +35 -0
- package/dist/utils/voice-blob-cache.js.map +1 -0
- package/dist/utils/voice-playback-chain.js +42 -0
- package/dist/utils/voice-playback-chain.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import React, { useRef, useState, useCallback, useImperativeHandle, useEffect, useLayoutEffect, useMemo } from 'react';
|
|
3
|
+
import { Icon24Pause, Icon24Play } from '@vkontakte/icons';
|
|
4
|
+
import { useVoiceMessagePlayer } from '../../hooks/useVoiceMessagePlayer.js';
|
|
5
|
+
import { VoicePlaybackChain } from '../../utils/voice-playback-chain.js';
|
|
6
|
+
import { PlayerWrapper, PlayButton, LoadingSpinner, ContentColumn, WaveformTrack, WaveformBar, Duration } from './VoiceMessagePlayer.styled.js';
|
|
7
|
+
|
|
8
|
+
const BAR_WIDTH = 3;
|
|
9
|
+
const BAR_GAP = 2;
|
|
10
|
+
const BAR_MIN_HEIGHT = 2;
|
|
11
|
+
const BAR_MAX_HEIGHT = 24;
|
|
12
|
+
const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
13
|
+
const { src, onClick, onEnded, chainId, duration, className, waveformData, autoPreload = true, } = props;
|
|
14
|
+
const trackRef = useRef(null);
|
|
15
|
+
const wrapperRef = useRef(null);
|
|
16
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
17
|
+
const handleEnded = useCallback(() => {
|
|
18
|
+
onEnded === null || onEnded === void 0 ? void 0 : onEnded();
|
|
19
|
+
if (chainId != null) {
|
|
20
|
+
VoicePlaybackChain.playNext(chainId);
|
|
21
|
+
}
|
|
22
|
+
}, [onEnded, chainId]);
|
|
23
|
+
const handleNextTrack = useCallback(() => {
|
|
24
|
+
if (chainId != null) {
|
|
25
|
+
VoicePlaybackChain.playNext(chainId);
|
|
26
|
+
}
|
|
27
|
+
}, [chainId]);
|
|
28
|
+
const handlePreviousTrack = useCallback(() => {
|
|
29
|
+
if (chainId != null) {
|
|
30
|
+
VoicePlaybackChain.playPrevious(chainId);
|
|
31
|
+
}
|
|
32
|
+
}, [chainId]);
|
|
33
|
+
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, seekToTime, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
34
|
+
src,
|
|
35
|
+
duration,
|
|
36
|
+
waveformData,
|
|
37
|
+
preloadedBlob: props.preloadedBlob,
|
|
38
|
+
onEnded: handleEnded,
|
|
39
|
+
onNextTrack: handleNextTrack,
|
|
40
|
+
onPreviousTrack: handlePreviousTrack,
|
|
41
|
+
});
|
|
42
|
+
useImperativeHandle(ref, () => ({ play, prefetch, seekToTime }), [
|
|
43
|
+
play,
|
|
44
|
+
prefetch,
|
|
45
|
+
seekToTime,
|
|
46
|
+
]);
|
|
47
|
+
/** Auto-preload audio when player enters viewport */
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!autoPreload || isLoaded || isLoading) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const el = wrapperRef.current;
|
|
53
|
+
if (!el) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
57
|
+
if (entry.isIntersecting) {
|
|
58
|
+
prefetch();
|
|
59
|
+
observer.disconnect();
|
|
60
|
+
}
|
|
61
|
+
}, { threshold: 0.1 });
|
|
62
|
+
observer.observe(el);
|
|
63
|
+
return () => observer.disconnect();
|
|
64
|
+
}, [
|
|
65
|
+
isLoaded,
|
|
66
|
+
prefetch,
|
|
67
|
+
isLoading,
|
|
68
|
+
autoPreload,
|
|
69
|
+
]);
|
|
70
|
+
/** Register in playback chain for sequential playback */
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (chainId != null) {
|
|
73
|
+
return VoicePlaybackChain.register(chainId, () => play());
|
|
74
|
+
}
|
|
75
|
+
}, [chainId, play]);
|
|
76
|
+
const measureTrackWidth = () => {
|
|
77
|
+
if (trackRef.current) {
|
|
78
|
+
setTrackWidth(trackRef.current.clientWidth);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/** Sync recalc before paint — catches layout shifts */
|
|
82
|
+
useLayoutEffect(measureTrackWidth);
|
|
83
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const el = trackRef.current;
|
|
86
|
+
if (!el) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let rafId = 0;
|
|
90
|
+
const observer = new ResizeObserver(() => {
|
|
91
|
+
cancelAnimationFrame(rafId);
|
|
92
|
+
rafId = requestAnimationFrame(measureTrackWidth);
|
|
93
|
+
});
|
|
94
|
+
observer.observe(el);
|
|
95
|
+
return () => {
|
|
96
|
+
cancelAnimationFrame(rafId);
|
|
97
|
+
observer.disconnect();
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
const displayTime = (isPlaying || currentTime > 0)
|
|
101
|
+
? formatTime(currentTime)
|
|
102
|
+
: formatTime(resolvedDuration);
|
|
103
|
+
const handleTrackClick = useCallback((e) => {
|
|
104
|
+
const track = trackRef.current;
|
|
105
|
+
if (!track) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rect = track.getBoundingClientRect();
|
|
109
|
+
const clickProgress = (e.clientX - rect.left) / rect.width;
|
|
110
|
+
seekTo(Math.max(0, Math.min(1, clickProgress)));
|
|
111
|
+
}, [seekTo]);
|
|
112
|
+
/** Interpolate waveform levels to fill container */
|
|
113
|
+
const interpolatedBars = useMemo(() => {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
const maxBars = trackWidth > 0
|
|
116
|
+
? Math.floor(trackWidth / (BAR_WIDTH + BAR_GAP))
|
|
117
|
+
: 48;
|
|
118
|
+
if (!waveformLevels.length) {
|
|
119
|
+
return Array.from({ length: maxBars }, () => 0);
|
|
120
|
+
}
|
|
121
|
+
const raw = [];
|
|
122
|
+
for (let i = 0; i < maxBars; i++) {
|
|
123
|
+
const srcIndex = (i / maxBars) * waveformLevels.length;
|
|
124
|
+
const low = Math.floor(srcIndex);
|
|
125
|
+
const high = Math.min(low + 1, waveformLevels.length - 1);
|
|
126
|
+
const frac = srcIndex - low;
|
|
127
|
+
raw.push(((_a = waveformLevels[low]) !== null && _a !== void 0 ? _a : 0) * (1 - frac)
|
|
128
|
+
+ ((_b = waveformLevels[high]) !== null && _b !== void 0 ? _b : 0) * frac);
|
|
129
|
+
}
|
|
130
|
+
/** Normalize to 0-100 range so bars use full height */
|
|
131
|
+
const peak = Math.max(...raw, 1);
|
|
132
|
+
return raw.map((v) => (v / peak) * 100);
|
|
133
|
+
}, [waveformLevels, trackWidth]);
|
|
134
|
+
return (jsxs(PlayerWrapper, { ref: wrapperRef, onClick: onClick, className: className, children: [jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsx(LoadingSpinner, {})) : isPlaying ? (jsx(Icon24Pause, {})) : (jsx(Icon24Play, {})) }), jsxs(ContentColumn, { children: [jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
|
|
135
|
+
const normalized = Math.min(100, level);
|
|
136
|
+
const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);
|
|
137
|
+
const barProgress = interpolatedBars.length > 0
|
|
138
|
+
? i / interpolatedBars.length
|
|
139
|
+
: 0;
|
|
140
|
+
const isPlayed = progress > 0 && barProgress <= progress;
|
|
141
|
+
return (jsx(WaveformBar, { style: {
|
|
142
|
+
height: `${height}px`,
|
|
143
|
+
background: isPlayed
|
|
144
|
+
? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 75%, transparent)'
|
|
145
|
+
: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 25%, transparent)',
|
|
146
|
+
} }, i));
|
|
147
|
+
}) }), jsx(Duration, { children: displayTime })] })] }));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export { VoiceMessagePlayer };
|
|
151
|
+
//# sourceMappingURL=VoiceMessagePlayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceMessagePlayer.js","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"sourcesContent":["/**\n * VoiceMessagePlayer\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React, { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nimport { Icon24Pause, Icon24Play } from '@vkontakte/icons';\n\nimport { useVoiceMessagePlayer } from '../../hooks';\n\nimport { VoicePlaybackChain } from '../../utils/voice-playback-chain';\n\nimport {\n ContentColumn,\n Duration,\n LoadingSpinner,\n PlayButton,\n PlayerWrapper,\n WaveformBar,\n WaveformTrack,\n} from './VoiceMessagePlayer.styled';\n\n\nconst BAR_WIDTH = 3;\nconst BAR_GAP = 2;\nconst BAR_MIN_HEIGHT = 2;\nconst BAR_MAX_HEIGHT = 24;\n\nexport interface VoiceMessagePlayerRef {\n play: () => void;\n prefetch: () => void;\n seekToTime: (seconds: number) => void;\n}\n\ninterface Props {\n src: string;\n chainId?: number;\n duration?: number;\n autoPreload?: boolean;\n waveformData?: number[];\n className?: string;\n onEnded?: () => void;\n preloadedBlob?: Blob | null;\n onClick?: (e: React.MouseEvent) => void;\n}\n\n\nconst VoiceMessagePlayer = React.forwardRef<VoiceMessagePlayerRef, Props>((props, ref) => {\n\n const {\n src,\n onClick,\n onEnded,\n chainId,\n duration,\n className,\n waveformData,\n autoPreload = true,\n } = props;\n\n const trackRef = useRef<HTMLDivElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const [ trackWidth, setTrackWidth ] = useState(0);\n\n const handleEnded = useCallback(() => {\n onEnded?.();\n\n if (chainId != null) {\n VoicePlaybackChain.playNext(chainId);\n }\n }, [ onEnded, chainId ]);\n\n const handleNextTrack = useCallback(() => {\n if (chainId != null) {\n VoicePlaybackChain.playNext(chainId);\n }\n }, [ chainId ]);\n\n const handlePreviousTrack = useCallback(() => {\n if (chainId != null) {\n VoicePlaybackChain.playPrevious(chainId);\n }\n }, [ chainId ]);\n\n const {\n play,\n progress,\n isLoaded,\n isPlaying,\n isLoading,\n currentTime,\n waveformLevels,\n seekTo,\n seekToTime,\n prefetch,\n preload,\n formatTime,\n togglePlayback,\n duration: resolvedDuration,\n } = useVoiceMessagePlayer({\n src,\n duration,\n waveformData,\n preloadedBlob: props.preloadedBlob,\n onEnded: handleEnded,\n onNextTrack: handleNextTrack,\n onPreviousTrack: handlePreviousTrack,\n });\n\n useImperativeHandle(\n ref,\n () => ({ play, prefetch, seekToTime }),\n [\n play,\n prefetch,\n seekToTime,\n ],\n );\n\n /** Auto-preload audio when player enters viewport */\n useEffect(() => {\n if (!autoPreload || isLoaded || isLoading) {\n return;\n }\n\n const el = wrapperRef.current;\n\n if (!el) {\n return;\n }\n\n const observer = new IntersectionObserver(\n ([ entry ]) => {\n if (entry.isIntersecting) {\n prefetch();\n observer.disconnect();\n }\n },\n { threshold: 0.1 },\n );\n\n observer.observe(el);\n\n return () => observer.disconnect();\n }, [\n isLoaded,\n prefetch,\n isLoading,\n autoPreload,\n ]);\n\n /** Register in playback chain for sequential playback */\n useEffect(() => {\n if (chainId != null) {\n return VoicePlaybackChain.register(chainId, () => play());\n }\n }, [ chainId, play ]);\n\n const measureTrackWidth = () => {\n if (trackRef.current) {\n setTrackWidth(trackRef.current.clientWidth);\n }\n };\n\n /** Sync recalc before paint — catches layout shifts */\n useLayoutEffect(measureTrackWidth);\n\n /** ResizeObserver for external size changes (window resize, container resize) */\n useEffect(() => {\n const el = trackRef.current;\n if (!el) {\n return;\n }\n\n let rafId = 0;\n\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(measureTrackWidth);\n });\n\n observer.observe(el);\n\n return () => {\n cancelAnimationFrame(rafId);\n observer.disconnect();\n };\n }, []);\n\n const displayTime = (isPlaying || currentTime > 0)\n ? formatTime(currentTime)\n : formatTime(resolvedDuration);\n\n const handleTrackClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n const track = trackRef.current;\n\n if (!track) {\n return;\n }\n\n const rect = track.getBoundingClientRect();\n const clickProgress = (e.clientX - rect.left) / rect.width;\n\n seekTo(Math.max(0, Math.min(1, clickProgress)));\n }, [ seekTo ]);\n\n /** Interpolate waveform levels to fill container */\n const interpolatedBars = useMemo(() => {\n const maxBars = trackWidth > 0\n ? Math.floor(trackWidth / (BAR_WIDTH + BAR_GAP))\n : 48;\n\n if (!waveformLevels.length) {\n return Array.from({ length: maxBars }, () => 0);\n }\n\n const raw: number[] = [];\n\n for (let i = 0; i < maxBars; i++) {\n const srcIndex = (i / maxBars) * waveformLevels.length;\n const low = Math.floor(srcIndex);\n const high = Math.min(low + 1, waveformLevels.length - 1);\n const frac = srcIndex - low;\n\n raw.push(\n (waveformLevels[low] ?? 0) * (1 - frac)\n + (waveformLevels[high] ?? 0) * frac,\n );\n }\n\n /** Normalize to 0-100 range so bars use full height */\n const peak = Math.max(...raw, 1);\n\n return raw.map((v) => (v / peak) * 100);\n }, [ waveformLevels, trackWidth ]);\n\n return (\n <PlayerWrapper ref={wrapperRef} onClick={onClick} className={className}>\n <PlayButton type=\"button\"\n disabled={isLoading || (!src && !props.preloadedBlob)}\n onClick={isLoaded ? togglePlayback : preload}\n aria-label={isPlaying ? 'Pause' : 'Play'}>\n {isLoading ? (\n <LoadingSpinner/>\n ) : isPlaying ? (\n <Icon24Pause/>\n ) : (\n <Icon24Play/>\n )}\n </PlayButton>\n\n <ContentColumn>\n <WaveformTrack ref={trackRef} onClick={handleTrackClick}>\n {interpolatedBars.map((level, i) => {\n const normalized = Math.min(100, level);\n const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);\n const barProgress = interpolatedBars.length > 0\n ? i / interpolatedBars.length\n : 0;\n const isPlayed = progress > 0 && barProgress <= progress;\n\n return (\n <WaveformBar key={i} style={{\n height: `${height}px`,\n background: isPlayed\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 75%, transparent)'\n : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 25%, transparent)',\n }}/>\n );\n })}\n </WaveformTrack>\n\n <Duration>{displayTime}</Duration>\n </ContentColumn>\n </PlayerWrapper>\n );\n});\n\n\nexport { VoiceMessagePlayer };\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VoiceMessagePlayer styles
|
|
5
|
+
*
|
|
6
|
+
* @author: exode <hello@exode.ru>
|
|
7
|
+
*/
|
|
8
|
+
const PlayerWrapper = styled.div `
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
min-width: 200px;
|
|
13
|
+
padding: 2px 0;
|
|
14
|
+
user-select: none;
|
|
15
|
+
`;
|
|
16
|
+
const PlayButton = styled.button `
|
|
17
|
+
width: 44px;
|
|
18
|
+
height: 44px;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
border-radius: 22px;
|
|
23
|
+
border: 0;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
background: var(--white, #ffffff);
|
|
27
|
+
color: var(--accent, #3f8ae0);
|
|
28
|
+
transition: transform 140ms ease, box-shadow 140ms ease;
|
|
29
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
30
|
+
padding: 0;
|
|
31
|
+
|
|
32
|
+
&:hover:not(:disabled) {
|
|
33
|
+
transform: scale(1.04);
|
|
34
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:active:not(:disabled) {
|
|
38
|
+
transform: scale(0.97);
|
|
39
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.08);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:disabled {
|
|
43
|
+
opacity: 0.5;
|
|
44
|
+
cursor: not-allowed;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
const ContentColumn = styled.div `
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
gap: 4px;
|
|
53
|
+
`;
|
|
54
|
+
const WaveformTrack = styled.div `
|
|
55
|
+
height: 28px;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: 2px;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
min-width: 0;
|
|
62
|
+
`;
|
|
63
|
+
const WaveformBar = styled.div `
|
|
64
|
+
width: 3px;
|
|
65
|
+
border-radius: 1.5px;
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
transition: background 100ms ease;
|
|
68
|
+
`;
|
|
69
|
+
const Duration = styled.span `
|
|
70
|
+
font-size: 12px;
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
line-height: 1;
|
|
73
|
+
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent);
|
|
74
|
+
font-variant-numeric: tabular-nums;
|
|
75
|
+
`;
|
|
76
|
+
const LoadingSpinner = styled.span `
|
|
77
|
+
width: 18px;
|
|
78
|
+
height: 18px;
|
|
79
|
+
display: inline-block;
|
|
80
|
+
border-radius: 50%;
|
|
81
|
+
border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);
|
|
82
|
+
border-top-color: var(--accent, #3f8ae0);
|
|
83
|
+
animation: voicePlayerSpin 0.7s linear infinite;
|
|
84
|
+
|
|
85
|
+
@keyframes voicePlayerSpin {
|
|
86
|
+
from { transform: rotate(0deg); }
|
|
87
|
+
to { transform: rotate(360deg); }
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
export { ContentColumn, Duration, LoadingSpinner, PlayButton, PlayerWrapper, WaveformBar, WaveformTrack };
|
|
92
|
+
//# sourceMappingURL=VoiceMessagePlayer.styled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceMessagePlayer.styled.js","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.ts"],"sourcesContent":["/**\n * VoiceMessagePlayer styles\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport styled from 'styled-components';\n\n\nexport const PlayerWrapper = styled.div`\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 200px;\n padding: 2px 0;\n user-select: none;\n`;\n\nexport const PlayButton = styled.button`\n width: 44px;\n height: 44px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 22px;\n border: 0;\n cursor: pointer;\n flex-shrink: 0;\n background: var(--white, #ffffff);\n color: var(--accent, #3f8ae0);\n transition: transform 140ms ease, box-shadow 140ms ease;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n padding: 0;\n\n &:hover:not(:disabled) {\n transform: scale(1.04);\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);\n }\n\n &:active:not(:disabled) {\n transform: scale(0.97);\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.08);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n`;\n\nexport const ContentColumn = styled.div`\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n gap: 4px;\n`;\n\nexport const WaveformTrack = styled.div`\n height: 28px;\n display: flex;\n align-items: center;\n gap: 2px;\n overflow: hidden;\n cursor: pointer;\n min-width: 0;\n`;\n\nexport const WaveformBar = styled.div`\n width: 3px;\n border-radius: 1.5px;\n flex-shrink: 0;\n transition: background 100ms ease;\n`;\n\nexport const Duration = styled.span`\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent);\n font-variant-numeric: tabular-nums;\n`;\n\nexport const LoadingSpinner = styled.span`\n width: 18px;\n height: 18px;\n display: inline-block;\n border-radius: 50%;\n border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);\n border-top-color: var(--accent, #3f8ae0);\n animation: voicePlayerSpin 0.7s linear infinite;\n\n @keyframes voicePlayerSpin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n }\n`;"],"names":[],"mappings":";;AAAA;;;;AAIG;AAKI,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;AAShC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgChC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;AAQhC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;AAUhC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;AAO9B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;AAQ5B,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import 'react';
|
|
3
|
+
import { TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH, TIMELINE_BAR_MAX_HEIGHT } from '../../constants.js';
|
|
4
|
+
|
|
5
|
+
const PreviewBars = (props) => {
|
|
6
|
+
var _a, _b;
|
|
7
|
+
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
|
|
8
|
+
const maxBars = visibleBarsCount;
|
|
9
|
+
const sourceLevels = timelineLevels.length > 0
|
|
10
|
+
? timelineLevels
|
|
11
|
+
: previewLevels;
|
|
12
|
+
if (!sourceLevels.length) {
|
|
13
|
+
return (jsx(Fragment, { children: Array.from({ length: maxBars }, (_, i) => (jsx("div", { style: {
|
|
14
|
+
width: `${TIMELINE_BAR_WIDTH}px`,
|
|
15
|
+
height: `${TIMELINE_BAR_MIN_HEIGHT}px`,
|
|
16
|
+
borderRadius: '9999px',
|
|
17
|
+
flexShrink: 0,
|
|
18
|
+
background: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 15%, transparent)',
|
|
19
|
+
} }, i))) }));
|
|
20
|
+
}
|
|
21
|
+
/** Interpolate source levels to fill container */
|
|
22
|
+
const bars = [];
|
|
23
|
+
for (let i = 0; i < maxBars; i++) {
|
|
24
|
+
const srcIndex = (i / maxBars) * sourceLevels.length;
|
|
25
|
+
const low = Math.floor(srcIndex);
|
|
26
|
+
const high = Math.min(low + 1, sourceLevels.length - 1);
|
|
27
|
+
const frac = srcIndex - low;
|
|
28
|
+
bars.push(((_a = sourceLevels[low]) !== null && _a !== void 0 ? _a : 0) * (1 - frac) + ((_b = sourceLevels[high]) !== null && _b !== void 0 ? _b : 0) * frac);
|
|
29
|
+
}
|
|
30
|
+
const totalDuration = recordingTime || lastDuration || 1;
|
|
31
|
+
const progress = isPlaying ? Math.min(1, playbackTime / totalDuration) : 0;
|
|
32
|
+
return (jsx(Fragment, { children: bars.map((level, i) => {
|
|
33
|
+
const normalized = Math.min(100, level);
|
|
34
|
+
const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);
|
|
35
|
+
const isPlayed = progress > 0 && (i / maxBars) <= progress;
|
|
36
|
+
return (jsx("div", { style: {
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
height: `${height}px`,
|
|
39
|
+
borderRadius: '9999px',
|
|
40
|
+
width: `${TIMELINE_BAR_WIDTH}px`,
|
|
41
|
+
background: !isPlaying
|
|
42
|
+
? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'
|
|
43
|
+
: isPlayed
|
|
44
|
+
? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 80%, transparent)'
|
|
45
|
+
: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 22%, transparent)',
|
|
46
|
+
transition: 'background 80ms ease',
|
|
47
|
+
} }, i));
|
|
48
|
+
}) }));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { PreviewBars };
|
|
52
|
+
//# sourceMappingURL=PreviewBars.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PreviewBars.js","sources":["../../../src/components/VoiceRecorderInput/PreviewBars.tsx"],"sourcesContent":["/**\n * PreviewBars — renders waveform bars in preview mode (paused/stopped)\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React from 'react';\n\nimport { TIMELINE_BAR_MAX_HEIGHT, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH } from '../../constants';\n\n\ninterface PreviewBarsProps {\n isPlaying: boolean;\n playbackTime: number;\n lastDuration: number;\n recordingTime: number;\n previewLevels: number[];\n timelineLevels: number[];\n visibleBarsCount: number;\n}\n\n\nconst PreviewBars = (props: PreviewBarsProps) => {\n\n const {\n isPlaying,\n playbackTime,\n lastDuration,\n previewLevels,\n recordingTime,\n timelineLevels,\n visibleBarsCount,\n } = props;\n\n const maxBars = visibleBarsCount;\n\n const sourceLevels = timelineLevels.length > 0\n ? timelineLevels\n : previewLevels;\n\n if (!sourceLevels.length) {\n return (\n <>\n {Array.from({ length: maxBars }, (_, i) => (\n <div key={i} style={{\n width: `${TIMELINE_BAR_WIDTH}px`,\n height: `${TIMELINE_BAR_MIN_HEIGHT}px`,\n borderRadius: '9999px',\n flexShrink: 0,\n background: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 15%, transparent)',\n }}/>\n ))}\n </>\n );\n }\n\n /** Interpolate source levels to fill container */\n const bars: number[] = [];\n\n for (let i = 0; i < maxBars; i++) {\n const srcIndex = (i / maxBars) * sourceLevels.length;\n const low = Math.floor(srcIndex);\n\n const high = Math.min(low + 1, sourceLevels.length - 1);\n const frac = srcIndex - low;\n\n bars.push((sourceLevels[low] ?? 0) * (1 - frac) + (sourceLevels[high] ?? 0) * frac);\n }\n\n const totalDuration = recordingTime || lastDuration || 1;\n const progress = isPlaying ? Math.min(1, playbackTime / totalDuration) : 0;\n\n return (\n <>\n {bars.map((level, i) => {\n const normalized = Math.min(100, level);\n const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);\n const isPlayed = progress > 0 && (i / maxBars) <= progress;\n\n return (\n <div key={i} style={{\n flexShrink: 0,\n height: `${height}px`,\n borderRadius: '9999px',\n width: `${TIMELINE_BAR_WIDTH}px`,\n background: !isPlaying\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'\n : isPlayed\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 80%, transparent)'\n : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 22%, transparent)',\n transition: 'background 80ms ease',\n }}/>\n );\n })}\n </>\n );\n};\n\n\nexport { PreviewBars };\n"],"names":["_jsx","_Fragment"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import 'react';
|
|
3
|
+
import { TIMELINE_BAR_WIDTH, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_MAX_HEIGHT } from '../../constants.js';
|
|
4
|
+
|
|
5
|
+
const TimelineBars = (props) => {
|
|
6
|
+
const { timelineLevels, visibleBarsCount } = props;
|
|
7
|
+
const maxBars = visibleBarsCount;
|
|
8
|
+
const visible = timelineLevels.slice(-maxBars);
|
|
9
|
+
const padded = [
|
|
10
|
+
...visible,
|
|
11
|
+
...Array.from({ length: Math.max(0, maxBars - visible.length) }, () => 0),
|
|
12
|
+
];
|
|
13
|
+
return (jsx(Fragment, { children: padded.map((level, i) => {
|
|
14
|
+
const normalized = Math.min(100, level);
|
|
15
|
+
const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);
|
|
16
|
+
const hasValue = i < visible.length;
|
|
17
|
+
return (jsx("div", { style: {
|
|
18
|
+
width: `${TIMELINE_BAR_WIDTH}px`,
|
|
19
|
+
height: `${height}px`,
|
|
20
|
+
borderRadius: '9999px',
|
|
21
|
+
flexShrink: 0,
|
|
22
|
+
transition: 'height 120ms ease',
|
|
23
|
+
background: hasValue
|
|
24
|
+
? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'
|
|
25
|
+
: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 18%, transparent)',
|
|
26
|
+
} }, i));
|
|
27
|
+
}) }));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { TimelineBars };
|
|
31
|
+
//# sourceMappingURL=TimelineBars.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimelineBars.js","sources":["../../../src/components/VoiceRecorderInput/TimelineBars.tsx"],"sourcesContent":["/**\n * TimelineBars — renders waveform bars during active recording\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React from 'react';\n\nimport { TIMELINE_BAR_MAX_HEIGHT, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH } from '../../constants';\n\n\ninterface TimelineBarsProps {\n timelineLevels: number[];\n visibleBarsCount: number;\n}\n\n\nconst TimelineBars = (props: TimelineBarsProps) => {\n\n const { timelineLevels, visibleBarsCount } = props;\n\n const maxBars = visibleBarsCount;\n const visible = timelineLevels.slice(-maxBars);\n\n const padded = [\n ...visible,\n ...Array.from({ length: Math.max(0, maxBars - visible.length) }, () => 0),\n ];\n\n return (\n <>\n {padded.map((level, i) => {\n const normalized = Math.min(100, level);\n const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);\n const hasValue = i < visible.length;\n\n return (\n <div key={i} style={{\n width: `${TIMELINE_BAR_WIDTH}px`,\n height: `${height}px`,\n borderRadius: '9999px',\n flexShrink: 0,\n transition: 'height 120ms ease',\n background: hasValue\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'\n : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 18%, transparent)',\n }}/>\n );\n })}\n </>\n );\n};\n\n\nexport { TimelineBars };\n"],"names":["_jsx","_Fragment"],"mappings":";;;;AAiBA,MAAM,YAAY,GAAG,CAAC,KAAwB,KAAI;AAE9C,IAAA,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,KAAK;IAElD,MAAM,OAAO,GAAG,gBAAgB;IAChC,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AAE9C,IAAA,MAAM,MAAM,GAAG;AACX,QAAA,GAAG,OAAO;QACV,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;KAC5E;IAED,QACIA,GAAA,CAAAC,QAAA,EAAA,EAAA,QAAA,EACK,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAI;YACrB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC;AACvC,YAAA,MAAM,MAAM,GAAG,uBAAuB,GAAG,CAAC,UAAU,GAAG,GAAG,KAAK,uBAAuB,GAAG,uBAAuB,CAAC;AACjH,YAAA,MAAM,QAAQ,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM;YAEnC,QACID,GAAA,CAAA,KAAA,EAAA,EAAa,KAAK,EAAE;oBAChB,KAAK,EAAE,CAAA,EAAG,kBAAkB,CAAA,EAAA,CAAI;oBAChC,MAAM,EAAE,CAAA,EAAG,MAAM,CAAA,EAAA,CAAI;AACrB,oBAAA,YAAY,EAAE,QAAQ;AACtB,oBAAA,UAAU,EAAE,CAAC;AACb,oBAAA,UAAU,EAAE,mBAAmB;AAC/B,oBAAA,UAAU,EAAE;AACR,0BAAE;AACF,0BAAE,mEAAmE;iBAC5E,EAAA,EATS,CAAC,CASP;QAEZ,CAAC,CAAC,EAAA,CACH;AAEX;;;;"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useRef, useState, useLayoutEffect, useEffect } from 'react';
|
|
3
|
+
import { Icon28DeleteOutlineAndroid, Icon20Voice, Icon16Pause, Icon16Play, Icon28SendOutline } from '@vkontakte/icons';
|
|
4
|
+
import { TIMELINE_BAR_WIDTH, TIMELINE_BAR_GAP } from '../../constants.js';
|
|
5
|
+
import { useVoiceRecorderInput } from '../../hooks/useVoiceRecorderInput.js';
|
|
6
|
+
import { PreviewBars } from './PreviewBars.js';
|
|
7
|
+
import { TimelineBars } from './TimelineBars.js';
|
|
8
|
+
import { Wrapper, CancelButton, RecordTrack, ButtonSlot, MicIndicator, PlayPauseButton, StopButton, WaveformArea, Timer, SendButton, SpinnerIcon } from './VoiceRecorderInput.styled.js';
|
|
9
|
+
|
|
10
|
+
const VoiceRecorderInput = (props) => {
|
|
11
|
+
const { className, ...inputOptions } = props;
|
|
12
|
+
const waveformRef = useRef(null);
|
|
13
|
+
const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
|
|
14
|
+
const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
|
|
15
|
+
const [visibleBarsCount, setVisibleBarsCount] = useState(0);
|
|
16
|
+
const measureBars = () => {
|
|
17
|
+
const el = waveformRef.current;
|
|
18
|
+
if (!el) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
|
|
22
|
+
};
|
|
23
|
+
/** Sync recalc before paint — catches layout shifts from button changes */
|
|
24
|
+
useLayoutEffect(measureBars);
|
|
25
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const el = waveformRef.current;
|
|
28
|
+
if (!el) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let rafId = 0;
|
|
32
|
+
const observer = new ResizeObserver(() => {
|
|
33
|
+
cancelAnimationFrame(rafId);
|
|
34
|
+
rafId = requestAnimationFrame(measureBars);
|
|
35
|
+
});
|
|
36
|
+
observer.observe(el);
|
|
37
|
+
return () => {
|
|
38
|
+
cancelAnimationFrame(rafId);
|
|
39
|
+
observer.disconnect();
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
const displayTime = (isPreview && isPlaying)
|
|
43
|
+
? formatTime(playbackTime)
|
|
44
|
+
: formatTime(recordingTime || lastDurationRef.current);
|
|
45
|
+
return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 28, height: 28, style: { minWidth: 28 } }) }), jsxs(RecordTrack, { children: [jsx(ButtonSlot, { children: isPreview ? (jsxs(Fragment, { children: [jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsx(Icon20Voice, { width: 14, height: 14 }) }), jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
|
|
46
|
+
? jsx(Icon16Pause, { width: 14, height: 14 })
|
|
47
|
+
: jsx(Icon16Play, { width: 14, height: 14 }) })] })) : (jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsx(Icon16Pause, { width: 14, height: 14 }) })) }), jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, visibleBarsCount: visibleBarsCount })) : (jsx(TimelineBars, { timelineLevels: timelineLevels, visibleBarsCount: visibleBarsCount })) }), jsx(Timer, { children: displayTime })] }), jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsx(SpinnerIcon, {})) : (jsx(Icon28SendOutline, {})) })] }));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { VoiceRecorderInput };
|
|
51
|
+
//# sourceMappingURL=VoiceRecorderInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceRecorderInput.js","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.tsx"],"sourcesContent":["/**\n * VoiceRecorderInput\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React, { useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nimport { Icon16Pause, Icon16Play, Icon20Voice, Icon28DeleteOutlineAndroid, Icon28SendOutline } from '@vkontakte/icons';\n\nimport type { UploadStrategy } from '../../types';\nimport { TIMELINE_BAR_GAP, TIMELINE_BAR_WIDTH } from '../../constants';\n\nimport { useVoiceRecorderInput } from '../../hooks/useVoiceRecorderInput';\n\nimport { PreviewBars } from './PreviewBars';\nimport { TimelineBars } from './TimelineBars';\n\nimport {\n ButtonSlot,\n CancelButton,\n MicIndicator,\n PlayPauseButton,\n RecordTrack,\n SendButton,\n SpinnerIcon,\n StopButton,\n Timer,\n WaveformArea,\n Wrapper,\n} from './VoiceRecorderInput.styled';\n\n\ninterface Props {\n uploader?: UploadStrategy<unknown>;\n onUploaded?: (file: unknown) => void | Promise<void>;\n onCancel?: () => void;\n onSend?: (file: File, duration: number) => void | Promise<void>;\n onError?: (error: unknown) => void;\n onPermissionDenied?: (error: unknown) => void;\n autoStart?: boolean;\n mimeType?: string;\n fileName?: string;\n className?: string;\n}\n\n\nconst VoiceRecorderInput = (props: Props) => {\n\n const { className, ...inputOptions } = props;\n\n const waveformRef = useRef<HTMLDivElement>(null);\n\n const {\n recorder,\n isSending,\n isPreview,\n timelineLevels,\n lastDurationRef,\n handleResume,\n handlePause,\n handleCancel,\n handleSendRequest,\n } = useVoiceRecorderInput(inputOptions);\n\n const {\n isPlaying,\n isRecording,\n recordingTime,\n hasRecording,\n previewLevels,\n playbackTime,\n formatTime,\n togglePlayback,\n } = recorder;\n\n const [ visibleBarsCount, setVisibleBarsCount ] = useState(0);\n\n const measureBars = () => {\n const el = waveformRef.current;\n if (!el) {\n return;\n }\n setVisibleBarsCount(\n Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)),\n );\n };\n\n /** Sync recalc before paint — catches layout shifts from button changes */\n useLayoutEffect(measureBars);\n\n /** ResizeObserver for external size changes (window resize, container resize) */\n useEffect(() => {\n const el = waveformRef.current;\n if (!el) {\n return;\n }\n\n let rafId = 0;\n\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(measureBars);\n });\n\n observer.observe(el);\n\n return () => {\n cancelAnimationFrame(rafId);\n observer.disconnect();\n };\n }, []);\n\n const displayTime = (isPreview && isPlaying)\n ? formatTime(playbackTime)\n : formatTime(recordingTime || lastDurationRef.current);\n\n return (\n <Wrapper className={className}>\n <CancelButton type=\"button\" onClick={handleCancel} aria-label=\"Cancel\">\n <Icon28DeleteOutlineAndroid fill=\"var(--exode_red)\" width={28} height={28} style={{ minWidth: 28 }}/>\n </CancelButton>\n\n <RecordTrack>\n <ButtonSlot>\n {isPreview ? (\n <>\n <MicIndicator type=\"button\" onClick={handleResume} aria-label=\"Resume recording\">\n <Icon20Voice width={14} height={14}/>\n </MicIndicator>\n\n <PlayPauseButton type=\"button\"\n onClick={togglePlayback}\n aria-label={isPlaying ? 'Pause' : 'Play'}>\n {isPlaying\n ? <Icon16Pause width={14} height={14}/>\n : <Icon16Play width={14} height={14}/>\n }\n </PlayPauseButton>\n </>\n ) : (\n <StopButton type=\"button\" onClick={handlePause} aria-label=\"Pause recording\">\n <Icon16Pause width={14} height={14}/>\n </StopButton>\n )}\n </ButtonSlot>\n\n <WaveformArea ref={waveformRef}>\n {isPreview ? (\n <PreviewBars timelineLevels={timelineLevels}\n previewLevels={previewLevels}\n isPlaying={isPlaying}\n playbackTime={playbackTime}\n recordingTime={recordingTime}\n lastDuration={lastDurationRef.current}\n visibleBarsCount={visibleBarsCount}/>\n ) : (\n <TimelineBars timelineLevels={timelineLevels} visibleBarsCount={visibleBarsCount}/>\n )}\n </WaveformArea>\n\n <Timer>\n {displayTime}\n </Timer>\n </RecordTrack>\n\n <SendButton type=\"button\"\n onClick={handleSendRequest}\n disabled={isSending || (!hasRecording && !isRecording)}\n aria-label=\"Send\">\n {isSending ? (\n <SpinnerIcon/>\n ) : (\n <Icon28SendOutline/>\n )}\n </SendButton>\n </Wrapper>\n );\n};\n\n\nexport { VoiceRecorderInput };\n"],"names":["_jsxs","_jsx","_Fragment"],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VoiceRecorderInput styles
|
|
5
|
+
*
|
|
6
|
+
* @author: exode <hello@exode.ru>
|
|
7
|
+
*/
|
|
8
|
+
const Wrapper = styled.div `
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
width: 100%;
|
|
12
|
+
gap: 6px;
|
|
13
|
+
padding: 0 4px;
|
|
14
|
+
color: var(--text_primary, #0a0a0a);
|
|
15
|
+
`;
|
|
16
|
+
const CancelButton = styled.button `
|
|
17
|
+
width: 32px;
|
|
18
|
+
height: 32px;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
border-radius: 16px;
|
|
23
|
+
background: transparent;
|
|
24
|
+
border: 0;
|
|
25
|
+
margin-right: 4px;
|
|
26
|
+
flex-shrink: 0;
|
|
27
|
+
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
transition: color 160ms ease, background-color 160ms ease;
|
|
30
|
+
`;
|
|
31
|
+
const RecordTrack = styled.div `
|
|
32
|
+
flex: 1;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 4px;
|
|
36
|
+
padding: 4px 10px 4px 4px;
|
|
37
|
+
border-radius: 9999px;
|
|
38
|
+
background: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
|
|
39
|
+
min-width: 0;
|
|
40
|
+
`;
|
|
41
|
+
/** Container for control buttons — auto-sized, no fixed width */
|
|
42
|
+
const ButtonSlot = styled.div `
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 4px;
|
|
46
|
+
padding: 2px;
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
`;
|
|
49
|
+
const StopButton = styled.button `
|
|
50
|
+
width: 26px;
|
|
51
|
+
height: 26px;
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
border-radius: 13px;
|
|
56
|
+
border: 0;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
background: var(--background_content, #ffffff);
|
|
60
|
+
color: var(--exode_red, #ed1c24);
|
|
61
|
+
transition: transform 120ms ease;
|
|
62
|
+
|
|
63
|
+
&:hover:not(:disabled) {
|
|
64
|
+
transform: scale(1.05);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&:active:not(:disabled) {
|
|
68
|
+
transform: scale(0.96);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&:disabled {
|
|
72
|
+
opacity: 0.4;
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const WaveformArea = styled.div `
|
|
77
|
+
flex: 1;
|
|
78
|
+
height: 28px;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: flex-end;
|
|
81
|
+
justify-content: flex-start;
|
|
82
|
+
gap: 2px;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
min-width: 0;
|
|
85
|
+
padding-bottom: 2px;
|
|
86
|
+
`;
|
|
87
|
+
const Timer = styled.span `
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
font-weight: 500;
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 60%, transparent);
|
|
92
|
+
font-variant-numeric: tabular-nums;
|
|
93
|
+
`;
|
|
94
|
+
const SendButton = styled.button `
|
|
95
|
+
width: 32px;
|
|
96
|
+
height: 32px;
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
border-radius: 16px;
|
|
101
|
+
border: 0;
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
background: transparent;
|
|
105
|
+
color: var(--accent, #3f8ae0);
|
|
106
|
+
transition: opacity 140ms ease;
|
|
107
|
+
padding: 0;
|
|
108
|
+
|
|
109
|
+
&:hover:not(:disabled) {
|
|
110
|
+
opacity: 0.75;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&:disabled {
|
|
114
|
+
opacity: 0.35;
|
|
115
|
+
cursor: not-allowed;
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
const MicIndicator = styled.button `
|
|
119
|
+
width: 26px;
|
|
120
|
+
height: 26px;
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
border-radius: 13px;
|
|
125
|
+
border: 0;
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
background: var(--background_content, #ffffff);
|
|
129
|
+
color: color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%);
|
|
130
|
+
transition: background-color 140ms ease, transform 120ms ease;
|
|
131
|
+
|
|
132
|
+
&:active {
|
|
133
|
+
transform: scale(0.94);
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
const PlayPauseButton = styled.button `
|
|
137
|
+
width: 26px;
|
|
138
|
+
height: 26px;
|
|
139
|
+
display: inline-flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
border-radius: 13px;
|
|
143
|
+
border: 0;
|
|
144
|
+
flex-shrink: 0;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
background: var(--background_content, #ffffff);
|
|
147
|
+
color: var(--text_primary, #0a0a0a);
|
|
148
|
+
transition: transform 120ms ease;
|
|
149
|
+
|
|
150
|
+
&:hover {
|
|
151
|
+
transform: scale(1.06);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
&:active {
|
|
155
|
+
transform: scale(0.94);
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
const SpinnerIcon = styled.span `
|
|
159
|
+
width: 20px;
|
|
160
|
+
height: 20px;
|
|
161
|
+
display: inline-block;
|
|
162
|
+
border-radius: 50%;
|
|
163
|
+
border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);
|
|
164
|
+
border-top-color: var(--accent, #3f8ae0);
|
|
165
|
+
border-right-color: var(--accent, #3f8ae0);
|
|
166
|
+
animation: voiceRecorderSpin 0.75s linear infinite;
|
|
167
|
+
|
|
168
|
+
@keyframes voiceRecorderSpin {
|
|
169
|
+
from { transform: rotate(0deg); }
|
|
170
|
+
to { transform: rotate(360deg); }
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
export { ButtonSlot, CancelButton, MicIndicator, PlayPauseButton, RecordTrack, SendButton, SpinnerIcon, StopButton, Timer, WaveformArea, Wrapper };
|
|
175
|
+
//# sourceMappingURL=VoiceRecorderInput.styled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceRecorderInput.styled.js","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.styled.ts"],"sourcesContent":["/**\n * VoiceRecorderInput styles\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport styled from 'styled-components';\n\n\nexport const Wrapper = styled.div`\n display: flex;\n align-items: center;\n width: 100%;\n gap: 6px;\n padding: 0 4px;\n color: var(--text_primary, #0a0a0a);\n`;\n\nexport const CancelButton = styled.button`\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 16px;\n background: transparent;\n border: 0;\n margin-right: 4px;\n flex-shrink: 0;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);\n cursor: pointer;\n transition: color 160ms ease, background-color 160ms ease;\n`;\n\nexport const RecordTrack = styled.div`\n flex: 1;\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 10px 4px 4px;\n border-radius: 9999px;\n background: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);\n min-width: 0;\n`;\n\n/** Container for control buttons — auto-sized, no fixed width */\nexport const ButtonSlot = styled.div`\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n flex-shrink: 0;\n`;\n\nexport const StopButton = styled.button`\n width: 26px;\n height: 26px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 13px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: var(--background_content, #ffffff);\n color: var(--exode_red, #ed1c24);\n transition: transform 120ms ease;\n\n &:hover:not(:disabled) {\n transform: scale(1.05);\n }\n\n &:active:not(:disabled) {\n transform: scale(0.96);\n }\n\n &:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n`;\n\nexport const WaveformArea = styled.div`\n flex: 1;\n height: 28px;\n display: flex;\n align-items: flex-end;\n justify-content: flex-start;\n gap: 2px;\n overflow: hidden;\n min-width: 0;\n padding-bottom: 2px;\n`;\n\nexport const Timer = styled.span`\n font-size: 13px;\n font-weight: 500;\n flex-shrink: 0;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 60%, transparent);\n font-variant-numeric: tabular-nums;\n`;\n\nexport const SendButton = styled.button`\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 16px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: transparent;\n color: var(--accent, #3f8ae0);\n transition: opacity 140ms ease;\n padding: 0;\n\n &:hover:not(:disabled) {\n opacity: 0.75;\n }\n\n &:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n }\n`;\n\nexport const MicIndicator = styled.button`\n width: 26px;\n height: 26px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 13px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: var(--background_content, #ffffff);\n color: color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%);\n transition: background-color 140ms ease, transform 120ms ease;\n\n &:active {\n transform: scale(0.94);\n }\n`;\n\nexport const PlayPauseButton = styled.button`\n width: 26px;\n height: 26px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 13px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: var(--background_content, #ffffff);\n color: var(--text_primary, #0a0a0a);\n transition: transform 120ms ease;\n\n &:hover {\n transform: scale(1.06);\n }\n\n &:active {\n transform: scale(0.94);\n }\n`;\n\nexport const SpinnerIcon = styled.span`\n width: 20px;\n height: 20px;\n display: inline-block;\n border-radius: 50%;\n border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);\n border-top-color: var(--accent, #3f8ae0);\n border-right-color: var(--accent, #3f8ae0);\n animation: voiceRecorderSpin 0.75s linear infinite;\n\n @keyframes voiceRecorderSpin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n }\n`;\n"],"names":[],"mappings":";;AAAA;;;;AAIG;AAKI,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;AAS1B,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;AAgBlC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;AAWrC;AACO,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;AAQ7B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;AAY/B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;AAQzB,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AAyBhC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;AAmBlC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;AAuBrC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;;"}
|