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:
Ilya Y 2023-07-18 14:26:32 +03:00 committed by GitHub
parent e66eeb48df
commit be9fea1265
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 211 additions and 297 deletions

View File

@ -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;

View File

@ -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>
) )

View File

@ -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;
} }
} }

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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()} />}>

View File

@ -1,11 +1,6 @@
.rating { .rating {
align-items: center; align-items: center;
display: flex; display: flex;
.ratingControl {
&:hover {
}
}
} }
.ratingValue { .ratingValue {

View File

@ -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>

View File

@ -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}

View File

@ -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

View File

@ -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
} }