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