diff --git a/.vscode/settings.json b/.vscode/settings.json
index 02c48f6..9e1fcd6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,6 +4,7 @@
"cascader",
"contacto",
"datepicker",
- "listbox"
+ "listbox",
+ "wavesurfer"
]
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 1f63381..11e7ada 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@contacto-io/style-guide",
- "version": "0.5.0",
+ "version": "0.5.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 1d6dc92..bdd0cf3 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"registry": "https://npm.pkg.github.com"
},
"repository": "git://github.com/contacto-io/contacto-console",
- "version": "0.5.0",
+ "version": "0.5.10",
"main": "build/index.js",
"module": "build/index.es.js",
"files": [
@@ -105,6 +105,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-sortable-hoc": "^2.0.0",
+ "wavesurfer.js": "^7.0.3",
"simplebar": "^5.3.5",
"simplebar-react": "^2.3.5"
}
diff --git a/src/components/AudioPlayer/components/PlayPauseIcon.js b/src/components/AudioPlayer/components/PlayPauseIcon.js
new file mode 100644
index 0000000..8e7eded
--- /dev/null
+++ b/src/components/AudioPlayer/components/PlayPauseIcon.js
@@ -0,0 +1,36 @@
+/* eslint-disable max-len */
+import React from 'react'
+
+export default function PlayPauseIcon({ isPlaying = false, size = 32 }) {
+ if (!isPlaying) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/AudioPlayer/components/PlaybackSpeed.js b/src/components/AudioPlayer/components/PlaybackSpeed.js
new file mode 100644
index 0000000..30f8dd6
--- /dev/null
+++ b/src/components/AudioPlayer/components/PlaybackSpeed.js
@@ -0,0 +1,42 @@
+import React, { useEffect, useState } from 'react'
+import { Popover } from 'antd'
+import { Button } from '../../Button/index'
+
+const speeds = [0.8, 1, 1.2, 1.5, 1.7, 2, 2.5]
+export default function PlaybackSpeed({ waveSurfer }) {
+ const [speed, setSpeed] = useState(waveSurfer?.getPlaybackRate())
+
+ const handleSpeedChange = (speed) => {
+ setSpeed(speed)
+ waveSurfer?.setPlaybackRate(speed)
+ }
+
+ useEffect(() => {
+ setSpeed(waveSurfer?.getPlaybackRate())
+ }, [waveSurfer])
+
+ return (
+ (
+
+ {speeds.map((speed) => (
+
+ ))}
+
+ )}
+ >
+
+
+ )
+}
diff --git a/src/components/AudioPlayer/helpers/useWaveSurfer.js b/src/components/AudioPlayer/helpers/useWaveSurfer.js
new file mode 100644
index 0000000..326c422
--- /dev/null
+++ b/src/components/AudioPlayer/helpers/useWaveSurfer.js
@@ -0,0 +1,83 @@
+import { useLayoutEffect, useRef, useState } from 'react'
+
+import WaveSurfer from 'wavesurfer.js'
+import { generateId } from './utils'
+
+const defaultPlayerConfig = (playerId) => ({
+ id: playerId,
+ isPlaying: false,
+ loading: true,
+})
+const defaultDurationConfig = () => ({
+ totalDuration: 0,
+ currentDuration: 0,
+})
+export default function useWaveSurfer(url) {
+ const playerId = useRef(generateId('contacto-player-wave-')).current
+ const [playerConfig, setPlayerConfig] = useState(defaultPlayerConfig(playerId))
+ const [durationConfig, setDurationConfig] = useState(defaultDurationConfig())
+ const [waveSurfer, setWaveSurfer] = useState(null)
+
+ useLayoutEffect(() => {
+ let wave = null
+ if (url) {
+ wave = WaveSurfer.create({
+ container: `#${playerId}`,
+ url: url,
+ waveColor: '#C3D2FF',
+ progressColor: '#0040E4',
+ cursorColor: '#0040E4',
+ responsive: true,
+ height: 24,
+ barHeight: 3,
+ barMinHeight: 1,
+ barWidth: 1,
+ barGap: 4,
+ hideScrollbar: true,
+ closeAudioContext: true,
+ partialRender: true,
+ })
+ wave.on('load', () => {
+ setPlayerConfig((prevConfig) => ({ ...prevConfig, loading: true }))
+ })
+ wave.on('ready', () => {
+ setDurationConfig((prevConfig) => ({
+ ...prevConfig,
+ totalDuration: parseInt(wave.getDuration()),
+ }))
+ setPlayerConfig((prevConfig) => ({ ...prevConfig, loading: false }))
+ })
+ wave.on('audioprocess', () => {
+ const duration = parseInt(wave.getCurrentTime())
+ if (durationConfig.currentDuration !== duration) {
+ setDurationConfig((prevConfig) => ({
+ ...prevConfig,
+ currentDuration: duration,
+ }))
+ }
+ })
+ wave.on('play', () => {
+ setPlayerConfig((prevConfig) => ({ ...prevConfig, isPlaying: true }))
+ })
+ wave.on('pause', () => {
+ setPlayerConfig((prevConfig) => ({ ...prevConfig, isPlaying: false }))
+ })
+ wave.on('finish', () => {
+ setPlayerConfig((prevConfig) => ({ ...prevConfig, isPlaying: false }))
+ })
+
+ setWaveSurfer(wave)
+ }
+
+ return () => {
+ wave?.stop()
+ wave?.destroy()
+ setWaveSurfer(null)
+ setPlayerConfig(defaultPlayerConfig(playerId))
+ setDurationConfig(defaultDurationConfig())
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [playerId, url])
+
+ return { waveSurfer, playerConfig, durationConfig }
+}
diff --git a/src/components/AudioPlayer/helpers/utils.js b/src/components/AudioPlayer/helpers/utils.js
new file mode 100644
index 0000000..baa47a3
--- /dev/null
+++ b/src/components/AudioPlayer/helpers/utils.js
@@ -0,0 +1,12 @@
+export const getDisplayTime = (seconds) => {
+ const int = +seconds / 60
+ let minutes = parseInt(int)
+ seconds = seconds % 60
+ minutes = (minutes < 10 ? '0' : '') + minutes
+ seconds = (seconds < 10 ? '0' : '') + seconds
+ return minutes + ':' + seconds
+}
+
+export const generateId = (prefix) => {
+ return `${prefix}${Math.random().toString(36).slice(2)}`
+}
diff --git a/src/components/AudioPlayer/index.js b/src/components/AudioPlayer/index.js
new file mode 100644
index 0000000..73dfb42
--- /dev/null
+++ b/src/components/AudioPlayer/index.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import Text from 'antd/lib/typography/Text'
+import { forwardRef } from 'react'
+import PlayPauseIcon from './components/PlayPauseIcon'
+import useWaveSurfer from './helpers/useWaveSurfer'
+import PlaybackSpeed from './components/PlaybackSpeed'
+import { Button } from '../Button/index'
+import { Icon } from '../Icon/index'
+import { getDisplayTime } from './helpers/utils'
+import './styles.scss'
+
+const AudioPlayer = forwardRef((props, ref) => {
+ const { className, url } = props
+ const { waveSurfer, playerConfig, durationConfig } = useWaveSurfer(url)
+
+ const { isPlaying, loading } = playerConfig
+ const { totalDuration, currentDuration } = durationConfig
+
+ return (
+
+
+
+
+ )
+})
+
+AudioPlayer.displayName = 'AudioPlayer'
+export { AudioPlayer }
diff --git a/src/components/AudioPlayer/index.stories.js b/src/components/AudioPlayer/index.stories.js
new file mode 100755
index 0000000..771601b
--- /dev/null
+++ b/src/components/AudioPlayer/index.stories.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { AudioPlayer } from './'
+
+export default {
+ title: 'Components/Audio Player',
+ component: AudioPlayer,
+}
+
+const Template = (args) =>
+
+export const Default = Template.bind({})
+Default.args = {
+ url: 'https://wavesurfer-js.org/wavesurfer-code/examples/audio/mono.mp3',
+}
diff --git a/src/components/AudioPlayer/styles.scss b/src/components/AudioPlayer/styles.scss
new file mode 100644
index 0000000..b59da7a
--- /dev/null
+++ b/src/components/AudioPlayer/styles.scss
@@ -0,0 +1,128 @@
+@mixin playback-speed-button {
+ display: flex;
+ width: 32px;
+ height: 26px !important;
+ min-width: unset !important;
+ justify-content: center;
+ background: transparent;
+ border-radius: 4px !important;
+ align-items: center;
+ padding: 0;
+ font-size: 12px;
+ font-weight: 400;
+ transition: none;
+
+ &:hover {
+ background: transparent !important;
+ }
+
+ &.contacto-button--table-action-link {
+ background-color: var(--background-highlight-color);
+
+ &:hover {
+ background-color: var(--background-highlight-color) !important;
+ }
+ }
+}
+
+.contacto-audio-player {
+ .audio-controls {
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .audio-controls-play-pause {
+ min-width: 38px;
+ max-width: 38px;
+ height: 36px;
+ padding: 0;
+ border: none;
+ box-shadow: none;
+
+ &::after {
+ display: none;
+ }
+ }
+ .audio-controls-wave-bar {
+ flex: 1;
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+
+ .audio-controls-time {
+ width: max-content;
+ min-width: max-content;
+
+ .ant-typography {
+ color: var(--gray-1);
+ }
+
+ &.left {
+ margin-left: 16px;
+ }
+ }
+
+ .audio-controls-wave-bar {
+ height: 24px;
+ background-color: transparent;
+ overflow: hidden;
+ transition: height 0.3s ease-in-out;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ div {
+ width: 100%;
+ }
+ }
+
+ .contacto-player-speed-trigger {
+ @include playback-speed-button;
+ margin-left: 32px;
+ }
+ }
+ &.loading {
+ .audio-controls-wave-bar {
+ height: 2px;
+ background-color: var(--gray-4);
+ }
+ }
+}
+
+.contacto-player-speed {
+ padding-top: 8px;
+
+ .ant-popover-arrow {
+ display: none;
+ }
+ .ant-popover-inner {
+ padding: 16px;
+ box-shadow: var(--box-shadow-default);
+
+ .ant-popover-title {
+ height: unset;
+ min-height: unset;
+ border: none;
+ padding: 0;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--primary-text-color);
+ margin-bottom: 8px;
+ }
+ .ant-popover-inner-content {
+ padding: 0;
+
+ div {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+
+ .contacto-player-speed {
+ @include playback-speed-button;
+ }
+ }
+ }
+ }
+}
diff --git a/src/index.js b/src/index.js
index 4343216..e9f5e39 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,4 @@
+export * from './components/AudioPlayer/index'
export * from './components/Button/index'
export * from './components/Textfield/index'
export * from './components/Typography/index'