audio hotfixies (#126)
* - audio hotfixies * refactoring: audio player * Update volume control styles * Artist Data conditional render * Update composeMediaItems title with remove File Extension --------- Co-authored-by: bniwredyc <bniwredyc@gmail.com>
This commit is contained in:
parent
e66eeb48df
commit
be9fea1265
|
@ -49,7 +49,7 @@
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
background: var(--placeholder-color-semi) url('../../icons/create-music.svg') no-repeat 50% 50%;
|
background: var(--placeholder-color-semi) url('/icons/create-music.svg') no-repeat 50% 50%;
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
|
@ -36,17 +36,19 @@ export const AudioHeader = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<h1>{props.title}</h1>
|
<h1>{props.title}</h1>
|
||||||
|
<Show when={props.artistData}>
|
||||||
<div class={styles.artistData}>
|
<div class={styles.artistData}>
|
||||||
<Show when={props.artistData.artist}>
|
<Show when={props.artistData?.artist}>
|
||||||
<div class={styles.item}>{props.artistData.artist}</div>
|
<div class={styles.item}>{props.artistData.artist}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.artistData.date}>
|
<Show when={props.artistData?.date}>
|
||||||
<div class={styles.item}>{props.artistData.date}</div>
|
<div class={styles.item}>{props.artistData.date}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.artistData.genre}>
|
<Show when={props.artistData?.genre}>
|
||||||
<div class={styles.item}>{props.artistData.genre}</div>
|
<div class={styles.item}>{props.artistData.genre}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -153,75 +153,56 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$vendors-track: ('::-webkit-slider-runnable-track', '::-moz-range-track', '::-ms-track');
|
||||||
|
$vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thumb');
|
||||||
|
|
||||||
.volume {
|
.volume {
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
right: 0;
|
|
||||||
bottom: 14px;
|
|
||||||
|
|
||||||
height: 28px;
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 10px 0;
|
height: 19px;
|
||||||
width: 80px;
|
float: left;
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
border: 2px solid black;
|
||||||
|
padding: 16px 8px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
left: -21px;
|
||||||
|
width: 100px;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
background: var(--background-color);
|
||||||
|
|
||||||
&::-moz-range-thumb,
|
@each $vendor in $vendors-track {
|
||||||
&::-webkit-slider-thumb {
|
&#{$vendor} {
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
border-radius: 100px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
&:focus::-webkit-slider-runnable-track,
|
|
||||||
&::-moz-range-thumb,
|
|
||||||
&::-webkit-slider-thumb,
|
|
||||||
&::-webkit-slider-runnable-track,
|
|
||||||
&::-moz-range-track,
|
|
||||||
&::-ms-track,
|
|
||||||
&::-ms-fill-lower,
|
|
||||||
&::-ms-fill-upper,
|
|
||||||
&::-ms-thumb {
|
|
||||||
background: #0e0e0e;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-slider-runnable-track,
|
|
||||||
&::-moz-range-track,
|
|
||||||
&::-ms-track {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6px;
|
height: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animate: 0.2s;
|
background: var(--background-color-invert);
|
||||||
border-radius: 10px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-ms-fill-lower,
|
@each $vendor in $vendors-thumb {
|
||||||
&::-ms-fill-upper {
|
&#{$vendor} {
|
||||||
border-radius: 10px;
|
position: relative;
|
||||||
}
|
-webkit-appearance: none;
|
||||||
|
box-sizing: content-box;
|
||||||
&::-ms-thumb {
|
width: 8px;
|
||||||
margin-top: 1px;
|
height: 8px;
|
||||||
height: 15px;
|
border-radius: 50%;
|
||||||
width: 15px;
|
border: 4px solid var(--default-color);
|
||||||
border-radius: 5px;
|
background-color: var(--background-color);
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin: -7px 0 0 0;
|
||||||
|
}
|
||||||
|
&:active#{$vendor} {
|
||||||
|
transform: scale(1.2);
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus::-ms-fill-lower,
|
&::-moz-range-progress {
|
||||||
&:focus::-ms-fill-upper {
|
background-color: var(--background-color);
|
||||||
background: #38bdf8;
|
}
|
||||||
|
&::-moz-focus-outer {
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,236 +1,170 @@
|
||||||
import { createEffect, createSignal, on, onMount, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, on, onMount, Show } from 'solid-js'
|
||||||
import { PlayerHeader } from './PlayerHeader'
|
import { PlayerHeader } from './PlayerHeader'
|
||||||
import { PlayerPlaylist } from './PlayerPlaylist'
|
import { PlayerPlaylist } from './PlayerPlaylist'
|
||||||
import styles from './AudioPlayer.module.scss'
|
import styles from './AudioPlayer.module.scss'
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
import { imageProxy } from '../../../utils/imageProxy'
|
||||||
|
|
||||||
export type Audio = {
|
|
||||||
pic?: string
|
|
||||||
index?: number
|
|
||||||
isCurrent?: boolean
|
|
||||||
isPlaying?: boolean
|
|
||||||
} & MediaItem
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
media: Audio[]
|
media: MediaItem[]
|
||||||
articleSlug?: string
|
articleSlug?: string
|
||||||
body?: string
|
body?: string
|
||||||
editorMode?: boolean
|
editorMode?: boolean
|
||||||
onAudioChange?: (index: number, field: string, value: string) => void
|
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void
|
||||||
}
|
|
||||||
|
|
||||||
const prepareMedia = (media: Audio[]) =>
|
|
||||||
media.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
url: imageProxy(item.url),
|
|
||||||
index: index,
|
|
||||||
isCurrent: false,
|
|
||||||
isPlaying: false
|
|
||||||
}))
|
|
||||||
|
|
||||||
const progressUpdate = (audioRef, progressFilledRef, duration) => {
|
|
||||||
progressFilledRef.current.style.width = `${(audioRef.current.currentTime / duration) * 100 || 0}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrub = (event, progressRef, duration, audioRef) => {
|
|
||||||
audioRef.current.currentTime = (event.offsetX / progressRef.current.offsetWidth) * duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
|
const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
|
||||||
|
|
||||||
export const AudioPlayer = (props: Props) => {
|
export const AudioPlayer = (props: Props) => {
|
||||||
const audioRef: { current: HTMLAudioElement } = { current: null }
|
const audioRef: { current: HTMLAudioElement } = { current: null }
|
||||||
|
const gainNodeRef: { current: GainNode } = { current: null }
|
||||||
const progressRef: { current: HTMLDivElement } = { current: null }
|
const progressRef: { current: HTMLDivElement } = { current: null }
|
||||||
const progressFilledRef: { current: HTMLDivElement } = { current: null }
|
const audioContextRef: { current: AudioContext } = { current: null }
|
||||||
|
const mouseDownRef: { current: boolean } = { current: false }
|
||||||
|
|
||||||
const [audioContext, setAudioContext] = createSignal<AudioContext>()
|
const [currentTrackDuration, setCurrentTrackDuration] = createSignal(0)
|
||||||
const [gainNode, setGainNode] = createSignal<GainNode>()
|
const [currentTime, setCurrentTime] = createSignal(0)
|
||||||
const [tracks, setTracks] = createSignal<Audio[] | null>(prepareMedia(props.media))
|
const [currentTrackIndex, setCurrentTrackIndex] = createSignal<number>(0)
|
||||||
const [duration, setDuration] = createSignal<number>(0)
|
const [isPlaying, setIsPlaying] = createSignal(false)
|
||||||
const [currentTimeContent, setCurrentTimeContent] = createSignal<string>('00:00')
|
|
||||||
const [currentDurationContent, setCurrentDurationContent] = createSignal<string>('00:00')
|
const currentTack = createMemo(() => props.media[currentTrackIndex()])
|
||||||
const [mousedown, setMousedown] = createSignal<boolean>(false)
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.media,
|
() => currentTrackIndex(),
|
||||||
() => {
|
() => {
|
||||||
setTracks(prepareMedia(props.media))
|
setCurrentTrackDuration(0)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const getCurrentTrack = () =>
|
|
||||||
tracks().find((track) => track.isCurrent) ||
|
|
||||||
(() => {
|
|
||||||
setTracks(
|
|
||||||
tracks().map((track, index) => ({
|
|
||||||
...track,
|
|
||||||
isCurrent: index === 0
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
return tracks()[0]
|
|
||||||
})()
|
|
||||||
|
|
||||||
createEffect(() => {
|
const handlePlayMedia = async (trackIndex: number) => {
|
||||||
if (audioRef.current.src !== getCurrentTrack().url) {
|
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
|
||||||
audioRef.current.src = getCurrentTrack().url
|
setCurrentTrackIndex(trackIndex)
|
||||||
|
|
||||||
audioRef.current.load()
|
if (audioContextRef.current.state === 'suspended') {
|
||||||
|
await audioContextRef.current.resume()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
if (isPlaying()) {
|
||||||
if (getCurrentTrack() && duration()) {
|
|
||||||
setCurrentDurationContent(getFormattedTime(duration()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const playMedia = async (m: Audio) => {
|
|
||||||
setTracks(
|
|
||||||
tracks().map((track) => ({
|
|
||||||
...track,
|
|
||||||
isCurrent: track.index === m.index,
|
|
||||||
isPlaying: track.index === m.index ? !track.isPlaying : false
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
progressUpdate(audioRef, progressFilledRef, duration())
|
|
||||||
|
|
||||||
if (audioContext().state === 'suspended') await audioContext().resume()
|
|
||||||
|
|
||||||
if (getCurrentTrack().isPlaying) {
|
|
||||||
await audioRef.current.play()
|
await audioRef.current.play()
|
||||||
} else {
|
} else {
|
||||||
audioRef.current.pause()
|
audioRef.current.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTimes = () => {
|
const handleVolumeChange = (volume: number) => {
|
||||||
setCurrentTimeContent(getFormattedTime(audioRef.current.currentTime))
|
gainNodeRef.current.gain.value = volume
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAudioEnd = () => {
|
const handleAudioEnd = () => {
|
||||||
progressFilledRef.current.style.width = '0%'
|
if (currentTrackIndex() < props.media.length - 1) {
|
||||||
|
playNextTrack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
audioRef.current.currentTime = 0
|
audioRef.current.currentTime = 0
|
||||||
|
setIsPlaying(false)
|
||||||
|
setCurrentTrackIndex(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAudioTimeUpdate = () => {
|
const handleAudioTimeUpdate = () => {
|
||||||
progressUpdate(audioRef, progressFilledRef, duration())
|
setCurrentTime(audioRef.current.currentTime)
|
||||||
|
|
||||||
setTimes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setAudioContext(new AudioContext())
|
audioContextRef.current = new AudioContext()
|
||||||
setGainNode(audioContext().createGain())
|
gainNodeRef.current = audioContextRef.current.createGain()
|
||||||
|
|
||||||
setTimes()
|
const track = audioContextRef.current.createMediaElementSource(audioRef.current)
|
||||||
|
track.connect(gainNodeRef.current).connect(audioContextRef.current.destination)
|
||||||
const track = audioContext().createMediaElementSource(audioRef.current)
|
|
||||||
track.connect(gainNode()).connect(audioContext().destination)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const playPrevTrack = () => {
|
const playPrevTrack = () => {
|
||||||
const { index } = getCurrentTrack()
|
let newCurrentTrackIndex = currentTrackIndex() - 1
|
||||||
const currIndex = tracks().findIndex((track) => track.index === index)
|
if (newCurrentTrackIndex < 0) {
|
||||||
|
newCurrentTrackIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
const getUpdatedStatus = (trackId) =>
|
setCurrentTrackIndex(newCurrentTrackIndex)
|
||||||
currIndex === 0
|
|
||||||
? trackId === tracks()[tracks().length - 1].index
|
|
||||||
: trackId === tracks()[currIndex - 1].index
|
|
||||||
|
|
||||||
setTracks(
|
|
||||||
tracks().map((track) => ({
|
|
||||||
...track,
|
|
||||||
isCurrent: getUpdatedStatus(track.index),
|
|
||||||
isPlaying: getUpdatedStatus(track.index)
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const playNextTrack = () => {
|
const playNextTrack = () => {
|
||||||
const { index } = getCurrentTrack()
|
let newCurrentTrackIndex = currentTrackIndex() + 1
|
||||||
const currIndex = tracks().findIndex((track) => track.index === index)
|
if (newCurrentTrackIndex > props.media.length - 1) {
|
||||||
|
newCurrentTrackIndex = props.media.length - 1
|
||||||
const getUpdatedStatus = (trackId) =>
|
|
||||||
currIndex === tracks().length - 1
|
|
||||||
? trackId === tracks()[0].index
|
|
||||||
: trackId === tracks()[currIndex + 1].index
|
|
||||||
|
|
||||||
setTracks(
|
|
||||||
tracks().map((track) => ({
|
|
||||||
...track,
|
|
||||||
isCurrent: getUpdatedStatus(track.index),
|
|
||||||
isPlaying: getUpdatedStatus(track.index)
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnAudioMetadataLoad = ({ target }) => {
|
setCurrentTrackIndex(newCurrentTrackIndex)
|
||||||
setDuration(target.duration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAudioDescriptionChange = (index: number, field: string, value) => {
|
const handleMediaItemFieldChange = (index: number, field: keyof MediaItem, value) => {
|
||||||
props.onAudioChange(index, field, value)
|
props.onMediaItemFieldChange(index, field, value)
|
||||||
setTracks(
|
}
|
||||||
tracks().map((track, idx) => {
|
|
||||||
return idx === index ? { ...track, [field]: value } : track
|
const scrub = (event) => {
|
||||||
})
|
audioRef.current.currentTime =
|
||||||
)
|
(event.offsetX / progressRef.current.offsetWidth) * currentTrackDuration()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Show when={getCurrentTrack()}>
|
<Show when={props.media}>
|
||||||
<PlayerHeader
|
<PlayerHeader
|
||||||
onPlayMedia={() => playMedia(getCurrentTrack())}
|
onPlayMedia={() => handlePlayMedia(currentTrackIndex())}
|
||||||
getCurrentTrack={getCurrentTrack}
|
|
||||||
playPrevTrack={playPrevTrack}
|
playPrevTrack={playPrevTrack}
|
||||||
playNextTrack={playNextTrack}
|
playNextTrack={playNextTrack}
|
||||||
gainNode={gainNode()}
|
onVolumeChange={handleVolumeChange}
|
||||||
|
isPlaying={isPlaying()}
|
||||||
|
currentTrack={currentTack()}
|
||||||
/>
|
/>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={getCurrentTrack()}>
|
|
||||||
<div class={styles.timeline}>
|
<div class={styles.timeline}>
|
||||||
<div
|
<div
|
||||||
class={styles.progress}
|
class={styles.progress}
|
||||||
ref={(el) => (progressRef.current = el)}
|
ref={(el) => (progressRef.current = el)}
|
||||||
onClick={(e) => scrub(e, progressRef, duration(), audioRef)}
|
onClick={(e) => scrub(e)}
|
||||||
onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)}
|
onMouseMove={(e) => mouseDownRef.current && scrub(e)}
|
||||||
onMouseDown={() => setMousedown(true)}
|
onMouseDown={() => (mouseDownRef.current = true)}
|
||||||
onMouseUp={() => setMousedown(false)}
|
onMouseUp={() => (mouseDownRef.current = false)}
|
||||||
>
|
>
|
||||||
<div class={styles.progressFilled} ref={(el) => (progressFilledRef.current = el)} />
|
<div
|
||||||
|
class={styles.progressFilled}
|
||||||
|
style={{
|
||||||
|
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.progressTiming}>
|
<div class={styles.progressTiming}>
|
||||||
<span>{currentTimeContent()}</span>
|
<span>{getFormattedTime(currentTime())}</span>
|
||||||
<span>{currentDurationContent()}</span>
|
<Show when={currentTrackDuration() > 0}>
|
||||||
|
<span>{getFormattedTime(currentTrackDuration())}</span>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<audio
|
<audio
|
||||||
ref={(el) => (audioRef.current = el)}
|
ref={(el) => (audioRef.current = el)}
|
||||||
onTimeUpdate={handleAudioTimeUpdate}
|
onTimeUpdate={handleAudioTimeUpdate}
|
||||||
|
// TEMP SOLUTION for http/https
|
||||||
|
src={currentTack().url.startsWith('https') ? currentTack().url : imageProxy(currentTack().url)}
|
||||||
onCanPlay={() => {
|
onCanPlay={() => {
|
||||||
if (getCurrentTrack().isPlaying) {
|
// start to play the next track on src change
|
||||||
|
if (isPlaying()) {
|
||||||
audioRef.current.play()
|
audioRef.current.play()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onLoadedMetadata={handleOnAudioMetadataLoad}
|
onLoadedMetadata={({ currentTarget }) => setCurrentTrackDuration(currentTarget.duration)}
|
||||||
onEnded={handleAudioEnd}
|
onEnded={handleAudioEnd}
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
<Show when={tracks()}>
|
|
||||||
<PlayerPlaylist
|
<PlayerPlaylist
|
||||||
editorMode={props.editorMode}
|
editorMode={props.editorMode}
|
||||||
playMedia={playMedia}
|
onPlayMedia={handlePlayMedia}
|
||||||
tracks={tracks()}
|
isPlaying={isPlaying()}
|
||||||
currentTrack={getCurrentTrack()}
|
media={props.media}
|
||||||
|
currentTrackIndex={currentTrackIndex()}
|
||||||
articleSlug={props.articleSlug}
|
articleSlug={props.articleSlug}
|
||||||
body={props.body}
|
body={props.body}
|
||||||
onAudioChange={handleAudioDescriptionChange}
|
onMediaItemFieldChange={handleMediaItemFieldChange}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,21 +5,24 @@ import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||||
|
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import styles from './AudioPlayer.module.scss'
|
import styles from './AudioPlayer.module.scss'
|
||||||
|
import { MediaItem } from '../../../pages/types'
|
||||||
|
|
||||||
export const PlayerHeader = (props) => {
|
type Props = {
|
||||||
let volumeRef: HTMLInputElement
|
onPlayMedia: () => void
|
||||||
|
playPrevTrack: () => void
|
||||||
|
playNextTrack: () => void
|
||||||
|
onVolumeChange: (volume: number) => void
|
||||||
|
isPlaying: boolean
|
||||||
|
currentTrack: MediaItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlayerHeader = (props: Props) => {
|
||||||
const volumeContainerRef: { current: HTMLDivElement } = {
|
const volumeContainerRef: { current: HTMLDivElement } = {
|
||||||
current: null
|
current: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getCurrentTrack, onPlayMedia, gainNode, playPrevTrack, playNextTrack } = props
|
|
||||||
|
|
||||||
const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false)
|
const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false)
|
||||||
|
|
||||||
const handleVolumeChange = () => {
|
|
||||||
gainNode.gain.value = Number(volumeRef.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleVolumeBar = () => {
|
const toggleVolumeBar = () => {
|
||||||
setIsVolumeBarOpened(!isVolumeBarOpened())
|
setIsVolumeBarOpened(!isVolumeBarOpened())
|
||||||
}
|
}
|
||||||
|
@ -32,23 +35,23 @@ export const PlayerHeader = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.playerHeader}>
|
<div class={styles.playerHeader}>
|
||||||
<div class={styles.playerTitle}>{getCurrentTrack().title}</div>
|
<div class={styles.playerTitle}>{props.currentTrack.title}</div>
|
||||||
<div class={styles.playerControls}>
|
<div class={styles.playerControls}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onPlayMedia}
|
onClick={props.onPlayMedia}
|
||||||
class={clsx(
|
class={clsx(
|
||||||
styles.playButton,
|
styles.playButton,
|
||||||
getCurrentTrack().isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay
|
props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay
|
||||||
)}
|
)}
|
||||||
aria-label="Play"
|
aria-label="Play"
|
||||||
data-playing="false"
|
data-playing="false"
|
||||||
>
|
>
|
||||||
<Icon name={getCurrentTrack().isPlaying ? 'pause' : 'play'} />
|
<Icon name={props.isPlaying ? 'pause' : 'play'} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={playPrevTrack}
|
onClick={props.playPrevTrack}
|
||||||
class={clsx(styles.controlsButton)}
|
class={clsx(styles.controlsButton)}
|
||||||
aria-label="Previous"
|
aria-label="Previous"
|
||||||
>
|
>
|
||||||
|
@ -56,7 +59,7 @@ export const PlayerHeader = (props) => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={playNextTrack}
|
onClick={props.playNextTrack}
|
||||||
class={clsx(styles.controlsButton, styles.controlsButtonNext)}
|
class={clsx(styles.controlsButton, styles.controlsButtonNext)}
|
||||||
aria-label="Next"
|
aria-label="Next"
|
||||||
>
|
>
|
||||||
|
@ -65,7 +68,6 @@ export const PlayerHeader = (props) => {
|
||||||
<div ref={(el) => (volumeContainerRef.current = el)} class={styles.volumeContainer}>
|
<div ref={(el) => (volumeContainerRef.current = el)} class={styles.volumeContainer}>
|
||||||
<Show when={isVolumeBarOpened()}>
|
<Show when={isVolumeBarOpened()}>
|
||||||
<input
|
<input
|
||||||
ref={volumeRef}
|
|
||||||
type="range"
|
type="range"
|
||||||
id="volume"
|
id="volume"
|
||||||
min="0"
|
min="0"
|
||||||
|
@ -73,7 +75,7 @@ export const PlayerHeader = (props) => {
|
||||||
value="1"
|
value="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
class={styles.volume}
|
class={styles.volume}
|
||||||
onChange={handleVolumeChange}
|
onChange={({ target }) => props.onVolumeChange(Number(target.value))}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<button onClick={toggleVolumeBar} class={styles.volumeButton} role="button" aria-label="Volume">
|
<button onClick={toggleVolumeBar} class={styles.volumeButton} role="button" aria-label="Volume">
|
||||||
|
|
|
@ -2,21 +2,22 @@ import { createSignal, For, Show } from 'solid-js'
|
||||||
import { SharePopup, getShareUrl } from '../SharePopup'
|
import { SharePopup, getShareUrl } from '../SharePopup'
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import type { Audio } from './AudioPlayer'
|
|
||||||
import { Popover } from '../../_shared/Popover'
|
import { Popover } from '../../_shared/Popover'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import styles from './AudioPlayer.module.scss'
|
import styles from './AudioPlayer.module.scss'
|
||||||
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||||
import MD from '../MD'
|
import MD from '../MD'
|
||||||
|
import { MediaItem } from '../../../pages/types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tracks: Audio[]
|
media: MediaItem[]
|
||||||
currentTrack: Audio
|
currentTrackIndex: number
|
||||||
playMedia: (audio: Audio) => void
|
isPlaying: boolean
|
||||||
|
onPlayMedia: (trackIndex: number) => void
|
||||||
articleSlug?: string
|
articleSlug?: string
|
||||||
body?: string
|
body?: string
|
||||||
editorMode?: boolean
|
editorMode?: boolean
|
||||||
onAudioChange?: (index: number, field: string, value: string) => void
|
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerPlaylist = (props: Props) => {
|
export const PlayerPlaylist = (props: Props) => {
|
||||||
|
@ -26,62 +27,52 @@ export const PlayerPlaylist = (props: Props) => {
|
||||||
const toggleDropDown = (index) => {
|
const toggleDropDown = (index) => {
|
||||||
setActiveEditIndex(activeEditIndex() === index ? -1 : index)
|
setActiveEditIndex(activeEditIndex() === index ? -1 : index)
|
||||||
}
|
}
|
||||||
const updateData = (key, value) => {
|
const handleMediaItemFieldChange = (field: keyof MediaItem, value: string) => {
|
||||||
props.onAudioChange(activeEditIndex(), key, value)
|
props.onMediaItemFieldChange(activeEditIndex(), field, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul class={styles.playlist}>
|
<ul class={styles.playlist}>
|
||||||
<For each={props.tracks}>
|
<For each={props.media}>
|
||||||
{(m: Audio, index) => (
|
{(mi, index) => (
|
||||||
<li>
|
<li>
|
||||||
<div class={styles.playlistItem}>
|
<div class={styles.playlistItem}>
|
||||||
<button
|
<button
|
||||||
class={styles.playlistItemPlayButton}
|
class={styles.playlistItemPlayButton}
|
||||||
onClick={() => props.playMedia(m)}
|
onClick={() => props.onPlayMedia(index())}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Play"
|
aria-label="Play"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon name={props.currentTrackIndex === index() && props.isPlaying ? 'pause' : 'play'} />
|
||||||
name={
|
|
||||||
props.currentTrack &&
|
|
||||||
props.currentTrack.index === m.index &&
|
|
||||||
props.currentTrack.isPlaying
|
|
||||||
? 'pause'
|
|
||||||
: 'play'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
<div class={styles.playlistItemText}>
|
<div class={styles.playlistItemText}>
|
||||||
<Show
|
<Show
|
||||||
when={activeEditIndex() === index() && props.editorMode}
|
when={activeEditIndex() === index() && props.editorMode}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<div class={styles.title}>
|
<div class={styles.title}>{mi.title || t('Song title')}</div>
|
||||||
{m.title.replace(/\.(wav|flac|mp3|aac)$/i, '') || t('Song title')}
|
<div class={styles.artist}>{mi.artist || t('Artist')}</div>
|
||||||
</div>
|
|
||||||
<div class={styles.artist}>{m.artist || t('Artist')}</div>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={m.title}
|
value={mi.title}
|
||||||
class={styles.title}
|
class={styles.title}
|
||||||
placeholder={t('Song title')}
|
placeholder={t('Song title')}
|
||||||
onChange={(e) => updateData('title', e.target.value)}
|
onChange={(e) => handleMediaItemFieldChange('title', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={m.artist}
|
value={mi.artist}
|
||||||
class={styles.artist}
|
class={styles.artist}
|
||||||
placeholder={t('Artist')}
|
placeholder={t('Artist')}
|
||||||
onChange={(e) => updateData('artist', e.target.value)}
|
onChange={(e) => handleMediaItemFieldChange('artist', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<Show when={(m.lyrics || m.body) && !props.editorMode}>
|
<Show when={(mi.lyrics || mi.body) && !props.editorMode}>
|
||||||
<Popover content={t('Show lyrics')}>
|
<Popover content={t('Show lyrics')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<button ref={triggerRef} type="button" onClick={() => toggleDropDown(index())}>
|
<button ref={triggerRef} type="button" onClick={() => toggleDropDown(index())}>
|
||||||
|
@ -102,9 +93,9 @@ export const PlayerPlaylist = (props: Props) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SharePopup
|
<SharePopup
|
||||||
title={m.title}
|
title={mi.title}
|
||||||
description={getDescription(props.body)}
|
description={getDescription(props.body)}
|
||||||
imageUrl={m.pic}
|
imageUrl={mi.pic}
|
||||||
shareUrl={getShareUrl({ pathname: `/${props.articleSlug}` })}
|
shareUrl={getShareUrl({ pathname: `/${props.articleSlug}` })}
|
||||||
trigger={
|
trigger={
|
||||||
<div>
|
<div>
|
||||||
|
@ -123,14 +114,15 @@ export const PlayerPlaylist = (props: Props) => {
|
||||||
when={props.editorMode}
|
when={props.editorMode}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.descriptionBlock}>
|
<div class={styles.descriptionBlock}>
|
||||||
<Show when={m.body}>
|
<Show when={mi.body}>
|
||||||
<div class={styles.description}>
|
<div class={styles.description}>
|
||||||
<MD body={m.body} />
|
{/*FIXME*/}
|
||||||
|
<MD body={mi.body} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={m.lyrics}>
|
<Show when={mi.lyrics}>
|
||||||
<div class={styles.lyrics}>
|
<div class={styles.lyrics}>
|
||||||
<MD body={m.lyrics} />
|
<MD body={mi.lyrics} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
@ -141,15 +133,15 @@ export const PlayerPlaylist = (props: Props) => {
|
||||||
allowEnterKey={true}
|
allowEnterKey={true}
|
||||||
class={styles.description}
|
class={styles.description}
|
||||||
placeholder={t('Description')}
|
placeholder={t('Description')}
|
||||||
value={(value) => updateData('body', value)}
|
value={(value) => handleMediaItemFieldChange('body', value)}
|
||||||
initialValue={m.body || ''}
|
initialValue={mi.body || ''}
|
||||||
/>
|
/>
|
||||||
<GrowingTextarea
|
<GrowingTextarea
|
||||||
allowEnterKey={true}
|
allowEnterKey={true}
|
||||||
class={styles.lyrics}
|
class={styles.lyrics}
|
||||||
placeholder={t('Song lyrics')}
|
placeholder={t('Song lyrics')}
|
||||||
value={(value) => updateData('lyrics', value)}
|
value={(value) => handleMediaItemFieldChange('lyrics', value)}
|
||||||
initialValue={m.lyrics || ''}
|
initialValue={mi.lyrics || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -189,6 +189,13 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={media().length > 0 && props.article.layout === 'audio'}>
|
||||||
|
<div class="media-items">
|
||||||
|
<AudioPlayer media={media()} articleSlug={props.article.slug} body={body()} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={body()}>
|
<Show when={body()}>
|
||||||
<div class={styles.shoutBody}>
|
<div class={styles.shoutBody}>
|
||||||
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>
|
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
.rating {
|
.rating {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.ratingControl {
|
|
||||||
&:hover {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratingValue {
|
.ratingValue {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import styles from './ShoutRatingControl.module.scss'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo, Show } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
import { ReactionKind, Shout } from '../../graphql/types.gen'
|
import { ReactionKind, Shout } from '../../graphql/types.gen'
|
||||||
|
@ -9,6 +8,7 @@ import { Popup } from '../_shared/Popup'
|
||||||
import { VotersList } from '../_shared/VotersList'
|
import { VotersList } from '../_shared/VotersList'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
import styles from './ShoutRatingControl.module.scss'
|
||||||
|
|
||||||
interface ShoutRatingControlProps {
|
interface ShoutRatingControlProps {
|
||||||
shout: Shout
|
shout: Shout
|
||||||
|
@ -82,7 +82,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.rating, props.class)}>
|
<div class={clsx(styles.rating, props.class)}>
|
||||||
<button class={styles.ratingControl} onClick={() => handleRatingChange(false)}>
|
<button onClick={() => handleRatingChange(false)}>
|
||||||
<Show when={!isDownvoted()}>
|
<Show when={!isDownvoted()}>
|
||||||
<Icon name="rating-control-less" />
|
<Icon name="rating-control-less" />
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -98,7 +98,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
/>
|
/>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
<button class={styles.ratingControl} onClick={() => handleRatingChange(true)}>
|
<button onClick={() => handleRatingChange(true)}>
|
||||||
<Show when={!isUpvoted()}>
|
<Show when={!isUpvoted()}>
|
||||||
<Icon name="rating-control-more" />
|
<Icon name="rating-control-more" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -25,14 +25,18 @@ type Props = {
|
||||||
export const AudioUploader = (props: Props) => {
|
export const AudioUploader = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const handleAudioDescriptionChange = (index: number, field: string, value) => {
|
const handleMediaItemFieldChange = (index: number, field: keyof MediaItem, value) => {
|
||||||
props.onAudioChange(index, { ...props.audio[index], [field]: value })
|
props.onAudioChange(index, { ...props.audio[index], [field]: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.AudioUploader, props.class)}>
|
<div class={clsx(styles.AudioUploader, props.class)}>
|
||||||
<Show when={props.audio.length > 0}>
|
<Show when={props.audio.length > 0}>
|
||||||
<AudioPlayer editorMode={true} media={props.audio} onAudioChange={handleAudioDescriptionChange} />
|
<AudioPlayer
|
||||||
|
editorMode={true}
|
||||||
|
media={props.audio}
|
||||||
|
onMediaItemFieldChange={handleMediaItemFieldChange}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<DropArea
|
<DropArea
|
||||||
isMultiply={true}
|
isMultiply={true}
|
||||||
|
|
|
@ -25,31 +25,24 @@ export type RootSearchParams = {
|
||||||
lang: string
|
lang: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UploadFile = {
|
|
||||||
source: string
|
|
||||||
name: string
|
|
||||||
size: number
|
|
||||||
file: File
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'
|
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'
|
||||||
|
|
||||||
export type FileTypeToUpload = 'image' | 'video' | 'doc' | 'audio'
|
export type FileTypeToUpload = 'image' | 'video' | 'doc' | 'audio'
|
||||||
|
|
||||||
export type AudioDescription = {
|
export type MediaItem = {
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
source?: string // for image
|
||||||
|
pic?: string
|
||||||
|
|
||||||
|
// audio specific properties
|
||||||
date?: string
|
date?: string
|
||||||
genre?: string
|
genre?: string
|
||||||
artist?: string
|
artist?: string
|
||||||
lyrics?: string
|
lyrics?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MediaItem = {
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
body: string
|
|
||||||
source?: string
|
|
||||||
} & AudioDescription
|
|
||||||
|
|
||||||
export type UploadedFile = {
|
export type UploadedFile = {
|
||||||
url: string
|
url: string
|
||||||
originalFilename: string
|
originalFilename: string
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { UploadedFile } from '../pages/types'
|
import { UploadedFile } from '../pages/types'
|
||||||
|
|
||||||
|
const removeFileExtension = (fileName: string) => {
|
||||||
|
return fileName.replace(/\.(wav|flac|mp3|aac|jpg|jpeg|png|gif)$/i, '')
|
||||||
|
}
|
||||||
|
|
||||||
export const composeMediaItems = (value: UploadedFile[], optionalParams = {}) => {
|
export const composeMediaItems = (value: UploadedFile[], optionalParams = {}) => {
|
||||||
return value.map((fileData) => {
|
return value.map((fileData) => {
|
||||||
return {
|
return {
|
||||||
url: fileData.url,
|
url: fileData.url,
|
||||||
source: '',
|
source: '',
|
||||||
title: fileData.originalFilename,
|
title: removeFileExtension(fileData.originalFilename),
|
||||||
body: '',
|
body: '',
|
||||||
...optionalParams
|
...optionalParams
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user