@exode-team/react-recorder 1.1.1 → 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/README.md +2 -0
- 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 +1 -0
- 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/hooks/useVoiceMessagePlayer.d.ts.map +1 -1
- package/dist/index.cjs +112 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +114 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,6 +144,7 @@ const Messages = () => {
|
|
|
144
144
|
| `src` | `string` | ✅ | — | URL аудиофайла. |
|
|
145
145
|
| `chainId` | `number` | — | — | ID для последовательного воспроизведения. При окончании автоматически запускает следующий. |
|
|
146
146
|
| `duration` | `number` | — | — | Длительность в секундах (отображается до загрузки аудио). |
|
|
147
|
+
| `autoPreload` | `boolean` | — | `true` | Автозагрузка аудио при попадании в viewport (IntersectionObserver). |
|
|
147
148
|
| `waveformData` | `number[]` | — | — | Предварительно вычисленные уровни waveform. |
|
|
148
149
|
| `preloadedBlob` | `Blob \| null` | — | — | Blob для мгновенного воспроизведения без сетевого запроса. |
|
|
149
150
|
| `onEnded` | `() => void` | — | — | Колбэк окончания воспроизведения. |
|
|
@@ -153,6 +154,7 @@ const Messages = () => {
|
|
|
153
154
|
**Ref API (`VoiceMessagePlayerRef`):**
|
|
154
155
|
|
|
155
156
|
- `play()` — программный запуск воспроизведения.
|
|
157
|
+
- `prefetch()` — загрузка аудио без воспроизведения (fetch + decode waveform).
|
|
156
158
|
|
|
157
159
|
#### `VoicePlaybackChain`
|
|
158
160
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioRecorder.styled.d.ts","sourceRoot":"","sources":["../../src/components/AudioRecorder.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"AudioRecorder.styled.d.ts","sourceRoot":"","sources":["../../src/components/AudioRecorder.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,SAAS,KAKrB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecordingShell.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"RecordingShell.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA2BH,UAAU,KAAK;IACX,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB,EAAE,MAAM,CAAC;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAGD,QAAA,MAAM,cAAc,GAAI,OAAO,KAAK,QA6HnC,CAAC;AAGF,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecordingShell.styled.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"RecordingShell.styled.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAOnB,CAAC;AAEF,eAAO,MAAM,YAAY,KAqBxB,CAAC;AAEF,eAAO,MAAM,KAAK,KAQjB,CAAC;AAEF,eAAO,MAAM,YAAY,KA2CxB,CAAC;AAEF,eAAO,MAAM,QAAQ,KAOpB,CAAC;AAEF,eAAO,MAAM,IAAI,KAMhB,CAAC;AAEF,eAAO,MAAM,GAAG,KAOf,CAAC;AAEF,eAAO,MAAM,KAAK,KAKjB,CAAC;AAEF,eAAO,MAAM,UAAU,KAyBtB,CAAC;AAEF,eAAO,MAAM,WAAW,KAMvB,CAAC;AAEF,eAAO,MAAM,OAAO,KAoBnB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"VoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB;AAeD,QAAA,MAAM,kBAAkB,KA8MtB,CAAC;AAGH,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -10,7 +10,7 @@ interface PreviewBarsProps {
|
|
|
10
10
|
recordingTime: number;
|
|
11
11
|
previewLevels: number[];
|
|
12
12
|
timelineLevels: number[];
|
|
13
|
-
|
|
13
|
+
visibleBarsCount: number;
|
|
14
14
|
}
|
|
15
15
|
declare const PreviewBars: (props: PreviewBarsProps) => any;
|
|
16
16
|
export { PreviewBars };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PreviewBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/PreviewBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,gBAAgB;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,
|
|
1
|
+
{"version":3,"file":"PreviewBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/PreviewBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,gBAAgB;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAGD,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,QA0E3C,CAAC;AAGF,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimelineBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/TimelineBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,iBAAiB;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,
|
|
1
|
+
{"version":3,"file":"TimelineBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/TimelineBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,iBAAiB;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAGD,QAAA,MAAM,YAAY,GAAI,OAAO,iBAAiB,QAkC7C,CAAC;AAGF,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceRecorderInput.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"VoiceRecorderInput.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAuBrE,UAAU,KAAK;IACX,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAGD,QAAA,MAAM,kBAAkB,GAAI,OAAO,KAAK,QAmIvC,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceRecorderInput.styled.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAOnB,CAAC;AAEF,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"VoiceRecorderInput.styled.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAOnB,CAAC;AAEF,eAAO,MAAM,YAAY,KAcxB,CAAC;AAEF,eAAO,MAAM,WAAW,KASvB,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,UAAU,KAMtB,CAAC;AAEF,eAAO,MAAM,UAAU,KA0BtB,CAAC;AAEF,eAAO,MAAM,YAAY,KAUxB,CAAC;AAEF,eAAO,MAAM,KAAK,KAMjB,CAAC;AAEF,eAAO,MAAM,UAAU,KAuBtB,CAAC;AAEF,eAAO,MAAM,YAAY,KAqBxB,CAAC;AAEF,eAAO,MAAM,eAAe,KAqB3B,CAAC;AAEF,eAAO,MAAM,WAAW,KAcvB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAudioBlobLoader.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioBlobLoader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,UAAU,yBAAyB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,iBAAiB,CAAC;CACxC;AAED,UAAU,wBAAwB;IAC9B,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,QAAA,MAAM,kBAAkB,GAAI,SAAS,yBAAyB,KAAG,
|
|
1
|
+
{"version":3,"file":"useAudioBlobLoader.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioBlobLoader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,UAAU,yBAAyB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,iBAAiB,CAAC;CACxC;AAED,UAAU,wBAAwB;IAC9B,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,QAAA,MAAM,kBAAkB,GAAI,SAAS,yBAAyB,KAAG,wBAoIhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAG1F,QAAA,MAAM,qBAAqB,GAAI,SAAS,4BAA4B,KAAG,
|
|
1
|
+
{"version":3,"file":"useVoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAG1F,QAAA,MAAM,qBAAqB,GAAI,SAAS,4BAA4B,KAAG,2BA6JtE,CAAC;AAGF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
package/dist/index.cjs
CHANGED
|
@@ -784,6 +784,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
784
784
|
const audioRef = React.useRef(null);
|
|
785
785
|
const objectUrlRef = React.useRef(null);
|
|
786
786
|
const abortControllerRef = React.useRef(null);
|
|
787
|
+
const failedSrcRef = React.useRef(null);
|
|
787
788
|
const callbacksRef = React.useRef(playbackCallbacks);
|
|
788
789
|
callbacksRef.current = playbackCallbacks;
|
|
789
790
|
const createAudioFromBlob = React.useCallback((blob) => {
|
|
@@ -820,7 +821,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
820
821
|
}, [waveformData, createAudioFromBlob]);
|
|
821
822
|
const ensureAudioLoaded = React.useCallback(async () => {
|
|
822
823
|
var _a;
|
|
823
|
-
if (audioRef.current) {
|
|
824
|
+
if (audioRef.current || failedSrcRef.current === src) {
|
|
824
825
|
return;
|
|
825
826
|
}
|
|
826
827
|
if (preloadedBlob) {
|
|
@@ -851,6 +852,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
851
852
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
852
853
|
return;
|
|
853
854
|
}
|
|
855
|
+
failedSrcRef.current = src;
|
|
854
856
|
console.warn('[voice-player] Failed to load audio', error);
|
|
855
857
|
}
|
|
856
858
|
finally {
|
|
@@ -861,6 +863,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
861
863
|
}, [src, waveformData, preloadedBlob, mountFromBlob, createAudioFromBlob]);
|
|
862
864
|
/** Cleanup on src change or unmount */
|
|
863
865
|
React.useEffect(() => {
|
|
866
|
+
failedSrcRef.current = null;
|
|
864
867
|
return () => {
|
|
865
868
|
var _a;
|
|
866
869
|
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
@@ -962,6 +965,13 @@ const useVoiceMessagePlayer = (options) => {
|
|
|
962
965
|
},
|
|
963
966
|
},
|
|
964
967
|
});
|
|
968
|
+
/** Pre-fetch audio without playing (for viewport preloading) */
|
|
969
|
+
const prefetch = React.useCallback(() => {
|
|
970
|
+
if (isLoading || audioRef.current) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
void ensureAudioLoaded();
|
|
974
|
+
}, [isLoading, ensureAudioLoaded]);
|
|
965
975
|
/** Load audio and auto-play after loaded */
|
|
966
976
|
const preload = React.useCallback(() => {
|
|
967
977
|
if (isLoading || audioRef.current) {
|
|
@@ -1032,6 +1042,7 @@ const useVoiceMessagePlayer = (options) => {
|
|
|
1032
1042
|
currentTime,
|
|
1033
1043
|
waveformLevels,
|
|
1034
1044
|
seekTo,
|
|
1045
|
+
prefetch,
|
|
1035
1046
|
preload,
|
|
1036
1047
|
formatTime,
|
|
1037
1048
|
togglePlayback,
|
|
@@ -1182,7 +1193,6 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1182
1193
|
const Container = styled.div `
|
|
1183
1194
|
position: relative;
|
|
1184
1195
|
width: 100%;
|
|
1185
|
-
padding: 16px;
|
|
1186
1196
|
transition: color 200ms ease, background-color 200ms ease;
|
|
1187
1197
|
color: var(--text_primary, #0a0a0a);
|
|
1188
1198
|
`;
|
|
@@ -1267,29 +1277,28 @@ const Wrapper$1 = styled.div `
|
|
|
1267
1277
|
gap: 12px;
|
|
1268
1278
|
padding: 10px 14px;
|
|
1269
1279
|
border-radius: 9999px;
|
|
1270
|
-
background: var(--
|
|
1271
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent);
|
|
1280
|
+
background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
|
|
1272
1281
|
`;
|
|
1273
1282
|
const CancelButton$1 = styled.button `
|
|
1274
|
-
width:
|
|
1275
|
-
height:
|
|
1283
|
+
width: 38px;
|
|
1284
|
+
height: 38px;
|
|
1276
1285
|
display: inline-flex;
|
|
1277
1286
|
align-items: center;
|
|
1278
1287
|
justify-content: center;
|
|
1279
|
-
border-radius:
|
|
1280
|
-
background: transparent;
|
|
1288
|
+
border-radius: 19px;
|
|
1289
|
+
background: color-mix(in srgb, var(--exode_red) 10%, transparent);
|
|
1281
1290
|
border: 0;
|
|
1291
|
+
flex-shrink: 0;
|
|
1282
1292
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
|
|
1283
1293
|
cursor: pointer;
|
|
1284
|
-
transition:
|
|
1294
|
+
transition: transform 160ms ease;
|
|
1285
1295
|
|
|
1286
|
-
&:hover {
|
|
1287
|
-
|
|
1288
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 76%, transparent);
|
|
1296
|
+
&:hover:not(:disabled) {
|
|
1297
|
+
transform: translateY(-1px);
|
|
1289
1298
|
}
|
|
1290
1299
|
|
|
1291
|
-
&:active {
|
|
1292
|
-
|
|
1300
|
+
&:active:not(:disabled) {
|
|
1301
|
+
transform: translateY(0);
|
|
1293
1302
|
}
|
|
1294
1303
|
`;
|
|
1295
1304
|
const Track = styled.div `
|
|
@@ -1297,7 +1306,7 @@ const Track = styled.div `
|
|
|
1297
1306
|
display: flex;
|
|
1298
1307
|
align-items: center;
|
|
1299
1308
|
gap: 12px;
|
|
1300
|
-
padding: 8px
|
|
1309
|
+
padding: 8px 10px;
|
|
1301
1310
|
border-radius: 9999px;
|
|
1302
1311
|
background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
|
|
1303
1312
|
`;
|
|
@@ -1461,7 +1470,9 @@ const RecordingShell = (props) => {
|
|
|
1461
1470
|
return formatTime(0);
|
|
1462
1471
|
}, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
|
|
1463
1472
|
const recordButtonDisabled = isSending;
|
|
1464
|
-
const recordButtonIcon = !isRecording || isPaused
|
|
1473
|
+
const recordButtonIcon = !isRecording || isPaused
|
|
1474
|
+
? jsxRuntime.jsx(icons.Icon16Microphone, { width: 24, height: 24 })
|
|
1475
|
+
: jsxRuntime.jsx(icons.Icon16Pause, { width: 24, height: 24 });
|
|
1465
1476
|
const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
|
|
1466
1477
|
const recordButtonAria = !isRecording
|
|
1467
1478
|
? startAriaLabel
|
|
@@ -1470,14 +1481,16 @@ const RecordingShell = (props) => {
|
|
|
1470
1481
|
: pauseRecordingAriaLabel || stopAriaLabel;
|
|
1471
1482
|
const canShowPlayback = !isRecording || isPaused;
|
|
1472
1483
|
const playbackDisabled = !hasRecording && !isRecording;
|
|
1473
|
-
const playbackIcon = isPlaying
|
|
1484
|
+
const playbackIcon = isPlaying
|
|
1485
|
+
? jsxRuntime.jsx(icons.Icon16Pause, { width: 24, height: 24 })
|
|
1486
|
+
: jsxRuntime.jsx(icons.Icon16Play, { width: 24, height: 24 });
|
|
1474
1487
|
const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
|
|
1475
1488
|
const sendDisabled = isSending || (!hasRecording && !isRecording);
|
|
1476
|
-
return (jsxRuntime.jsxs(Wrapper$1, { children: [jsxRuntime.jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsxRuntime.jsx(icons.
|
|
1489
|
+
return (jsxRuntime.jsxs(Wrapper$1, { children: [jsxRuntime.jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsxRuntime.jsx(icons.Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxRuntime.jsxs(Track, { children: [jsxRuntime.jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsxRuntime.jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsxRuntime.jsx(Waveform, { "$expanded": canShowPlayback, children: jsxRuntime.jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
|
|
1477
1490
|
height: `${12 + normalized / 2}px`,
|
|
1478
1491
|
maxHeight: '34px',
|
|
1479
1492
|
opacity: 0.38 + Math.min(0.5, normalized / 120),
|
|
1480
|
-
}) }) }), jsxRuntime.jsx(Timer$1, { children: displayTime })] }), jsxRuntime.jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsxRuntime.jsx(SpinnerSlot, { children: jsxRuntime.jsx(Spinner, {}) })) : (jsxRuntime.jsx(icons.Icon28SendOutline, { width:
|
|
1493
|
+
}) }) }), jsxRuntime.jsx(Timer$1, { children: displayTime })] }), jsxRuntime.jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsxRuntime.jsx(SpinnerSlot, { children: jsxRuntime.jsx(Spinner, {}) })) : (jsxRuntime.jsx(icons.Icon28SendOutline, { width: 24, height: 24 })) })] }));
|
|
1481
1494
|
};
|
|
1482
1495
|
|
|
1483
1496
|
/**
|
|
@@ -1737,8 +1750,8 @@ const useVoiceRecorderInput = (options) => {
|
|
|
1737
1750
|
|
|
1738
1751
|
const PreviewBars = (props) => {
|
|
1739
1752
|
var _a, _b;
|
|
1740
|
-
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels,
|
|
1741
|
-
const maxBars =
|
|
1753
|
+
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
|
|
1754
|
+
const maxBars = visibleBarsCount;
|
|
1742
1755
|
const sourceLevels = timelineLevels.length > 0
|
|
1743
1756
|
? timelineLevels
|
|
1744
1757
|
: previewLevels;
|
|
@@ -1782,8 +1795,8 @@ const PreviewBars = (props) => {
|
|
|
1782
1795
|
};
|
|
1783
1796
|
|
|
1784
1797
|
const TimelineBars = (props) => {
|
|
1785
|
-
const { timelineLevels,
|
|
1786
|
-
const maxBars =
|
|
1798
|
+
const { timelineLevels, visibleBarsCount } = props;
|
|
1799
|
+
const maxBars = visibleBarsCount;
|
|
1787
1800
|
const visible = timelineLevels.slice(-maxBars);
|
|
1788
1801
|
const padded = [
|
|
1789
1802
|
...visible,
|
|
@@ -1828,15 +1841,11 @@ const CancelButton = styled.button `
|
|
|
1828
1841
|
border-radius: 16px;
|
|
1829
1842
|
background: transparent;
|
|
1830
1843
|
border: 0;
|
|
1844
|
+
margin-right: 4px;
|
|
1831
1845
|
flex-shrink: 0;
|
|
1832
1846
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
|
|
1833
1847
|
cursor: pointer;
|
|
1834
1848
|
transition: color 160ms ease, background-color 160ms ease;
|
|
1835
|
-
|
|
1836
|
-
&:hover {
|
|
1837
|
-
background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
|
|
1838
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 72%, transparent);
|
|
1839
|
-
}
|
|
1840
1849
|
`;
|
|
1841
1850
|
const RecordTrack = styled.div `
|
|
1842
1851
|
flex: 1;
|
|
@@ -1852,7 +1861,8 @@ const RecordTrack = styled.div `
|
|
|
1852
1861
|
const ButtonSlot = styled.div `
|
|
1853
1862
|
display: flex;
|
|
1854
1863
|
align-items: center;
|
|
1855
|
-
gap:
|
|
1864
|
+
gap: 4px;
|
|
1865
|
+
padding: 2px;
|
|
1856
1866
|
flex-shrink: 0;
|
|
1857
1867
|
`;
|
|
1858
1868
|
const StopButton = styled.button `
|
|
@@ -1989,19 +1999,39 @@ const VoiceRecorderInput = (props) => {
|
|
|
1989
1999
|
const waveformRef = React.useRef(null);
|
|
1990
2000
|
const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
|
|
1991
2001
|
const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
|
|
1992
|
-
const
|
|
2002
|
+
const [visibleBarsCount, setVisibleBarsCount] = React.useState(0);
|
|
2003
|
+
const measureBars = () => {
|
|
1993
2004
|
const el = waveformRef.current;
|
|
1994
2005
|
if (!el) {
|
|
1995
|
-
return
|
|
2006
|
+
return;
|
|
1996
2007
|
}
|
|
1997
|
-
|
|
2008
|
+
setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
|
|
2009
|
+
};
|
|
2010
|
+
/** Sync recalc before paint — catches layout shifts from button changes */
|
|
2011
|
+
React.useLayoutEffect(measureBars);
|
|
2012
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2013
|
+
React.useEffect(() => {
|
|
2014
|
+
const el = waveformRef.current;
|
|
2015
|
+
if (!el) {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
let rafId = 0;
|
|
2019
|
+
const observer = new ResizeObserver(() => {
|
|
2020
|
+
cancelAnimationFrame(rafId);
|
|
2021
|
+
rafId = requestAnimationFrame(measureBars);
|
|
2022
|
+
});
|
|
2023
|
+
observer.observe(el);
|
|
2024
|
+
return () => {
|
|
2025
|
+
cancelAnimationFrame(rafId);
|
|
2026
|
+
observer.disconnect();
|
|
2027
|
+
};
|
|
1998
2028
|
}, []);
|
|
1999
2029
|
const displayTime = (isPreview && isPlaying)
|
|
2000
2030
|
? formatTime(playbackTime)
|
|
2001
2031
|
: formatTime(recordingTime || lastDurationRef.current);
|
|
2002
|
-
return (jsxRuntime.jsxs(Wrapper, { className: className, children: [jsxRuntime.jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsxRuntime.jsx(icons.
|
|
2032
|
+
return (jsxRuntime.jsxs(Wrapper, { className: className, children: [jsxRuntime.jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsxRuntime.jsx(icons.Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 28, height: 28, style: { minWidth: 28 } }) }), jsxRuntime.jsxs(RecordTrack, { children: [jsxRuntime.jsx(ButtonSlot, { children: isPreview ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsxRuntime.jsx(icons.Icon20Voice, { width: 14, height: 14 }) }), jsxRuntime.jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
|
|
2003
2033
|
? jsxRuntime.jsx(icons.Icon16Pause, { width: 14, height: 14 })
|
|
2004
|
-
: jsxRuntime.jsx(icons.Icon16Play, { width: 14, height: 14 }) })] })) : (jsxRuntime.jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsxRuntime.jsx(icons.
|
|
2034
|
+
: jsxRuntime.jsx(icons.Icon16Play, { width: 14, height: 14 }) })] })) : (jsxRuntime.jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsxRuntime.jsx(icons.Icon16Pause, { width: 14, height: 14 }) })) }), jsxRuntime.jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsxRuntime.jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, visibleBarsCount: visibleBarsCount })) : (jsxRuntime.jsx(TimelineBars, { timelineLevels: timelineLevels, visibleBarsCount: visibleBarsCount })) }), jsxRuntime.jsx(Timer, { children: displayTime })] }), jsxRuntime.jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsxRuntime.jsx(SpinnerIcon, {})) : (jsxRuntime.jsx(icons.Icon28SendOutline, {})) })] }));
|
|
2005
2035
|
};
|
|
2006
2036
|
|
|
2007
2037
|
/**
|
|
@@ -2130,8 +2160,9 @@ const BAR_GAP = 2;
|
|
|
2130
2160
|
const BAR_MIN_HEIGHT = 2;
|
|
2131
2161
|
const BAR_MAX_HEIGHT = 24;
|
|
2132
2162
|
const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
2133
|
-
const { src, onClick, onEnded, chainId, duration, className, waveformData, } = props;
|
|
2163
|
+
const { src, onClick, onEnded, chainId, duration, className, waveformData, autoPreload = true, } = props;
|
|
2134
2164
|
const trackRef = React.useRef(null);
|
|
2165
|
+
const wrapperRef = React.useRef(null);
|
|
2135
2166
|
const [trackWidth, setTrackWidth] = React.useState(0);
|
|
2136
2167
|
const handleEnded = React.useCallback(() => {
|
|
2137
2168
|
onEnded === null || onEnded === void 0 ? void 0 : onEnded();
|
|
@@ -2139,25 +2170,66 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2139
2170
|
VoicePlaybackChain.playNext(chainId);
|
|
2140
2171
|
}
|
|
2141
2172
|
}, [onEnded, chainId]);
|
|
2142
|
-
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
2173
|
+
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
2143
2174
|
src,
|
|
2144
2175
|
duration,
|
|
2145
2176
|
waveformData,
|
|
2146
2177
|
preloadedBlob: props.preloadedBlob,
|
|
2147
2178
|
onEnded: handleEnded,
|
|
2148
2179
|
});
|
|
2149
|
-
React.useImperativeHandle(ref, () => ({ play }), [play]);
|
|
2180
|
+
React.useImperativeHandle(ref, () => ({ play, prefetch }), [play, prefetch]);
|
|
2181
|
+
/** Auto-preload audio when player enters viewport */
|
|
2182
|
+
React.useEffect(() => {
|
|
2183
|
+
if (!autoPreload || isLoaded || isLoading) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const el = wrapperRef.current;
|
|
2187
|
+
if (!el) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
2191
|
+
if (entry.isIntersecting) {
|
|
2192
|
+
prefetch();
|
|
2193
|
+
observer.disconnect();
|
|
2194
|
+
}
|
|
2195
|
+
}, { threshold: 0.1 });
|
|
2196
|
+
observer.observe(el);
|
|
2197
|
+
return () => observer.disconnect();
|
|
2198
|
+
}, [
|
|
2199
|
+
isLoaded,
|
|
2200
|
+
prefetch,
|
|
2201
|
+
isLoading,
|
|
2202
|
+
autoPreload,
|
|
2203
|
+
]);
|
|
2150
2204
|
/** Register in playback chain for sequential playback */
|
|
2151
2205
|
React.useEffect(() => {
|
|
2152
2206
|
if (chainId != null) {
|
|
2153
2207
|
return VoicePlaybackChain.register(chainId, () => play());
|
|
2154
2208
|
}
|
|
2155
2209
|
}, [chainId, play]);
|
|
2156
|
-
|
|
2157
|
-
React.useLayoutEffect(() => {
|
|
2210
|
+
const measureTrackWidth = () => {
|
|
2158
2211
|
if (trackRef.current) {
|
|
2159
2212
|
setTrackWidth(trackRef.current.clientWidth);
|
|
2160
2213
|
}
|
|
2214
|
+
};
|
|
2215
|
+
/** Sync recalc before paint — catches layout shifts */
|
|
2216
|
+
React.useLayoutEffect(measureTrackWidth);
|
|
2217
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2218
|
+
React.useEffect(() => {
|
|
2219
|
+
const el = trackRef.current;
|
|
2220
|
+
if (!el) {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
let rafId = 0;
|
|
2224
|
+
const observer = new ResizeObserver(() => {
|
|
2225
|
+
cancelAnimationFrame(rafId);
|
|
2226
|
+
rafId = requestAnimationFrame(measureTrackWidth);
|
|
2227
|
+
});
|
|
2228
|
+
observer.observe(el);
|
|
2229
|
+
return () => {
|
|
2230
|
+
cancelAnimationFrame(rafId);
|
|
2231
|
+
observer.disconnect();
|
|
2232
|
+
};
|
|
2161
2233
|
}, []);
|
|
2162
2234
|
const displayTime = isPlaying
|
|
2163
2235
|
? formatTime(currentTime)
|
|
@@ -2193,7 +2265,7 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2193
2265
|
const peak = Math.max(...raw, 1);
|
|
2194
2266
|
return raw.map((v) => (v / peak) * 100);
|
|
2195
2267
|
}, [waveformLevels, trackWidth]);
|
|
2196
|
-
return (jsxRuntime.jsxs(PlayerWrapper, { onClick: onClick, className: className, children: [jsxRuntime.jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsxRuntime.jsx(LoadingSpinner, {})) : isPlaying ? (jsxRuntime.jsx(icons.Icon24Pause, {})) : (jsxRuntime.jsx(icons.Icon24Play, {})) }), jsxRuntime.jsxs(ContentColumn, { children: [jsxRuntime.jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
|
|
2268
|
+
return (jsxRuntime.jsxs(PlayerWrapper, { ref: wrapperRef, onClick: onClick, className: className, children: [jsxRuntime.jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsxRuntime.jsx(LoadingSpinner, {})) : isPlaying ? (jsxRuntime.jsx(icons.Icon24Pause, {})) : (jsxRuntime.jsx(icons.Icon24Play, {})) }), jsxRuntime.jsxs(ContentColumn, { children: [jsxRuntime.jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
|
|
2197
2269
|
const normalized = Math.min(100, level);
|
|
2198
2270
|
const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);
|
|
2199
2271
|
const barProgress = interpolatedBars.length > 0
|