@exode-team/react-recorder 1.1.2 → 1.1.3
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.styled.d.ts.map +1 -1
- package/dist/components/RecordingShell/RecordingShell.d.ts.map +1 -1
- package/dist/components/RecordingShell/RecordingShell.styled.d.ts.map +1 -1
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/PreviewBars.d.ts +1 -1
- package/dist/components/VoiceRecorderInput/PreviewBars.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/TimelineBars.d.ts +1 -1
- package/dist/components/VoiceRecorderInput/TimelineBars.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.d.ts.map +1 -1
- package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
- package/dist/index.cjs +76 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +78 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import React, { useState, useRef, useCallback, useEffect, useMemo,
|
|
2
|
+
import React, { useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useImperativeHandle } from 'react';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
|
-
import { Icon32Microphone, Icon16Microphone,
|
|
4
|
+
import { Icon32Microphone, Icon16Microphone, Icon16Pause, Icon16Play, Icon28DeleteOutlineAndroid, Icon28SendOutline, Icon20Voice, Icon24Pause, Icon24Play } from '@vkontakte/icons';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Math utilities
|
|
@@ -782,6 +782,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
782
782
|
const audioRef = useRef(null);
|
|
783
783
|
const objectUrlRef = useRef(null);
|
|
784
784
|
const abortControllerRef = useRef(null);
|
|
785
|
+
const failedSrcRef = useRef(null);
|
|
785
786
|
const callbacksRef = useRef(playbackCallbacks);
|
|
786
787
|
callbacksRef.current = playbackCallbacks;
|
|
787
788
|
const createAudioFromBlob = useCallback((blob) => {
|
|
@@ -818,7 +819,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
818
819
|
}, [waveformData, createAudioFromBlob]);
|
|
819
820
|
const ensureAudioLoaded = useCallback(async () => {
|
|
820
821
|
var _a;
|
|
821
|
-
if (audioRef.current) {
|
|
822
|
+
if (audioRef.current || failedSrcRef.current === src) {
|
|
822
823
|
return;
|
|
823
824
|
}
|
|
824
825
|
if (preloadedBlob) {
|
|
@@ -849,6 +850,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
849
850
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
850
851
|
return;
|
|
851
852
|
}
|
|
853
|
+
failedSrcRef.current = src;
|
|
852
854
|
console.warn('[voice-player] Failed to load audio', error);
|
|
853
855
|
}
|
|
854
856
|
finally {
|
|
@@ -859,6 +861,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
859
861
|
}, [src, waveformData, preloadedBlob, mountFromBlob, createAudioFromBlob]);
|
|
860
862
|
/** Cleanup on src change or unmount */
|
|
861
863
|
useEffect(() => {
|
|
864
|
+
failedSrcRef.current = null;
|
|
862
865
|
return () => {
|
|
863
866
|
var _a;
|
|
864
867
|
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
@@ -1188,7 +1191,6 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1188
1191
|
const Container = styled.div `
|
|
1189
1192
|
position: relative;
|
|
1190
1193
|
width: 100%;
|
|
1191
|
-
padding: 16px;
|
|
1192
1194
|
transition: color 200ms ease, background-color 200ms ease;
|
|
1193
1195
|
color: var(--text_primary, #0a0a0a);
|
|
1194
1196
|
`;
|
|
@@ -1273,29 +1275,28 @@ const Wrapper$1 = styled.div `
|
|
|
1273
1275
|
gap: 12px;
|
|
1274
1276
|
padding: 10px 14px;
|
|
1275
1277
|
border-radius: 9999px;
|
|
1276
|
-
background: var(--
|
|
1277
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent);
|
|
1278
|
+
background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
|
|
1278
1279
|
`;
|
|
1279
1280
|
const CancelButton$1 = styled.button `
|
|
1280
|
-
width:
|
|
1281
|
-
height:
|
|
1281
|
+
width: 38px;
|
|
1282
|
+
height: 38px;
|
|
1282
1283
|
display: inline-flex;
|
|
1283
1284
|
align-items: center;
|
|
1284
1285
|
justify-content: center;
|
|
1285
|
-
border-radius:
|
|
1286
|
-
background: transparent;
|
|
1286
|
+
border-radius: 19px;
|
|
1287
|
+
background: color-mix(in srgb, var(--exode_red) 10%, transparent);
|
|
1287
1288
|
border: 0;
|
|
1289
|
+
flex-shrink: 0;
|
|
1288
1290
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
|
|
1289
1291
|
cursor: pointer;
|
|
1290
|
-
transition:
|
|
1292
|
+
transition: transform 160ms ease;
|
|
1291
1293
|
|
|
1292
|
-
&:hover {
|
|
1293
|
-
|
|
1294
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 76%, transparent);
|
|
1294
|
+
&:hover:not(:disabled) {
|
|
1295
|
+
transform: translateY(-1px);
|
|
1295
1296
|
}
|
|
1296
1297
|
|
|
1297
|
-
&:active {
|
|
1298
|
-
|
|
1298
|
+
&:active:not(:disabled) {
|
|
1299
|
+
transform: translateY(0);
|
|
1299
1300
|
}
|
|
1300
1301
|
`;
|
|
1301
1302
|
const Track = styled.div `
|
|
@@ -1303,7 +1304,7 @@ const Track = styled.div `
|
|
|
1303
1304
|
display: flex;
|
|
1304
1305
|
align-items: center;
|
|
1305
1306
|
gap: 12px;
|
|
1306
|
-
padding: 8px
|
|
1307
|
+
padding: 8px 10px;
|
|
1307
1308
|
border-radius: 9999px;
|
|
1308
1309
|
background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
|
|
1309
1310
|
`;
|
|
@@ -1467,7 +1468,9 @@ const RecordingShell = (props) => {
|
|
|
1467
1468
|
return formatTime(0);
|
|
1468
1469
|
}, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
|
|
1469
1470
|
const recordButtonDisabled = isSending;
|
|
1470
|
-
const recordButtonIcon = !isRecording || isPaused
|
|
1471
|
+
const recordButtonIcon = !isRecording || isPaused
|
|
1472
|
+
? jsx(Icon16Microphone, { width: 24, height: 24 })
|
|
1473
|
+
: jsx(Icon16Pause, { width: 24, height: 24 });
|
|
1471
1474
|
const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
|
|
1472
1475
|
const recordButtonAria = !isRecording
|
|
1473
1476
|
? startAriaLabel
|
|
@@ -1476,14 +1479,16 @@ const RecordingShell = (props) => {
|
|
|
1476
1479
|
: pauseRecordingAriaLabel || stopAriaLabel;
|
|
1477
1480
|
const canShowPlayback = !isRecording || isPaused;
|
|
1478
1481
|
const playbackDisabled = !hasRecording && !isRecording;
|
|
1479
|
-
const playbackIcon = isPlaying
|
|
1482
|
+
const playbackIcon = isPlaying
|
|
1483
|
+
? jsx(Icon16Pause, { width: 24, height: 24 })
|
|
1484
|
+
: jsx(Icon16Play, { width: 24, height: 24 });
|
|
1480
1485
|
const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
|
|
1481
1486
|
const sendDisabled = isSending || (!hasRecording && !isRecording);
|
|
1482
|
-
return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(
|
|
1487
|
+
return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
|
|
1483
1488
|
height: `${12 + normalized / 2}px`,
|
|
1484
1489
|
maxHeight: '34px',
|
|
1485
1490
|
opacity: 0.38 + Math.min(0.5, normalized / 120),
|
|
1486
|
-
}) }) }), jsx(Timer$1, { children: displayTime })] }), jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width:
|
|
1491
|
+
}) }) }), jsx(Timer$1, { children: displayTime })] }), jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width: 24, height: 24 })) })] }));
|
|
1487
1492
|
};
|
|
1488
1493
|
|
|
1489
1494
|
/**
|
|
@@ -1743,8 +1748,8 @@ const useVoiceRecorderInput = (options) => {
|
|
|
1743
1748
|
|
|
1744
1749
|
const PreviewBars = (props) => {
|
|
1745
1750
|
var _a, _b;
|
|
1746
|
-
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels,
|
|
1747
|
-
const maxBars =
|
|
1751
|
+
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
|
|
1752
|
+
const maxBars = visibleBarsCount;
|
|
1748
1753
|
const sourceLevels = timelineLevels.length > 0
|
|
1749
1754
|
? timelineLevels
|
|
1750
1755
|
: previewLevels;
|
|
@@ -1788,8 +1793,8 @@ const PreviewBars = (props) => {
|
|
|
1788
1793
|
};
|
|
1789
1794
|
|
|
1790
1795
|
const TimelineBars = (props) => {
|
|
1791
|
-
const { timelineLevels,
|
|
1792
|
-
const maxBars =
|
|
1796
|
+
const { timelineLevels, visibleBarsCount } = props;
|
|
1797
|
+
const maxBars = visibleBarsCount;
|
|
1793
1798
|
const visible = timelineLevels.slice(-maxBars);
|
|
1794
1799
|
const padded = [
|
|
1795
1800
|
...visible,
|
|
@@ -1834,15 +1839,11 @@ const CancelButton = styled.button `
|
|
|
1834
1839
|
border-radius: 16px;
|
|
1835
1840
|
background: transparent;
|
|
1836
1841
|
border: 0;
|
|
1842
|
+
margin-right: 4px;
|
|
1837
1843
|
flex-shrink: 0;
|
|
1838
1844
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
|
|
1839
1845
|
cursor: pointer;
|
|
1840
1846
|
transition: color 160ms ease, background-color 160ms ease;
|
|
1841
|
-
|
|
1842
|
-
&:hover {
|
|
1843
|
-
background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
|
|
1844
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 72%, transparent);
|
|
1845
|
-
}
|
|
1846
1847
|
`;
|
|
1847
1848
|
const RecordTrack = styled.div `
|
|
1848
1849
|
flex: 1;
|
|
@@ -1858,7 +1859,8 @@ const RecordTrack = styled.div `
|
|
|
1858
1859
|
const ButtonSlot = styled.div `
|
|
1859
1860
|
display: flex;
|
|
1860
1861
|
align-items: center;
|
|
1861
|
-
gap:
|
|
1862
|
+
gap: 4px;
|
|
1863
|
+
padding: 2px;
|
|
1862
1864
|
flex-shrink: 0;
|
|
1863
1865
|
`;
|
|
1864
1866
|
const StopButton = styled.button `
|
|
@@ -1995,19 +1997,39 @@ const VoiceRecorderInput = (props) => {
|
|
|
1995
1997
|
const waveformRef = useRef(null);
|
|
1996
1998
|
const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
|
|
1997
1999
|
const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
|
|
1998
|
-
const
|
|
2000
|
+
const [visibleBarsCount, setVisibleBarsCount] = useState(0);
|
|
2001
|
+
const measureBars = () => {
|
|
1999
2002
|
const el = waveformRef.current;
|
|
2000
2003
|
if (!el) {
|
|
2001
|
-
return
|
|
2004
|
+
return;
|
|
2002
2005
|
}
|
|
2003
|
-
|
|
2006
|
+
setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
|
|
2007
|
+
};
|
|
2008
|
+
/** Sync recalc before paint — catches layout shifts from button changes */
|
|
2009
|
+
useLayoutEffect(measureBars);
|
|
2010
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2011
|
+
useEffect(() => {
|
|
2012
|
+
const el = waveformRef.current;
|
|
2013
|
+
if (!el) {
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
let rafId = 0;
|
|
2017
|
+
const observer = new ResizeObserver(() => {
|
|
2018
|
+
cancelAnimationFrame(rafId);
|
|
2019
|
+
rafId = requestAnimationFrame(measureBars);
|
|
2020
|
+
});
|
|
2021
|
+
observer.observe(el);
|
|
2022
|
+
return () => {
|
|
2023
|
+
cancelAnimationFrame(rafId);
|
|
2024
|
+
observer.disconnect();
|
|
2025
|
+
};
|
|
2004
2026
|
}, []);
|
|
2005
2027
|
const displayTime = (isPreview && isPlaying)
|
|
2006
2028
|
? formatTime(playbackTime)
|
|
2007
2029
|
: formatTime(recordingTime || lastDurationRef.current);
|
|
2008
|
-
return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(
|
|
2030
|
+
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
|
|
2009
2031
|
? jsx(Icon16Pause, { width: 14, height: 14 })
|
|
2010
|
-
: jsx(Icon16Play, { width: 14, height: 14 }) })] })) : (jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsx(
|
|
2032
|
+
: 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, {})) })] }));
|
|
2011
2033
|
};
|
|
2012
2034
|
|
|
2013
2035
|
/**
|
|
@@ -2183,11 +2205,29 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2183
2205
|
return VoicePlaybackChain.register(chainId, () => play());
|
|
2184
2206
|
}
|
|
2185
2207
|
}, [chainId, play]);
|
|
2186
|
-
|
|
2187
|
-
useLayoutEffect(() => {
|
|
2208
|
+
const measureTrackWidth = () => {
|
|
2188
2209
|
if (trackRef.current) {
|
|
2189
2210
|
setTrackWidth(trackRef.current.clientWidth);
|
|
2190
2211
|
}
|
|
2212
|
+
};
|
|
2213
|
+
/** Sync recalc before paint — catches layout shifts */
|
|
2214
|
+
useLayoutEffect(measureTrackWidth);
|
|
2215
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2216
|
+
useEffect(() => {
|
|
2217
|
+
const el = trackRef.current;
|
|
2218
|
+
if (!el) {
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
let rafId = 0;
|
|
2222
|
+
const observer = new ResizeObserver(() => {
|
|
2223
|
+
cancelAnimationFrame(rafId);
|
|
2224
|
+
rafId = requestAnimationFrame(measureTrackWidth);
|
|
2225
|
+
});
|
|
2226
|
+
observer.observe(el);
|
|
2227
|
+
return () => {
|
|
2228
|
+
cancelAnimationFrame(rafId);
|
|
2229
|
+
observer.disconnect();
|
|
2230
|
+
};
|
|
2191
2231
|
}, []);
|
|
2192
2232
|
const displayTime = isPlaying
|
|
2193
2233
|
? formatTime(currentTime)
|