Merge remote-tracking branch 'origin/react-player' into develop

This commit is contained in:
Hossein Mehrabi 2023-08-11 19:58:07 +03:30
commit 70f83844d1
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
17 changed files with 516 additions and 1 deletions

View File

@ -48,6 +48,7 @@
"react-blurhash": "^0.3.0",
"react-dom": "18.2.0",
"react-imgix": "^9.7.0",
"react-player": "^2.12.0",
"typescript": "5.0.4"
},
"devDependencies": {

View File

@ -4,9 +4,11 @@ import {
extractIdFromFirstTag,
extractInnerHtml,
} from '@/utils/html.utils'
import { convertToIframe } from '@/utils/string.utils'
import { HeadingElementsRef } from '@/utils/ui.utils'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import ReactPlayer from 'react-player'
import { LPE } from '../../types/lpe.types'
import { PostImageRatio } from '../Post/Post'
import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper'
@ -43,6 +45,55 @@ export const RenderArticleBlock = ({
/>
)
}
case 'p': {
const isIframeRegex = /<iframe[^>]*>(?:<\/iframe>|[^]*?<\/iframe>)/
const isIframe = isIframeRegex.test(block.text)
const isYoutubeRegex =
/^(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/.+$/
const isYoutube = isYoutubeRegex.test(block.text)
const youtubeLink = block.text.match(isYoutubeRegex) ?? []
const isSimplecastRegex =
/^https?:\/\/([a-zA-Z0-9-]+\.)*simplecast\.com\/[^?\s]+(\?[\s\S]*)?$/
const isSimplecast = isSimplecastRegex.test(block.text)
const simplecastLink = block.text.match(isSimplecastRegex) ?? []
// const episodeId = extractUUIDFromEpisode(simplecastLink[0] ?? '')
// let audioSrc = ''
// if (isSimplecast) {
// fetch(
// `https://api.simplecast.com/episodes/audio/bc313c16-82e9-439a-8e0c-af59833d22d7`,
// )
// .then((response) => response.json())
// .then((data) => console.log(data))
// }
return isIframe ? (
<IframeContainer dangerouslySetInnerHTML={{ __html: block.text }} />
) : isYoutube ? (
<ReactPlayer url={youtubeLink[0]} />
) : isSimplecast ? (
<IframeContainer
dangerouslySetInnerHTML={{
__html: convertToIframe(simplecastLink[0] ?? ''),
}}
/>
) : (
<Paragraph
variant="body1"
component={block.tagName as any}
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
id={extractIdFromFirstTag(block.html) || `p-${block.order}`}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>
)
}
default:
return (
<Paragraph
@ -67,3 +118,21 @@ const Paragraph = styled(Typography)`
line-height: var(--lsd-h6-lineHeight);
}
`
const IframeContainer = styled.div`
position: relative;
padding-bottom: 56.25%;
padding-top: 30px;
height: 0;
overflow: hidden;
& > iframe,
& > object,
& > embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
`

View File

@ -0,0 +1,20 @@
.audioPlayer > input[type='range'] {
width: 100%;
accent-color: black;
}
.audioPlayer > input[type='range']:focus {
outline: none;
}
.audioPlayer > input[type='range']::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: grabbing;
}
.audioPlayer > input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -4px;
}

View File

@ -0,0 +1,273 @@
import ReactPlayer from 'react-player'
import styled from '@emotion/styled'
import { useRef, useState } from 'react'
import { PlayIcon } from '../Icons/PlayIcon'
import { PauseIcon } from '../Icons/PauseIcon'
import { VolumeIcon } from '../Icons/VolumeIcon'
import styles from './GlobalAudioPlayer.module.css'
import { convertSecToMinAndSec } from '@/utils/string.utils'
import { Typography } from '@acid-info/lsd-react'
type StateProps = {
url: string | null
pip: boolean
playing: boolean
playedSeconds: number
controls: boolean
light: boolean
volume: number
muted: boolean
played: number
loaded: number
duration: number
playbackRate: number
loop: boolean
seeking: boolean
}
const TEMP_URL =
'https://cdn.simplecast.com/audio/b54c0885-7c72-415d-b032-7d294b78d785/episodes/30d4e2f5-4434-419c-8fc1-a76e4b367e20/audio/3c8eb229-3f34-45a4-84f1-ce1d6bd65922/default_tc.mp3'
export default function GlobalAudioPlayer() {
const ref = useRef<ReactPlayer>(null)
const [state, setState] = useState<StateProps>({
url: TEMP_URL,
pip: false,
playing: false,
playedSeconds: 0,
controls: false,
light: false,
volume: 0.8,
muted: false,
played: 0,
loaded: 0,
duration: 0,
playbackRate: 1.0,
loop: false,
seeking: false,
})
const [showVolume, setShowVolume] = useState(false)
// const handleLoad = (url: string) => {
// setState((prev) => ({ ...prev, url, played: 0, loaded: 0, pip: false }))
// }
const handlePlay = () => {
setState((prev) => ({ ...prev, playing: true }))
}
const handlePlayPause = () => {
setState((prev) => ({ ...prev, playing: !state.playing }))
}
// const handleStop = () => {
// setState((prev) => ({ ...prev, url: null, playing: false }))
// }
const handleEnded = () => {
setState((prev) => ({ ...prev, playing: prev.loop }))
}
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((prev) => ({ ...prev, volume: parseFloat(e.target.value) }))
}
// const handleToggleMuted = (e: React.ChangeEvent<HTMLInputElement>) => {
// setState((prev) => ({ ...prev, muted: !state.muted }))
// }
// const handleSetPlaybackRate = (e: React.ChangeEvent<HTMLInputElement>) => {
// setState((prev) => ({ ...prev, playbackRate: parseFloat(e.target.value) }))
// }
const handlePause = () => {
setState((prev) => ({ ...prev, playing: false }))
}
const handleSeekMouseDown = (
e: React.MouseEvent<HTMLInputElement, MouseEvent>,
) => {
setState((prev) => ({ ...prev, seeking: true }))
}
const handleSeekChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((prev) => ({ ...prev, played: parseFloat(e.target.value) }))
}
const handleSeekMouseUp = (
e: React.MouseEvent<HTMLInputElement, MouseEvent>,
) => {
setState((prev) => ({ ...prev, seeking: false }))
const target = e.target as HTMLInputElement
ref.current?.seekTo(parseFloat(target?.value))
}
const handleDuration = (duration: number) => {
setState((prev) => ({ ...prev, duration }))
}
const handleOnPlaybackRateChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
setState((prev) => ({ ...prev, playbackRate: parseFloat(e.target.value) }))
}
const handleProgress = (newState: { playedSeconds: number }) => {
setState((prev) => ({ ...prev, playedSeconds: newState.playedSeconds }))
}
return (
<Container>
<AudioPlayer>
<Buttons>
<Row>
<PlayPause onClick={handlePlayPause}>
{state.playing ? <PauseIcon /> : <PlayIcon />}
</PlayPause>
<TimeContainer>
<Time variant="body3">
{convertSecToMinAndSec(state.playedSeconds)}
</Time>
<Typography variant="body3">/</Typography>
<Time variant="body3">
{convertSecToMinAndSec(state.duration)}
</Time>
</TimeContainer>
</Row>
<VolumeContainer onClick={() => setShowVolume((prev) => !prev)}>
{showVolume && (
<VolumeGauge>
<input
type="range"
min={0}
max={1}
step="any"
value={state.volume}
onChange={handleVolumeChange}
/>
</VolumeGauge>
)}
<VolumeIcon />
</VolumeContainer>
</Buttons>
{/* <button onClick={handleStop}>Stop</button> */}
<Seek className={styles.audioPlayer}>
<input
type="range"
min={0}
max={0.999999}
step="any"
value={state.played}
onMouseDown={handleSeekMouseDown}
onChange={handleSeekChange}
onMouseUp={handleSeekMouseUp}
/>
</Seek>
</AudioPlayer>
<ReactPlayer
ref={ref}
style={{ display: 'none' }}
url={state.url ?? TEMP_URL}
width="100%"
height="100%"
pip={state.pip}
playing={state.playing}
controls={state.controls}
light={state.light}
loop={state.loop}
playbackRate={state.playbackRate}
volume={state.volume}
muted={state.muted}
onReady={() => console.log('onReady')}
onStart={() => console.log('onStart')}
onPlay={handlePlay}
onPause={handlePause}
onBuffer={() => console.log('onBuffer')}
onPlaybackRateChange={handleOnPlaybackRateChange}
onSeek={(e) => console.log('onSeek', e)}
onEnded={handleEnded}
onError={(e) => console.log('onError', e)}
onDuration={handleDuration}
onProgress={handleProgress}
/>
<RightMenu></RightMenu>
</Container>
)
}
const Container = styled.div`
width: 100vw;
height: 80px;
padding: 22px 16px;
background: rgb(var(--lsd-surface-primary));
position: fixed;
display: flex;
align-items: center;
justify-content: center;
bottom: 0;
left: 0;
border-top: 1px solid rgb(var(--lsd-border-primary));
box-sizing: border-box;
`
const Buttons = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
`
const VolumeContainer = styled.div`
display: flex;
justify-content: center;
position: relative;
align-items: center;
`
const Seek = styled.div`
display: flex;
width: 100%;
`
const VolumeGauge = styled.div`
position: absolute;
top: -30px;
`
const AudioPlayer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
width: 60%;
`
const RightMenu = styled.div`
width: 40%;
`
const PlayPause = styled.button`
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
margin-right: 8px;
`
const Row = styled.div`
display: flex;
align-items: center;
white-space: pre-wrap;
`
const TimeContainer = styled(Row)`
gap: 8px;
`
const Time = styled(Typography)`
width: 32px;
`

View File

@ -0,0 +1 @@
export { default as GlobalAudioPlayer } from './GlobalAudioPlayer'

View File

@ -0,0 +1,22 @@
import { LsdIcon } from '@acid-info/lsd-react'
export const PauseIcon = LsdIcon(
(props) => (
<svg
width="20px"
height="20px"
version="1.1"
viewBox="0 0 512 512"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
{...props}
>
<g>
<path d="M224,435.8V76.1c0-6.7-5.4-12.1-12.2-12.1h-71.6c-6.8,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6 C218.6,448,224,442.6,224,435.8z" />
<path d="M371.8,64h-71.6c-6.7,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6c6.7,0,12.2-5.4,12.2-12.2V76.1 C384,69.4,378.6,64,371.8,64z" />
</g>
</svg>
),
{ filled: true },
)

View File

@ -0,0 +1 @@
export * from './PauseIcon'

View File

@ -0,0 +1,20 @@
import { LsdIcon } from '@acid-info/lsd-react'
export const PlayIcon = LsdIcon(
(props) => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6.6665 15.8346V4.16797L15.8332 10.0013L6.6665 15.8346Z"
fill="black"
/>
</svg>
),
{ filled: true },
)

View File

@ -0,0 +1 @@
export * from './PlayIcon'

View File

@ -0,0 +1,27 @@
import { LsdIcon } from '@acid-info/lsd-react'
export const VolumeIcon = LsdIcon(
(props) => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path="url(#clip0_228_17463)">
<path
d="M2 5.99901V9.99901H4.66667L8 13.3323V2.66568L4.66667 5.99901H2ZM11 7.99901C11 6.81901 10.32 5.80568 9.33333 5.31234V10.679C10.32 10.1923 11 9.17901 11 7.99901ZM9.33333 2.15234V3.52568C11.26 4.09901 12.6667 5.88568 12.6667 7.99901C12.6667 10.1123 11.26 11.899 9.33333 12.4723V13.8457C12.0067 13.239 14 10.8523 14 7.99901C14 5.14568 12.0067 2.75901 9.33333 2.15234Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_228_17463">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
),
{ filled: true },
)

View File

@ -0,0 +1 @@
export * from './VolumeIcon'

View File

@ -8,6 +8,7 @@ import type { AppProps } from 'next/app'
import Head from 'next/head'
import { ReactNode } from 'react'
import { LSDThemeProvider } from '../containers/LSDThemeProvider'
import { GlobalAudioPlayer } from '@/components/GlobalAudioPlayer'
type NextLayoutComponentType<P = {}> = NextComponentType<
NextPageContext,
@ -94,6 +95,7 @@ export default function App({ Component, pageProps }: AppLayoutProps) {
<SearchBarProvider>
{getLayout(<Component {...pageProps} />)}
</SearchBarProvider>
<GlobalAudioPlayer />
</LSDThemeProvider>
)
}

View File

@ -0,0 +1 @@

View File

@ -24,3 +24,25 @@ export const shuffle = (array: any[]) => {
}
export const unique = (arr: any[]) => Array.from(new Set(arr))
export const getAudioSourceFromSimplecastPlayer = async (url: string) => {
const myHeaders = new Headers()
myHeaders.append(
'Authorization',
'Bearer eyJhcGlfa2V5IjoiMzg3OTdhY2Y5N2NmZjgzZjQxNGI5ODNiN2E2MjY3NmQifQ==',
)
const requestOptions = {
method: 'GET',
headers: myHeaders,
}
const result = await fetch(
`https://api.simplecast.com/episodes/${url}`,
requestOptions,
)
const data = await result.json()
console.log(data)
return data
}

View File

@ -41,3 +41,27 @@ export const calcReadingTime = (text: string): number => {
const numberOfWords = text.split(/\s/g).length
return Math.ceil(numberOfWords / wordsPerMinute)
}
export function convertSecToMinAndSec(totalSeconds: number) {
// Convert seconds to minutes and seconds
const minutes = Math.floor(totalSeconds / 60)
const seconds = Math.floor(totalSeconds % 60)
// Ensure two digit format
const formattedMinutes = String(minutes).padStart(2, '0')
const formattedSeconds = String(seconds).padStart(2, '0')
return `${formattedMinutes}:${formattedSeconds}`
}
export function extractUUIDFromEpisode(url: string) {
const regex = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/
const match = url.match(regex)
return match ? match[1] : null
}
export function convertToIframe(url: string) {
if (!url) return ''
return `<iframe height="200px" width="100%" frameborder="no" scrolling="no" seamless src="${url}"></iframe>`
}

View File

@ -83,7 +83,6 @@ export function useIntersectionObserver(
headings.forEach((heading) => {
if (heading.isIntersecting && heading.target instanceof HTMLElement) {
const targetId = heading.target.getAttribute('id')
console.log(targetId)
if (targetId) setActiveId(targetId)
}
})

View File

@ -2438,6 +2438,11 @@ defaults@^1.0.3:
integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==
dependencies:
clone "^1.0.2"
deepmerge@^4.0.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
define-lazy-prop@^2.0.0:
version "2.0.0"
@ -3938,6 +3943,11 @@ locate-path@^5.0.0:
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
load-script@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
locate-path@^6.0.0:
version "6.0.0"
@ -4033,6 +4043,11 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
memoize-one@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -4616,6 +4631,11 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-fast-compare@^3.0.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
react-imgix@^9.7.0:
version "9.7.0"
resolved "https://registry.yarnpkg.com/react-imgix/-/react-imgix-9.7.0.tgz#944f63693daf6524d07898aaf7d1cbbe59e5edca"
@ -4642,6 +4662,17 @@ react-measure@^2.3.0:
prop-types "^15.6.2"
resize-observer-polyfill "^1.5.0"
react-player@^2.12.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.12.0.tgz#2fc05dbfec234c829292fbca563b544064bd14f0"
integrity sha512-rymLRz/2GJJD+Wc01S7S+i9pGMFYnNmQibR2gVE3KmHJCBNN8BhPAlOPTGZtn1uKpJ6p4RPLlzPQ1OLreXd8gw==
dependencies:
deepmerge "^4.0.0"
load-script "^1.0.0"
memoize-one "^5.1.1"
prop-types "^15.7.2"
react-fast-compare "^3.0.1"
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"