@exode-team/react-recorder 1.1.2 → 1.1.5
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.d.ts +2 -3
- package/dist/components/AudioRecorder.d.ts.map +1 -1
- 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 +1 -2
- 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/hooks/useRecordingSender.d.ts +1 -2
- package/dist/hooks/useRecordingSender.d.ts.map +1 -1
- package/dist/hooks/useVoiceRecorder.d.ts.map +1 -1
- package/dist/hooks/useVoiceRecorderInput.d.ts +1 -2
- package/dist/hooks/useVoiceRecorderInput.d.ts.map +1 -1
- package/dist/index.cjs +84 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -67
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/recorder.d.ts +1 -2
- package/dist/types/recorder.d.ts.map +1 -1
- package/dist/types/upload.d.ts +0 -7
- package/dist/types/upload.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,9 +12,8 @@ export { useVoiceRecorder } from './hooks/useVoiceRecorder';
|
|
|
12
12
|
export { useVoiceMessagePlayer } from './hooks/useVoiceMessagePlayer';
|
|
13
13
|
export { useRecorderVisualization } from './hooks/useRecorderVisualization';
|
|
14
14
|
export { useRecordingTiming } from './hooks/useRecordingTiming';
|
|
15
|
-
export type {
|
|
15
|
+
export type { StorageFileEntity, UploadStrategy } from './types/upload';
|
|
16
16
|
export type { UseVoiceRecorderOptions, UseVoiceRecorderResult, UseVoiceMessagePlayerOptions, UseVoiceMessagePlayerResult, } from './types/recorder';
|
|
17
|
-
export { StorageSpaceEnum } from './types/upload';
|
|
18
17
|
export { AUDIO_LEVEL_BARS } from './constants';
|
|
19
18
|
export { clampValue } from './utils/math';
|
|
20
19
|
export { formatTime } from './utils/format';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,oDAAoD,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGhE,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,oDAAoD,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGhE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,YAAY,EACR,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,EAC5B,2BAA2B,GAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
|
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
|
|
@@ -585,7 +585,7 @@ const useRecorderVisualization = () => {
|
|
|
585
585
|
* @author: exode <hello@exode.ru>
|
|
586
586
|
*/
|
|
587
587
|
const useRecordingSender = (options) => {
|
|
588
|
-
const {
|
|
588
|
+
const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
|
|
589
589
|
const sendRecording = useCallback(async () => {
|
|
590
590
|
if (!audioBlob) {
|
|
591
591
|
const error = new Error('Voice recording is not available');
|
|
@@ -601,7 +601,7 @@ const useRecordingSender = (options) => {
|
|
|
601
601
|
const file = new File([audioBlob], fileName, {
|
|
602
602
|
type: audioBlob.type || mimeType,
|
|
603
603
|
});
|
|
604
|
-
const uploaded = await uploader(file, {
|
|
604
|
+
const uploaded = await uploader(file, { uploadId });
|
|
605
605
|
await onUploaded(uploaded);
|
|
606
606
|
if (autoResetAfterUpload) {
|
|
607
607
|
deleteRecording();
|
|
@@ -612,7 +612,6 @@ const useRecordingSender = (options) => {
|
|
|
612
612
|
throw error;
|
|
613
613
|
}
|
|
614
614
|
}, [
|
|
615
|
-
space,
|
|
616
615
|
uploadId,
|
|
617
616
|
uploader,
|
|
618
617
|
fileName,
|
|
@@ -632,7 +631,7 @@ const useRecordingSender = (options) => {
|
|
|
632
631
|
* @author: exode <hello@exode.ru>
|
|
633
632
|
*/
|
|
634
633
|
function useVoiceRecorder(props) {
|
|
635
|
-
const {
|
|
634
|
+
const { uploadId, uploader, onUploaded, onRecordingStop, onRecordingStart, onRecordingError, onRecordingDelete, onPermissionDenied, fileName = 'record.webm', mimeType = 'audio/webm', autoResetAfterUpload = true, } = props;
|
|
636
635
|
const [audioBlob, setAudioBlob] = useState(null);
|
|
637
636
|
const { audioURL, isPlaying, mountBlob, playbackTime, stopPlayback, resetPlayback, togglePlayback: _togglePlayback, playbackDuration, } = useAudioPlayback();
|
|
638
637
|
const { audioLevels, previewLevels, resetAudioLevels, stopVisualization, resetPreviewLevels, resumeVisualization, cleanupVisualization, generatePreviewLevels, initializeVisualization, } = useRecorderVisualization();
|
|
@@ -662,7 +661,6 @@ function useVoiceRecorder(props) {
|
|
|
662
661
|
audioBlob,
|
|
663
662
|
fileName,
|
|
664
663
|
mimeType,
|
|
665
|
-
space,
|
|
666
664
|
uploadId,
|
|
667
665
|
uploader,
|
|
668
666
|
autoResetAfterUpload,
|
|
@@ -782,6 +780,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
782
780
|
const audioRef = useRef(null);
|
|
783
781
|
const objectUrlRef = useRef(null);
|
|
784
782
|
const abortControllerRef = useRef(null);
|
|
783
|
+
const failedSrcRef = useRef(null);
|
|
785
784
|
const callbacksRef = useRef(playbackCallbacks);
|
|
786
785
|
callbacksRef.current = playbackCallbacks;
|
|
787
786
|
const createAudioFromBlob = useCallback((blob) => {
|
|
@@ -818,7 +817,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
818
817
|
}, [waveformData, createAudioFromBlob]);
|
|
819
818
|
const ensureAudioLoaded = useCallback(async () => {
|
|
820
819
|
var _a;
|
|
821
|
-
if (audioRef.current) {
|
|
820
|
+
if (audioRef.current || failedSrcRef.current === src) {
|
|
822
821
|
return;
|
|
823
822
|
}
|
|
824
823
|
if (preloadedBlob) {
|
|
@@ -849,6 +848,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
849
848
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
850
849
|
return;
|
|
851
850
|
}
|
|
851
|
+
failedSrcRef.current = src;
|
|
852
852
|
console.warn('[voice-player] Failed to load audio', error);
|
|
853
853
|
}
|
|
854
854
|
finally {
|
|
@@ -859,6 +859,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
859
859
|
}, [src, waveformData, preloadedBlob, mountFromBlob, createAudioFromBlob]);
|
|
860
860
|
/** Cleanup on src change or unmount */
|
|
861
861
|
useEffect(() => {
|
|
862
|
+
failedSrcRef.current = null;
|
|
862
863
|
return () => {
|
|
863
864
|
var _a;
|
|
864
865
|
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
@@ -1188,7 +1189,6 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1188
1189
|
const Container = styled.div `
|
|
1189
1190
|
position: relative;
|
|
1190
1191
|
width: 100%;
|
|
1191
|
-
padding: 16px;
|
|
1192
1192
|
transition: color 200ms ease, background-color 200ms ease;
|
|
1193
1193
|
color: var(--text_primary, #0a0a0a);
|
|
1194
1194
|
`;
|
|
@@ -1273,29 +1273,28 @@ const Wrapper$1 = styled.div `
|
|
|
1273
1273
|
gap: 12px;
|
|
1274
1274
|
padding: 10px 14px;
|
|
1275
1275
|
border-radius: 9999px;
|
|
1276
|
-
background: var(--
|
|
1277
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent);
|
|
1276
|
+
background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
|
|
1278
1277
|
`;
|
|
1279
1278
|
const CancelButton$1 = styled.button `
|
|
1280
|
-
width:
|
|
1281
|
-
height:
|
|
1279
|
+
width: 38px;
|
|
1280
|
+
height: 38px;
|
|
1282
1281
|
display: inline-flex;
|
|
1283
1282
|
align-items: center;
|
|
1284
1283
|
justify-content: center;
|
|
1285
|
-
border-radius:
|
|
1286
|
-
background: transparent;
|
|
1284
|
+
border-radius: 19px;
|
|
1285
|
+
background: color-mix(in srgb, var(--exode_red) 10%, transparent);
|
|
1287
1286
|
border: 0;
|
|
1287
|
+
flex-shrink: 0;
|
|
1288
1288
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
|
|
1289
1289
|
cursor: pointer;
|
|
1290
|
-
transition:
|
|
1290
|
+
transition: transform 160ms ease;
|
|
1291
1291
|
|
|
1292
|
-
&:hover {
|
|
1293
|
-
|
|
1294
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 76%, transparent);
|
|
1292
|
+
&:hover:not(:disabled) {
|
|
1293
|
+
transform: translateY(-1px);
|
|
1295
1294
|
}
|
|
1296
1295
|
|
|
1297
|
-
&:active {
|
|
1298
|
-
|
|
1296
|
+
&:active:not(:disabled) {
|
|
1297
|
+
transform: translateY(0);
|
|
1299
1298
|
}
|
|
1300
1299
|
`;
|
|
1301
1300
|
const Track = styled.div `
|
|
@@ -1303,7 +1302,7 @@ const Track = styled.div `
|
|
|
1303
1302
|
display: flex;
|
|
1304
1303
|
align-items: center;
|
|
1305
1304
|
gap: 12px;
|
|
1306
|
-
padding: 8px
|
|
1305
|
+
padding: 8px 10px;
|
|
1307
1306
|
border-radius: 9999px;
|
|
1308
1307
|
background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
|
|
1309
1308
|
`;
|
|
@@ -1318,7 +1317,7 @@ const CircleButton = styled.button `
|
|
|
1318
1317
|
cursor: pointer;
|
|
1319
1318
|
transition: transform 160ms ease, color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
|
|
1320
1319
|
flex-shrink: 0;
|
|
1321
|
-
background:
|
|
1320
|
+
background: var(--background_content, #ffffff);
|
|
1322
1321
|
|
|
1323
1322
|
color: ${({ $variant }) => {
|
|
1324
1323
|
if ($variant === 'record') {
|
|
@@ -1467,7 +1466,9 @@ const RecordingShell = (props) => {
|
|
|
1467
1466
|
return formatTime(0);
|
|
1468
1467
|
}, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
|
|
1469
1468
|
const recordButtonDisabled = isSending;
|
|
1470
|
-
const recordButtonIcon = !isRecording || isPaused
|
|
1469
|
+
const recordButtonIcon = !isRecording || isPaused
|
|
1470
|
+
? jsx(Icon16Microphone, { width: 24, height: 24 })
|
|
1471
|
+
: jsx(Icon16Pause, { width: 24, height: 24 });
|
|
1471
1472
|
const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
|
|
1472
1473
|
const recordButtonAria = !isRecording
|
|
1473
1474
|
? startAriaLabel
|
|
@@ -1476,14 +1477,16 @@ const RecordingShell = (props) => {
|
|
|
1476
1477
|
: pauseRecordingAriaLabel || stopAriaLabel;
|
|
1477
1478
|
const canShowPlayback = !isRecording || isPaused;
|
|
1478
1479
|
const playbackDisabled = !hasRecording && !isRecording;
|
|
1479
|
-
const playbackIcon = isPlaying
|
|
1480
|
+
const playbackIcon = isPlaying
|
|
1481
|
+
? jsx(Icon16Pause, { width: 24, height: 24 })
|
|
1482
|
+
: jsx(Icon16Play, { width: 24, height: 24 });
|
|
1480
1483
|
const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
|
|
1481
1484
|
const sendDisabled = isSending || (!hasRecording && !isRecording);
|
|
1482
|
-
return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(
|
|
1485
|
+
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
1486
|
height: `${12 + normalized / 2}px`,
|
|
1484
1487
|
maxHeight: '34px',
|
|
1485
1488
|
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:
|
|
1489
|
+
}) }) }), 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
1490
|
};
|
|
1488
1491
|
|
|
1489
1492
|
/**
|
|
@@ -1531,24 +1534,12 @@ const mergeMessages = (custom) => {
|
|
|
1531
1534
|
};
|
|
1532
1535
|
};
|
|
1533
1536
|
|
|
1534
|
-
/**
|
|
1535
|
-
* Upload types
|
|
1536
|
-
*
|
|
1537
|
-
* @author: exode <hello@exode.ru>
|
|
1538
|
-
*/
|
|
1539
|
-
const StorageSpaceEnum = {
|
|
1540
|
-
User: 'user',
|
|
1541
|
-
Shared: 'shared',
|
|
1542
|
-
System: 'system',
|
|
1543
|
-
};
|
|
1544
|
-
|
|
1545
1537
|
const AudioRecorder = (props) => {
|
|
1546
|
-
const {
|
|
1538
|
+
const { fileName, uploadId, mimeType, className, uploader, onUploaded, onUploadError, onRecordingStop, onRecordingError, onRecordingStart, onRecordingDelete, onPermissionDenied, autoResetAfterUpload, messages: customMessages, } = props;
|
|
1547
1539
|
const messages = useMemo(() => mergeMessages(customMessages), [customMessages]);
|
|
1548
1540
|
const [resolvedUploadId] = useState(() => uploadId !== null && uploadId !== void 0 ? uploadId : (typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
1549
1541
|
? crypto.randomUUID()
|
|
1550
1542
|
: `upload-${Date.now()}`));
|
|
1551
|
-
const resolvedSpace = space !== null && space !== void 0 ? space : StorageSpaceEnum.User;
|
|
1552
1543
|
const handleRecorderError = useCallback((error) => {
|
|
1553
1544
|
onRecordingError === null || onRecordingError === void 0 ? void 0 : onRecordingError(error);
|
|
1554
1545
|
onUploadError === null || onUploadError === void 0 ? void 0 : onUploadError(error);
|
|
@@ -1566,7 +1557,6 @@ const AudioRecorder = (props) => {
|
|
|
1566
1557
|
onRecordingDelete,
|
|
1567
1558
|
onPermissionDenied,
|
|
1568
1559
|
autoResetAfterUpload,
|
|
1569
|
-
space: resolvedSpace,
|
|
1570
1560
|
uploader,
|
|
1571
1561
|
uploadId: resolvedUploadId,
|
|
1572
1562
|
onRecordingError: handleRecorderError,
|
|
@@ -1589,7 +1579,7 @@ const AudioRecorder = (props) => {
|
|
|
1589
1579
|
* @author: exode <hello@exode.ru>
|
|
1590
1580
|
*/
|
|
1591
1581
|
const useVoiceRecorderInput = (options) => {
|
|
1592
|
-
const { onSend, onError, onCancel, uploader, onUploaded, onPermissionDenied, autoStart = true,
|
|
1582
|
+
const { onSend, onError, onCancel, uploader, onUploaded, onPermissionDenied, autoStart = true, mimeType = 'audio/webm', fileName = 'voice-message.webm', } = options;
|
|
1593
1583
|
const [isSending, setIsSending] = useState(false);
|
|
1594
1584
|
const [timelineLevels, setTimelineLevels] = useState([]);
|
|
1595
1585
|
const audioBlobRef = useRef(null);
|
|
@@ -1610,7 +1600,6 @@ const useVoiceRecorderInput = (options) => {
|
|
|
1610
1600
|
}
|
|
1611
1601
|
}, []);
|
|
1612
1602
|
const recorder = useVoiceRecorder({
|
|
1613
|
-
space,
|
|
1614
1603
|
fileName,
|
|
1615
1604
|
mimeType,
|
|
1616
1605
|
uploadId,
|
|
@@ -1743,8 +1732,8 @@ const useVoiceRecorderInput = (options) => {
|
|
|
1743
1732
|
|
|
1744
1733
|
const PreviewBars = (props) => {
|
|
1745
1734
|
var _a, _b;
|
|
1746
|
-
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels,
|
|
1747
|
-
const maxBars =
|
|
1735
|
+
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
|
|
1736
|
+
const maxBars = visibleBarsCount;
|
|
1748
1737
|
const sourceLevels = timelineLevels.length > 0
|
|
1749
1738
|
? timelineLevels
|
|
1750
1739
|
: previewLevels;
|
|
@@ -1788,8 +1777,8 @@ const PreviewBars = (props) => {
|
|
|
1788
1777
|
};
|
|
1789
1778
|
|
|
1790
1779
|
const TimelineBars = (props) => {
|
|
1791
|
-
const { timelineLevels,
|
|
1792
|
-
const maxBars =
|
|
1780
|
+
const { timelineLevels, visibleBarsCount } = props;
|
|
1781
|
+
const maxBars = visibleBarsCount;
|
|
1793
1782
|
const visible = timelineLevels.slice(-maxBars);
|
|
1794
1783
|
const padded = [
|
|
1795
1784
|
...visible,
|
|
@@ -1834,15 +1823,11 @@ const CancelButton = styled.button `
|
|
|
1834
1823
|
border-radius: 16px;
|
|
1835
1824
|
background: transparent;
|
|
1836
1825
|
border: 0;
|
|
1826
|
+
margin-right: 4px;
|
|
1837
1827
|
flex-shrink: 0;
|
|
1838
1828
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
|
|
1839
1829
|
cursor: pointer;
|
|
1840
1830
|
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
1831
|
`;
|
|
1847
1832
|
const RecordTrack = styled.div `
|
|
1848
1833
|
flex: 1;
|
|
@@ -1858,7 +1843,8 @@ const RecordTrack = styled.div `
|
|
|
1858
1843
|
const ButtonSlot = styled.div `
|
|
1859
1844
|
display: flex;
|
|
1860
1845
|
align-items: center;
|
|
1861
|
-
gap:
|
|
1846
|
+
gap: 4px;
|
|
1847
|
+
padding: 2px;
|
|
1862
1848
|
flex-shrink: 0;
|
|
1863
1849
|
`;
|
|
1864
1850
|
const StopButton = styled.button `
|
|
@@ -1940,14 +1926,10 @@ const MicIndicator = styled.button `
|
|
|
1940
1926
|
border: 0;
|
|
1941
1927
|
flex-shrink: 0;
|
|
1942
1928
|
cursor: pointer;
|
|
1943
|
-
background:
|
|
1944
|
-
color: var(--
|
|
1929
|
+
background: var(--background_content, #ffffff);
|
|
1930
|
+
color: color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%);
|
|
1945
1931
|
transition: background-color 140ms ease, transform 120ms ease;
|
|
1946
1932
|
|
|
1947
|
-
&:hover {
|
|
1948
|
-
background: color-mix(in srgb, var(--exode_red, #ed1c24) 22%, transparent);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
1933
|
&:active {
|
|
1952
1934
|
transform: scale(0.94);
|
|
1953
1935
|
}
|
|
@@ -1995,19 +1977,39 @@ const VoiceRecorderInput = (props) => {
|
|
|
1995
1977
|
const waveformRef = useRef(null);
|
|
1996
1978
|
const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
|
|
1997
1979
|
const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
|
|
1998
|
-
const
|
|
1980
|
+
const [visibleBarsCount, setVisibleBarsCount] = useState(0);
|
|
1981
|
+
const measureBars = () => {
|
|
1999
1982
|
const el = waveformRef.current;
|
|
2000
1983
|
if (!el) {
|
|
2001
|
-
return
|
|
1984
|
+
return;
|
|
2002
1985
|
}
|
|
2003
|
-
|
|
1986
|
+
setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
|
|
1987
|
+
};
|
|
1988
|
+
/** Sync recalc before paint — catches layout shifts from button changes */
|
|
1989
|
+
useLayoutEffect(measureBars);
|
|
1990
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
1991
|
+
useEffect(() => {
|
|
1992
|
+
const el = waveformRef.current;
|
|
1993
|
+
if (!el) {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
let rafId = 0;
|
|
1997
|
+
const observer = new ResizeObserver(() => {
|
|
1998
|
+
cancelAnimationFrame(rafId);
|
|
1999
|
+
rafId = requestAnimationFrame(measureBars);
|
|
2000
|
+
});
|
|
2001
|
+
observer.observe(el);
|
|
2002
|
+
return () => {
|
|
2003
|
+
cancelAnimationFrame(rafId);
|
|
2004
|
+
observer.disconnect();
|
|
2005
|
+
};
|
|
2004
2006
|
}, []);
|
|
2005
2007
|
const displayTime = (isPreview && isPlaying)
|
|
2006
2008
|
? formatTime(playbackTime)
|
|
2007
2009
|
: formatTime(recordingTime || lastDurationRef.current);
|
|
2008
|
-
return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(
|
|
2010
|
+
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
2011
|
? 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(
|
|
2012
|
+
: 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
2013
|
};
|
|
2012
2014
|
|
|
2013
2015
|
/**
|
|
@@ -2183,11 +2185,29 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2183
2185
|
return VoicePlaybackChain.register(chainId, () => play());
|
|
2184
2186
|
}
|
|
2185
2187
|
}, [chainId, play]);
|
|
2186
|
-
|
|
2187
|
-
useLayoutEffect(() => {
|
|
2188
|
+
const measureTrackWidth = () => {
|
|
2188
2189
|
if (trackRef.current) {
|
|
2189
2190
|
setTrackWidth(trackRef.current.clientWidth);
|
|
2190
2191
|
}
|
|
2192
|
+
};
|
|
2193
|
+
/** Sync recalc before paint — catches layout shifts */
|
|
2194
|
+
useLayoutEffect(measureTrackWidth);
|
|
2195
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2196
|
+
useEffect(() => {
|
|
2197
|
+
const el = trackRef.current;
|
|
2198
|
+
if (!el) {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
let rafId = 0;
|
|
2202
|
+
const observer = new ResizeObserver(() => {
|
|
2203
|
+
cancelAnimationFrame(rafId);
|
|
2204
|
+
rafId = requestAnimationFrame(measureTrackWidth);
|
|
2205
|
+
});
|
|
2206
|
+
observer.observe(el);
|
|
2207
|
+
return () => {
|
|
2208
|
+
cancelAnimationFrame(rafId);
|
|
2209
|
+
observer.disconnect();
|
|
2210
|
+
};
|
|
2191
2211
|
}, []);
|
|
2192
2212
|
const displayTime = isPlaying
|
|
2193
2213
|
? formatTime(currentTime)
|
|
@@ -2272,5 +2292,5 @@ class VoiceBlobCacheClass {
|
|
|
2272
2292
|
}
|
|
2273
2293
|
const VoiceBlobCache = new VoiceBlobCacheClass();
|
|
2274
2294
|
|
|
2275
|
-
export { AUDIO_LEVEL_BARS, AudioRecorder,
|
|
2295
|
+
export { AUDIO_LEVEL_BARS, AudioRecorder, VoiceBlobCache, VoiceMessagePlayer, VoicePlaybackChain, VoiceRecorderInput, WaveformBars, clampValue, formatTime, useRecorderVisualization, useRecordingTiming, useVoiceMessagePlayer, useVoiceRecorder };
|
|
2276
2296
|
//# sourceMappingURL=index.js.map
|