diff --git a/src/components/Article/AudioHeader/AudioHeader.module.scss b/src/components/Article/AudioHeader/AudioHeader.module.scss index c3739655..d27a7c60 100644 --- a/src/components/Article/AudioHeader/AudioHeader.module.scss +++ b/src/components/Article/AudioHeader/AudioHeader.module.scss @@ -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; diff --git a/src/components/Article/AudioHeader/AudioHeader.tsx b/src/components/Article/AudioHeader/AudioHeader.tsx index 548c0ef4..4569caf3 100644 --- a/src/components/Article/AudioHeader/AudioHeader.tsx +++ b/src/components/Article/AudioHeader/AudioHeader.tsx @@ -36,17 +36,19 @@ export const AudioHeader = (props: Props) => {

{props.title}

-
- -
{props.artistData.artist}
-
- -
{props.artistData.date}
-
- -
{props.artistData.genre}
-
-
+ +
+ +
{props.artistData.artist}
+
+ +
{props.artistData.date}
+
+ +
{props.artistData.genre}
+
+
+
) diff --git a/src/components/Article/AudioPlayer/AudioPlayer.module.scss b/src/components/Article/AudioPlayer/AudioPlayer.module.scss index 84b5671c..775b72b8 100644 --- a/src/components/Article/AudioPlayer/AudioPlayer.module.scss +++ b/src/components/Article/AudioPlayer/AudioPlayer.module.scss @@ -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; } } diff --git a/src/components/Article/AudioPlayer/AudioPlayer.tsx b/src/components/Article/AudioPlayer/AudioPlayer.tsx index 8f0951c5..10ff45b3 100644 --- a/src/components/Article/AudioPlayer/AudioPlayer.tsx +++ b/src/components/Article/AudioPlayer/AudioPlayer.tsx @@ -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() - const [gainNode, setGainNode] = createSignal() - const [tracks, setTracks] = createSignal(prepareMedia(props.media)) - const [duration, setDuration] = createSignal(0) - const [currentTimeContent, setCurrentTimeContent] = createSignal('00:00') - const [currentDurationContent, setCurrentDurationContent] = createSignal('00:00') - const [mousedown, setMousedown] = createSignal(false) + const [currentTrackDuration, setCurrentTrackDuration] = createSignal(0) + const [currentTime, setCurrentTime] = createSignal(0) + const [currentTrackIndex, setCurrentTrackIndex] = createSignal(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 (
- + playMedia(getCurrentTrack())} - getCurrentTrack={getCurrentTrack} + onPlayMedia={() => handlePlayMedia(currentTrackIndex())} playPrevTrack={playPrevTrack} playNextTrack={playNextTrack} - gainNode={gainNode()} + onVolumeChange={handleVolumeChange} + isPlaying={isPlaying()} + currentTrack={currentTack()} /> - - -
(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)} > -
(progressFilledRef.current = el)} /> +
- {currentTimeContent()} - {currentDurationContent()} + {getFormattedTime(currentTime())} + 0}> + {getFormattedTime(currentTrackDuration())} +
- -
diff --git a/src/components/Article/AudioPlayer/PlayerHeader.tsx b/src/components/Article/AudioPlayer/PlayerHeader.tsx index 04aa50e7..ff5019ed 100644 --- a/src/components/Article/AudioPlayer/PlayerHeader.tsx +++ b/src/components/Article/AudioPlayer/PlayerHeader.tsx @@ -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 (
-
{getCurrentTrack().title}
+
{props.currentTrack.title}
-
- {m.title.replace(/\.(wav|flac|mp3|aac)$/i, '') || t('Song title')} -
-
{m.artist || t('Artist')}
+
{mi.title || t('Song title')}
+
{mi.artist || t('Artist')}
} > updateData('title', e.target.value)} + onChange={(e) => handleMediaItemFieldChange('title', e.target.value)} /> updateData('artist', e.target.value)} + onChange={(e) => handleMediaItemFieldChange('artist', e.target.value)} />
- + {(triggerRef: (el) => void) => (
diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index e971c197..a74c9484 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -189,6 +189,13 @@ export const FullArticle = (props: ArticleProps) => {
+ + 0 && props.article.layout === 'audio'}> +
+ +
+
+
}> diff --git a/src/components/Article/ShoutRatingControl.module.scss b/src/components/Article/ShoutRatingControl.module.scss index 16b55d89..642204e6 100644 --- a/src/components/Article/ShoutRatingControl.module.scss +++ b/src/components/Article/ShoutRatingControl.module.scss @@ -1,11 +1,6 @@ .rating { align-items: center; display: flex; - - .ratingControl { - &:hover { - } - } } .ratingValue { diff --git a/src/components/Article/ShoutRatingControl.tsx b/src/components/Article/ShoutRatingControl.tsx index f4bd2b9e..c7b4c6f9 100644 --- a/src/components/Article/ShoutRatingControl.tsx +++ b/src/components/Article/ShoutRatingControl.tsx @@ -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 (
-