fix: Added close button for audio player

This commit is contained in:
florian 2024-05-25 14:29:37 +02:00
parent 03134c37d6
commit 895dda9258
7 changed files with 65 additions and 31 deletions

View File

@ -16,7 +16,11 @@ const initialState: State = {
songs: [], songs: [],
}; };
type Action = { type: 'SET_CURRENT_SONG'; song: Song } | { type: 'SHUFFLE_SONGS' } | { type: 'ADD_SONG'; song: Song }; type Action =
| { type: 'SET_CURRENT_SONG'; song: Song }
| { type: 'SHUFFLE_SONGS' }
| { type: 'RESET_CURRENT_SONG' }
| { type: 'ADD_SONG'; song: Song };
const reducer = (state: State, action: Action): State => { const reducer = (state: State, action: Action): State => {
switch (action.type) { switch (action.type) {
@ -26,6 +30,8 @@ const reducer = (state: State, action: Action): State => {
return { ...state, songs: [...state.songs].sort(() => Math.random() - 0.5) }; return { ...state, songs: [...state.songs].sort(() => Math.random() - 0.5) };
case 'ADD_SONG': case 'ADD_SONG':
return { ...state, songs: [...state.songs, action.song] }; return { ...state, songs: [...state.songs, action.song] };
case 'RESET_CURRENT_SONG':
return { ...state, currentSong: undefined };
default: default:
return state; return state;
} }

View File

@ -1,15 +1,13 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { useGlobalContext } from '../GlobalState'; import { useGlobalContext } from '../GlobalState';
import { PauseIcon, PlayIcon, SpeakerWaveIcon, SpeakerXMarkIcon } from '@heroicons/react/24/outline'; import { PauseIcon, PlayIcon, SpeakerWaveIcon, SpeakerXMarkIcon, XMarkIcon } from '@heroicons/react/24/outline';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'; import duration from 'dayjs/plugin/duration';
dayjs.extend(duration); dayjs.extend(duration);
const AudioPlayer: React.FC = () => { const AudioPlayer: React.FC = () => {
const { state } = useGlobalContext(); const { state, dispatch } = useGlobalContext();
const { currentSong } = state; const { currentSong } = state;
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(1); const [volume, setVolume] = useState(1);
@ -25,9 +23,19 @@ const AudioPlayer: React.FC = () => {
setProgress(((audio.currentTime || 0) / audio.duration) * 100); setProgress(((audio.currentTime || 0) / audio.duration) * 100);
} }
}; };
audio.addEventListener('timeupdate', updateProgress);
const updateProgressFrame = () => {
updateProgress();
requestAnimationFrame(updateProgressFrame);
};
audio.addEventListener('play', updateProgressFrame);
audio.addEventListener('pause', updateProgressFrame);
requestAnimationFrame(updateProgressFrame);
return () => { return () => {
audio.removeEventListener('timeupdate', updateProgress); audio.removeEventListener('play', updateProgressFrame);
audio.removeEventListener('pause', updateProgressFrame);
}; };
} }
}, [currentSong]); }, [currentSong]);
@ -61,36 +69,44 @@ const AudioPlayer: React.FC = () => {
tuneVolume(newVolume); tuneVolume(newVolume);
}; };
const resetPlayer = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
setIsPlaying(false);
setProgress(0);
dispatch({ type: 'RESET_CURRENT_SONG' }); // Assuming this resets the current song in global state
};
return ( return (
currentSong && ( currentSong && (
<div className="audio-player flex items-center space-x-4"> <div className="audio-player flex items-center space-x-4 w-full">
<audio ref={audioRef} /> <audio ref={audioRef} />
{/*currentSong && <span className="font-semibold">{currentSong}</span>
*/}
<button className="btn btn-icon" onClick={playPause}> <button className="btn btn-icon" onClick={playPause}>
{isPlaying ? <PauseIcon className="h-6 w-6" /> : <PlayIcon className="h-6 w-6" />} {isPlaying ? <PauseIcon className="h-6 w-6" /> : <PlayIcon className="h-6 w-6" />}
</button> </button>
<span className="w-10 hidden md:block"> <span className="w-10 hidden md:block">
{' '}
{dayjs.duration(audioRef.current?.currentTime || 0, 'seconds').format('m:ss')} {dayjs.duration(audioRef.current?.currentTime || 0, 'seconds').format('m:ss')}
</span> </span>
<div className="flex-grow w-60 hidden md:block cursor-pointer"> <div className="flex-grow w-60 hidden md:block">
<input <input
type="range" type="range"
min="0" min="0"
max="100" max="100"
value={progress} value={progress}
onChange={e => onChange={e => {
audioRef.current && if (audioRef.current) {
(audioRef.current.currentTime = (parseInt(e.target.value) / 100) * audioRef.current.duration) audioRef.current.currentTime = (parseInt(e.target.value) / 100) * audioRef.current.duration;
} }
className="progress progress-primary w-full" }}
className="progress progress-primary w-full cursor-pointer"
/> />
</div> </div>
<div className="flex items-center space-x-2 cursor-pointer"> <div className="flex items-center space-x-2" >
{volume == 0 ? ( {volume === 0 ? (
<SpeakerXMarkIcon <SpeakerXMarkIcon
className="h-6 w-6 text-gray-500" className="h-6 w-6 text-gray-500"
onClick={() => { onClick={() => {
@ -114,20 +130,25 @@ const AudioPlayer: React.FC = () => {
step="0.01" step="0.01"
value={volume} value={volume}
onChange={changeVolume} onChange={changeVolume}
className="progress progress-primary" className="progress progress-primary cursor-pointer"
/> />
</div> </div>
{currentSong.id3 && ( {currentSong.id3 && (
<> <>
<div> <div>
<img className="w-12 h-12" src={currentSong.id3?.cover}></img> <img className="w-12 h-12" src={currentSong.id3.cover} alt={currentSong.id3.title} />
</div> </div>
<div className="flex flex-col text-sm"> <div className="flex flex-col text-sm">
<div className="text-white">{currentSong?.id3.title}</div> <div className="text-white">{currentSong.id3.title}</div>
<div>{currentSong?.id3.artist}</div> <div>{currentSong.id3.artist}</div>
</div> </div>
</> </>
)} )}
<button className="btn btn-icon ml-auto" onClick={resetPlayer}>
<XMarkIcon className="h-6 w-6 text-gray-500" />
</button>
</div> </div>
) )
); );

View File

@ -6,10 +6,12 @@ import { useEffect } from 'react';
import ThemeSwitcher from '../ThemeSwitcher'; import ThemeSwitcher from '../ThemeSwitcher';
import AudioPlayer from '../AudioPlayer'; import AudioPlayer from '../AudioPlayer';
import BottomNavbar from '../BottomNavBar/BottomNavBar'; import BottomNavbar from '../BottomNavBar/BottomNavBar';
import { useGlobalContext } from '../../GlobalState';
export const Layout = () => { export const Layout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { loginWithExtension, user } = useNDK(); const { loginWithExtension, user } = useNDK();
const { state } = useGlobalContext();
useEffect(() => { useEffect(() => {
if (!user) loginWithExtension(); if (!user) loginWithExtension();
@ -69,9 +71,11 @@ export const Layout = () => {
</div> </div>
<div className="content">{<Outlet />}</div> <div className="content">{<Outlet />}</div>
{state.currentSong && (
<BottomNavbar> <BottomNavbar>
<AudioPlayer /> <AudioPlayer />
</BottomNavbar> </BottomNavbar>
)}
<div className="footer"> <div className="footer">
<span className="whitespace-nowrap block"> <span className="whitespace-nowrap block">
made with 💜 by{' '} made with 💜 by{' '}

View File

@ -8,6 +8,7 @@ import { useMemo, useState } from 'react';
import { useNDK } from '../../utils/ndk'; import { useNDK } from '../../utils/ndk';
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { USER_BLOSSOM_SERVER_LIST_KIND } from 'blossom-client-sdk';
type ServerListProps = { type ServerListProps = {
servers: ServerInfo[]; servers: ServerInfo[];
@ -49,7 +50,7 @@ export const ServerList = ({
const handleSaveServers = async (newServers: ServerType[]) => { const handleSaveServers = async (newServers: ServerType[]) => {
const ev = new NDKEvent(ndk, { const ev = new NDKEvent(ndk, {
kind: 10063, kind: USER_BLOSSOM_SERVER_LIST_KIND,
created_at: dayjs().unix(), created_at: dayjs().unix(),
content: '', content: '',
pubkey: user?.pubkey || '', pubkey: user?.pubkey || '',

View File

@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk'; import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../utils/ndk'; import { useNDK } from '../utils/ndk';
import BlobList from '../components/BlobList/BlobList'; import BlobList from '../components/BlobList/BlobList';
import { ServerInfo, useServerInfo } from '../utils/useServerInfo'; import { useServerInfo } from '../utils/useServerInfo';
import { ServerList } from '../components/ServerList/ServerList'; import { ServerList } from '../components/ServerList/ServerList';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';

View File

@ -4,8 +4,9 @@ import { NDKKind } from '@nostr-dev-kit/ndk';
import countBy from 'lodash/countBy'; import countBy from 'lodash/countBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import toPairs from 'lodash/toPairs'; import toPairs from 'lodash/toPairs';
import { USER_BLOSSOM_SERVER_LIST_KIND } from 'blossom-client-sdk';
const blossomServerListFilter = { kinds: [10063 as NDKKind] }; const blossomServerListFilter = { kinds: [USER_BLOSSOM_SERVER_LIST_KIND as NDKKind] };
const useBlossomServerEvents = () => { const useBlossomServerEvents = () => {
const blossomServerEvents = useEvents(blossomServerListFilter); const blossomServerEvents = useEvents(blossomServerListFilter);

View File

@ -3,6 +3,7 @@ import { useNDK } from '../utils/ndk';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { NDKKind } from '@nostr-dev-kit/ndk'; import { NDKKind } from '@nostr-dev-kit/ndk';
import useEvent from '../utils/useEvent'; import useEvent from '../utils/useEvent';
import { USER_BLOSSOM_SERVER_LIST_KIND } from 'blossom-client-sdk';
export type Server = { export type Server = {
name: string; name: string;
@ -14,7 +15,7 @@ export const useUserServers = (): Server[] => {
const pubkey = user?.npub && (nip19.decode(user?.npub).data as string); // TODO validate type const pubkey = user?.npub && (nip19.decode(user?.npub).data as string); // TODO validate type
const serverListEvent = useEvent({ kinds: [10063 as NDKKind], authors: [pubkey!] }, { disable: !pubkey }); const serverListEvent = useEvent({ kinds: [USER_BLOSSOM_SERVER_LIST_KIND as NDKKind], authors: [pubkey!] }, { disable: !pubkey });
const servers = useMemo(() => { const servers = useMemo(() => {
const serverUrls = (serverListEvent?.getMatchingTags('server').map(t => t[1]) || []).map(s => const serverUrls = (serverListEvent?.getMatchingTags('server').map(t => t[1]) || []).map(s =>