Skip to content

Commit 279bc86

Browse files
dougfabrisggazzo
andauthored
feat: AudioPlayer Component (#1046)
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
1 parent 6bae1cf commit 279bc86

File tree

9 files changed

+269
-3
lines changed

9 files changed

+269
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
Title,
3+
Subtitle,
4+
Description,
5+
Primary as PrimaryStory,
6+
ArgsTable,
7+
Stories,
8+
PRIMARY_STORY,
9+
} from '@storybook/addon-docs';
10+
import type { ComponentMeta } from '@storybook/react';
11+
import React from 'react';
12+
13+
import { AudioPlayer } from '../..';
14+
15+
export default {
16+
title: 'Media/AudioPlayer',
17+
component: AudioPlayer,
18+
parameters: {
19+
docs: {
20+
description: {
21+
component: 'A fuselage`s custom AudioPlayer.',
22+
},
23+
page: () => (
24+
<>
25+
<Title />
26+
<Subtitle />
27+
<Description />
28+
<PrimaryStory />
29+
<Stories title={''} />
30+
<ArgsTable story={PRIMARY_STORY} />
31+
</>
32+
),
33+
},
34+
},
35+
} as ComponentMeta<typeof AudioPlayer>;
36+
37+
const AUDIO_URL =
38+
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-17.mp3';
39+
40+
export const AudioPlayerDefault = () => <AudioPlayer src={AUDIO_URL} />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useMergedRefs } from '@rocket.chat/fuselage-hooks';
2+
import React, { useState, useRef, forwardRef } from 'react';
3+
4+
import { Box, IconButton } from '../..';
5+
import { Slider } from '../Slider';
6+
7+
const getMaskTime = (durationTime: number) =>
8+
new Date(durationTime * 1000)
9+
.toISOString()
10+
.slice(durationTime > 60 * 60 ? 11 : 14, 19);
11+
12+
function forceDownload(url: string, fileName?: string) {
13+
const xhr = new XMLHttpRequest();
14+
xhr.open('GET', url, true);
15+
xhr.responseType = 'blob';
16+
xhr.onload = function () {
17+
const urlCreator = window.URL || window.webkitURL;
18+
const imageUrl = urlCreator.createObjectURL(this.response);
19+
const tag = document.createElement('a');
20+
tag.href = imageUrl;
21+
if (fileName) {
22+
tag.download = fileName;
23+
}
24+
document.body.appendChild(tag);
25+
tag.click();
26+
document.body.removeChild(tag);
27+
};
28+
xhr.send();
29+
}
30+
31+
export const AudioPlayer = forwardRef<
32+
HTMLAudioElement,
33+
{
34+
src: string;
35+
maxPlaybackSpeed?: number;
36+
minPlaybackSpeed?: number;
37+
playbackSpeedStep?: number;
38+
}
39+
>(
40+
(
41+
{
42+
src,
43+
maxPlaybackSpeed = 2,
44+
minPlaybackSpeed = 0.5,
45+
playbackSpeedStep = 0.5,
46+
},
47+
ref
48+
) => {
49+
const audioRef = useRef<HTMLAudioElement>(null);
50+
const refs = useMergedRefs(ref, audioRef);
51+
const [isPlaying, setIsPlaying] = useState(false);
52+
const [currentTime, setCurrentTime] = useState(0);
53+
const [durationTime, setDurationTime] = useState(0);
54+
const [playbackSpeed, setPlaybackSpeed] = useState(1);
55+
56+
const handlePlay = () => {
57+
const isPlaying = audioRef.current?.paused;
58+
59+
if (isPlaying) {
60+
audioRef.current?.play();
61+
} else {
62+
audioRef.current?.pause();
63+
}
64+
};
65+
66+
const handlePlaybackSpeed = (mod: 1 | -1) => {
67+
if (audioRef.current) {
68+
audioRef.current.playbackRate = Math.max(
69+
Math.min(
70+
audioRef.current.playbackRate + playbackSpeedStep * mod,
71+
maxPlaybackSpeed
72+
),
73+
minPlaybackSpeed
74+
);
75+
}
76+
};
77+
78+
const handleIncreasePlayBackSpeed = () => handlePlaybackSpeed(1);
79+
80+
const handleDecreasePlayBackSpeed = () => handlePlaybackSpeed(-1);
81+
82+
return (
83+
<Box
84+
borderWidth='default'
85+
borderColor='extra-light'
86+
p='x16'
87+
width='fit-content'
88+
borderRadius='x4'
89+
>
90+
<Box display='flex' alignItems='center'>
91+
<IconButton
92+
large
93+
onClick={handlePlay}
94+
icon={isPlaying ? 'pause-unfilled' : 'play-unfilled'}
95+
/>
96+
<Box mi='x12' position='relative'>
97+
<Slider
98+
showOutput={false}
99+
value={currentTime}
100+
maxValue={durationTime}
101+
onChange={(value) => {
102+
if (audioRef.current) {
103+
audioRef.current.currentTime = value;
104+
}
105+
}}
106+
/>
107+
<Box
108+
display='flex'
109+
alignItems='center'
110+
justifyContent='space-between'
111+
color='secondary-info'
112+
fontScale='micro'
113+
position='absolute'
114+
width='100%'
115+
mb={'neg-x8'}
116+
>
117+
{getMaskTime(currentTime)}
118+
<Box
119+
fontScale='micro'
120+
display='flex'
121+
justifyContent='space-around'
122+
id='controllers'
123+
>
124+
<Box
125+
mi='x8'
126+
display='flex'
127+
alignItems='center'
128+
justifyContent='space-between'
129+
>
130+
<IconButton
131+
disabled={playbackSpeed <= minPlaybackSpeed}
132+
icon='h-bar'
133+
mini
134+
onClick={handleDecreasePlayBackSpeed}
135+
/>
136+
<Box mi='x8'>{playbackSpeed.toFixed(1)}x</Box>
137+
<IconButton
138+
disabled={playbackSpeed >= maxPlaybackSpeed}
139+
icon='plus'
140+
mini
141+
onClick={handleIncreasePlayBackSpeed}
142+
/>
143+
</Box>
144+
</Box>
145+
{getMaskTime(durationTime)}
146+
</Box>
147+
</Box>
148+
<IconButton
149+
is='a'
150+
href={src}
151+
download
152+
icon='download'
153+
large
154+
onClick={(e) => {
155+
const { host } = new URL(src);
156+
if (host !== window.location.host) {
157+
e.preventDefault();
158+
forceDownload(src);
159+
}
160+
}}
161+
/>
162+
</Box>
163+
<audio
164+
style={{ display: 'none' }}
165+
onTimeUpdate={(e) => {
166+
setCurrentTime((e.target as HTMLAudioElement).currentTime);
167+
}}
168+
onLoadedData={(e) => {
169+
setDurationTime((e.target as HTMLAudioElement).duration);
170+
}}
171+
onEnded={() => setIsPlaying(false)}
172+
ref={refs}
173+
preload='metadata'
174+
onRateChange={(e) => {
175+
setPlaybackSpeed((e.target as HTMLAudioElement).playbackRate);
176+
}}
177+
onPlay={() => {
178+
setIsPlaying(true);
179+
}}
180+
onPause={() => {
181+
setIsPlaying(false);
182+
}}
183+
controls
184+
>
185+
<source src={src} type='audio/mpeg' />
186+
</audio>
187+
</Box>
188+
);
189+
}
190+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './AudioPlayer';

packages/fuselage/src/components/Button/Button.styles.scss

+14
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@
5858
);
5959
}
6060

61+
&--large {
62+
@include typography.use-font-scale(p1);
63+
64+
@include with-rectangular-size(
65+
$height: 48px,
66+
$padding-x: 24px,
67+
$line-height: typography.line-height(p1)
68+
);
69+
}
70+
6171
&--square {
6272
@include with-squared-size($size: 40px);
6373
display: flex;
@@ -125,6 +135,10 @@
125135
@include with-squared-size($size: 32px);
126136
}
127137

138+
&--large-square {
139+
@include with-squared-size($size: 48px);
140+
}
141+
128142
&--primary {
129143
@include button.kind-variant(colors.$primary);
130144
}

packages/fuselage/src/components/Button/Button.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type ButtonProps = ComponentProps<typeof Box> & {
1313
small?: boolean;
1414
mini?: boolean;
1515
tiny?: boolean;
16+
large?: boolean;
1617
square?: boolean;
1718
external?: boolean;
1819
};
@@ -30,6 +31,7 @@ export const Button = forwardRef(function Button(
3031
small,
3132
tiny,
3233
mini,
34+
large,
3335
square,
3436
...props
3537
}: ButtonProps,
@@ -73,10 +75,12 @@ export const Button = forwardRef(function Button(
7375
rcx-button
7476
{...kindAndVariantProps}
7577
rcx-button--small={small}
78+
rcx-button--large={large}
7679
rcx-button--square={square}
7780
rcx-button--small-square={small && square}
7881
rcx-button--tiny-square={tiny && square}
7982
rcx-button--mini-square={mini && square}
83+
rcx-button--large-square={large && square}
8084
ref={ref}
8185
{...extraProps}
8286
{...props}

packages/fuselage/src/components/Button/IconButton.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Box from '../Box';
55
import { Icon } from '../Icon';
66

77
type ButtonSize = {
8+
large?: boolean;
89
medium?: boolean;
910
small?: boolean;
1011
tiny?: boolean;
@@ -51,6 +52,7 @@ export const IconButton = forwardRef(
5152
warning,
5253
success,
5354
mini,
55+
large,
5456
tiny,
5557
small,
5658
medium,
@@ -91,8 +93,9 @@ export const IconButton = forwardRef(
9193
(mini && 'mini') ||
9294
(tiny && 'tiny') ||
9395
(small && 'small') ||
94-
(medium && 'medium'),
95-
[medium, mini, small, tiny]
96+
(medium && 'medium') ||
97+
(large && 'large'),
98+
[medium, mini, small, tiny, large]
9699
);
97100

98101
const getSizeClass = () => ({ [`rcx-button--${size}-square`]: true });
@@ -107,6 +110,11 @@ export const IconButton = forwardRef(
107110
if (medium) {
108111
return 'x20';
109112
}
113+
114+
if (large) {
115+
return 'x32';
116+
}
117+
110118
return 'x24';
111119
};
112120

packages/fuselage/src/components/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './Accordion';
22
export { default as AnimatedVisibility } from './AnimatedVisibility';
3+
export * from './AudioPlayer';
34
export * from './AutoComplete';
45
export * from './Avatar';
56
export * from './Badge';
@@ -45,6 +46,7 @@ export * from './RadioButton';
4546
export { default as Scrollable } from './Scrollable';
4647
export * from './SearchInput';
4748
export * from './Select';
49+
export * from './Slider';
4850
export * from './PaginatedSelect';
4951
export * from './SelectInput';
5052
export { default as Sidebar } from './Sidebar';

packages/fuselage/src/styles/colors.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ $-map-type-to-prefix: (
3333
$base-color: map.get(token-colors.$colors, #{$prefix}#{$grade});
3434

3535
@if not $base-color {
36-
@error 'invalid color reference';
36+
@error 'invalid color reference: #{$prefix}#{$grade}';
3737
}
3838

3939
@if ($alpha != null) {

packages/fuselage/webpack.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ module.exports = (env, { mode = 'production' }) => ({
9999
'react-dom',
100100
'@rocket.chat/icons',
101101
'@rocket.chat/fuselage-hooks',
102+
'react-aria',
103+
'react-stately',
104+
'@rocket.chat/css-in-js',
105+
'@rocket.chat/css-supports',
106+
'@rocket.chat/fuselage-tokens',
107+
'@rocket.chat/memo',
108+
'@rocket.chat/styled',
102109
],
103110
plugins: [
104111
new webpack.DefinePlugin({

0 commit comments

Comments
 (0)