From fbde04cc5e07932fc9df0528832b351fb692dc60 Mon Sep 17 00:00:00 2001 From: florian <> Date: Sun, 19 May 2024 23:33:14 +0200 Subject: [PATCH] feat: Added audio player --- public/music-placeholder.png | Bin 0 -> 25210 bytes src/GlobalState.tsx | 58 +++++++ src/components/AudioPlayer.tsx | 136 ++++++++++++++++ src/components/BlobList/BlobList.css | 13 ++ src/components/BlobList/BlobList.tsx | 57 +++---- src/components/BottomNavBar/BottomNavBar.tsx | 17 ++ src/components/Layout/Layout.tsx | 7 + src/main.tsx | 5 +- src/utils/id3.ts | 155 +++++++++++++++++++ src/utils/useFileMetaEvents.ts | 2 +- 10 files changed, 415 insertions(+), 35 deletions(-) create mode 100644 public/music-placeholder.png create mode 100644 src/GlobalState.tsx create mode 100644 src/components/AudioPlayer.tsx create mode 100644 src/components/BottomNavBar/BottomNavBar.tsx create mode 100644 src/utils/id3.ts diff --git a/public/music-placeholder.png b/public/music-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..da5135b700e52c492e7512a14ec1aeec027a8ed9 GIT binary patch literal 25210 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 literal 0 HcmV?d00001 diff --git a/src/GlobalState.tsx b/src/GlobalState.tsx new file mode 100644 index 0000000..c63b218 --- /dev/null +++ b/src/GlobalState.tsx @@ -0,0 +1,58 @@ +import React, { createContext, useReducer, useContext, ReactNode } from 'react'; +import { ID3Tag } from './utils/id3'; + + +type Song = { + url: string; + id3?: ID3Tag; +} + +interface State { + currentSong?: Song; + songs: Song[]; +} + +const initialState: State = { + currentSong: undefined, + songs: [], +}; + +type Action = + | { type: 'SET_CURRENT_SONG'; song: Song } + | { type: 'SHUFFLE_SONGS' } + | { type: 'ADD_SONG'; song: Song }; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'SET_CURRENT_SONG': + return { ...state, currentSong: action.song }; + case 'SHUFFLE_SONGS': + return { ...state, songs: [...state.songs].sort(() => Math.random() - 0.5) }; + case 'ADD_SONG': + return { ...state, songs: [...state.songs, action.song] }; + default: + return state; + } +}; + +const GlobalContext = createContext<{ state: State; dispatch: React.Dispatch } | undefined>(undefined); + +interface GlobalProviderProps { + children: ReactNode; +} + +const GlobalProvider: React.FC = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + + return {children}; +}; + +const useGlobalContext = () => { + const context = useContext(GlobalContext); + if (context === undefined) { + throw new Error('useGlobalContext must be used within a GlobalProvider'); + } + return context; +}; + +export { GlobalProvider, useGlobalContext }; diff --git a/src/components/AudioPlayer.tsx b/src/components/AudioPlayer.tsx new file mode 100644 index 0000000..3f28d33 --- /dev/null +++ b/src/components/AudioPlayer.tsx @@ -0,0 +1,136 @@ +import React, { useState, useRef, useEffect } from 'react'; + +import { useGlobalContext } from '../GlobalState'; +import { PauseIcon, PlayIcon, SpeakerWaveIcon, SpeakerXMarkIcon } from '@heroicons/react/24/outline'; + +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; + +dayjs.extend(duration); + +const AudioPlayer: React.FC = () => { + const { state } = useGlobalContext(); + const { currentSong } = state; + const [isPlaying, setIsPlaying] = useState(false); + const [volume, setVolume] = useState(1); + const [volumeBeforeMute, setVolumeBeforeMute] = useState(1); + const [progress, setProgress] = useState(0); + const audioRef = useRef(null); + + useEffect(() => { + const audio = audioRef.current; + if (audio) { + const updateProgress = () => { + if (audio.duration && !isNaN(audio.currentTime)) { + setProgress(((audio.currentTime || 0) / audio.duration) * 100); + } + }; + audio.addEventListener('timeupdate', updateProgress); + return () => { + audio.removeEventListener('timeupdate', updateProgress); + }; + } + }, [currentSong]); + + useEffect(() => { + if (audioRef.current && currentSong) { + audioRef.current.src = currentSong.url; + audioRef.current.play(); + setIsPlaying(true); + } + }, [currentSong]); + + const playPause = () => { + if (isPlaying) { + audioRef.current?.pause(); + } else { + audioRef.current?.play(); + } + setIsPlaying(!isPlaying); + }; + + const tuneVolume = (newVolume: number) => { + setVolume(newVolume); + if (audioRef.current) { + audioRef.current.volume = newVolume; + } + }; + + const changeVolume = (event: React.ChangeEvent) => { + const newVolume = parseFloat(event.target.value); + tuneVolume(newVolume); + }; + + return ( + currentSong && ( +
+
+ ) + ); +}; + +export default AudioPlayer; diff --git a/src/components/BlobList/BlobList.css b/src/components/BlobList/BlobList.css index 6d21afa..a77d850 100644 --- a/src/components/BlobList/BlobList.css +++ b/src/components/BlobList/BlobList.css @@ -17,3 +17,16 @@ .blog-list-header svg { @apply w-6 opacity-80 hover:opacity-100; } + +.blob-list .cover-image { + @apply min-h-[96px] min-w-[96px]; +} + +.blob-list .cover-image:hover .play-icon { + @apply opacity-100; +} + +.blob-list .cover-image .play-icon { + @apply opacity-0 absolute text-white top-8 left-8 w-16 h-16 rounded-full bg-[rgba(0,0,0,.4)] p-2 cursor-pointer; +} + diff --git a/src/components/BlobList/BlobList.tsx b/src/components/BlobList/BlobList.tsx index 9c12f0d..898c588 100644 --- a/src/components/BlobList/BlobList.tsx +++ b/src/components/BlobList/BlobList.tsx @@ -6,6 +6,7 @@ import { ListBulletIcon, MusicalNoteIcon, PhotoIcon, + PlayIcon, TrashIcon, } from '@heroicons/react/24/outline'; import { BlobDescriptor } from 'blossom-client-sdk'; @@ -13,14 +14,14 @@ import { formatDate, formatFileSize } from '../../utils/utils'; import './BlobList.css'; import { useEffect, useMemo, useState } from 'react'; import { Document, Page } from 'react-pdf'; -import * as id3 from 'id3js'; -import { ID3Tag, ID3TagV2 } from 'id3js/lib/id3Tag'; import { useQueries } from '@tanstack/react-query'; import { useServerInfo } from '../../utils/useServerInfo'; import useFileMetaEventsByHash, { KIND_BLOSSOM_DRIVE, KIND_FILE_META } from '../../utils/useFileMetaEvents'; import { nip19 } from 'nostr-tools'; import { AddressPointer, EventPointer } from 'nostr-tools/nip19'; import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { useGlobalContext } from '../../GlobalState'; +import { fetchId3Tag } from '../../utils/id3'; type ListMode = 'gallery' | 'list' | 'audio' | 'video' | 'docs'; @@ -31,12 +32,11 @@ type BlobListProps = { className?: string; }; -type AudioBlob = BlobDescriptor & { id3?: ID3Tag; imageData?: string }; - const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => { const [mode, setMode] = useState('list'); const { distribution } = useServerInfo(); const fileMetaEventsByHash = useFileMetaEventsByHash(); + const { dispatch } = useGlobalContext(); const images = useMemo( () => blobs.filter(b => b.type?.startsWith('image/')).sort((a, b) => (a.uploaded > b.uploaded ? -1 : 1)), // descending @@ -48,29 +48,11 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => [blobs] ); - const fetchId3Tag = async (blob: BlobDescriptor) => { - const id3Tag = await id3.fromUrl(blob.url).catch(e => console.warn(e)); - - if (id3Tag && id3Tag.kind == 'v2') { - const id3v2 = id3Tag as ID3TagV2; - if (id3v2.images[0].data) { - const base64data = btoa( - new Uint8Array(id3v2.images[0].data).reduce(function (data, byte) { - return data + String.fromCharCode(byte); - }, '') - ); - const imageData = `data:${id3v2.images[0].type};base64,${base64data}`; - return { ...blob, id3: id3Tag, imageData } as AudioBlob; - } - } - return { ...blob, id3: id3Tag } as AudioBlob; - }; - const audioFiles = useMemo( () => blobs.filter(b => b.type?.startsWith('audio/')).sort((a, b) => (a.uploaded > b.uploaded ? -1 : 1)), [blobs] ); - +console.log(audioFiles); const audioFilesWithId3 = useQueries({ queries: audioFiles.map(af => ({ queryKey: ['id3', af.sha256], @@ -263,7 +245,7 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => )} {mode == 'audio' && ( -
+
{audioFilesWithId3.map( blob => blob.isSuccess && ( @@ -272,10 +254,21 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => className="p-4 rounded-lg bg-base-300 m-2 relative flex flex-col" style={{ width: '24em' }} > - {blob.data.id3 && ( -
- {blob.data.imageData && } - +
+
+ dispatch({ type: 'SET_CURRENT_SONG', song: {url: blob.data.url, id3: blob.data.id3 }})} + /> + dispatch({ type: 'SET_CURRENT_SONG', song: {url: blob.data.url, id3: blob.data.id3 }})} + > +
+ {blob.data.id3 && (
{blob.data.id3.title && {blob.data.id3.title}} {blob.data.id3.artist && {blob.data.id3.artist}} @@ -285,12 +278,10 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => )}
-
- )} + )} +
- - -
+
{formatFileSize(blob.data.size)} {formatDate(blob.data.uploaded)}
diff --git a/src/components/BottomNavBar/BottomNavBar.tsx b/src/components/BottomNavBar/BottomNavBar.tsx new file mode 100644 index 0000000..cc7fa91 --- /dev/null +++ b/src/components/BottomNavBar/BottomNavBar.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; + +interface BottomNavbarProps { + children: ReactNode; +} + +const BottomNavbar: React.FC = ({ children }) => { + return ( +
+
+ {children} +
+
+ ); +}; + +export default BottomNavbar; \ No newline at end of file diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 30df028..26996e0 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -4,6 +4,8 @@ import './Layout.css'; import { ArrowUpOnSquareIcon, MagnifyingGlassIcon, ServerStackIcon } from '@heroicons/react/24/outline'; import { useEffect } from 'react'; import ThemeSwitcher from '../ThemeSwitcher'; +import AudioPlayer from '../AudioPlayer'; +import BottomNavbar from '../BottomNavBar/BottomNavBar'; export const Layout = () => { const navigate = useNavigate(); @@ -57,6 +59,7 @@ export const Layout = () => {
{navItems}
+
@@ -67,6 +70,10 @@ export const Layout = () => {
{}
+ + + +
made with 💜 by{' '} diff --git a/src/main.tsx b/src/main.tsx index a766f6b..6f89123 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,7 @@ import Upload from './pages/Upload.tsx'; import Check from './pages/Check.tsx'; import { pdfjs } from 'react-pdf'; +import { GlobalProvider } from './GlobalState.tsx'; pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString(); @@ -75,7 +76,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render( >*/} - + + + diff --git a/src/utils/id3.ts b/src/utils/id3.ts new file mode 100644 index 0000000..d56f442 --- /dev/null +++ b/src/utils/id3.ts @@ -0,0 +1,155 @@ +import * as id3 from 'id3js'; +import { BlobDescriptor } from 'blossom-client-sdk'; +import { ID3TagV2 } from 'id3js/lib/id3Tag'; + +export type AudioBlob = BlobDescriptor & { id3?: ID3Tag }; + +export interface ID3Tag { + artist?: string; + album?: string; + title?: string; + year?: string; + cover?: string; +} + +// Function to open IndexedDB +function openIndexedDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open('bouquet', 1); + + request.onupgradeneeded = event => { + const db = (event.target as IDBOpenDBRequest).result; + db.createObjectStore('id3'); + }; + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onerror = () => { + reject(request.error); + }; + }); +} + +// Function to get ID3Tag from IndexedDB +function getID3TagFromDB(db: IDBDatabase, hash: string): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('id3', 'readonly'); + const store = transaction.objectStore('id3'); + const request = store.get(hash); + + request.onsuccess = () => { + resolve(request.result || null); + }; + + request.onerror = () => { + reject(request.error); + }; + }); +} + +// Function to save ID3Tag to IndexedDB +function saveID3TagToDB(db: IDBDatabase, key: string, id3Tag: ID3Tag): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('id3', 'readwrite'); + const store = transaction.objectStore('id3'); + const request = store.put(id3Tag, key); + + request.onsuccess = () => { + resolve(); + }; + + request.onerror = () => { + reject(request.error); + }; + }); +} + +// Function to resize image +function resizeImage(imageArray: ArrayBuffer, maxWidth: number, maxHeight: number): Promise { + return new Promise((resolve, reject) => { + const blob = new Blob([imageArray], { type: 'image/jpeg' }); + const url = URL.createObjectURL(blob); + + const img = new Image(); + img.onload = () => { + let width = img.width; + let height = img.height; + + // Calculate the aspect ratio + const aspectRatio = width / height; + + // Adjust the width and height to maintain the aspect ratio within the max dimensions + if (width > height) { + if (width > maxWidth) { + width = maxWidth; + height = Math.round(width / aspectRatio); + } + } else { + if (height > maxHeight) { + height = maxHeight; + width = Math.round(height * aspectRatio); + } + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + + if (ctx) { + // Draw the image onto the canvas with the new dimensions + ctx.drawImage(img, 0, 0, width, height); + // Convert the canvas to a data URL + const dataUrl = canvas.toDataURL('image/jpeg'); + resolve(dataUrl); + } else { + reject(new Error('Canvas context could not be retrieved')); + } + + URL.revokeObjectURL(url); // Clean up + }; + + img.onerror = () => { + reject(new Error('Image could not be loaded')); + URL.revokeObjectURL(url); // Clean up + }; + + img.src = url; + }); +} + +export const fetchId3Tag = async (blob: BlobDescriptor): Promise => { + const db = await openIndexedDB(); + const cachedID3Tag = await getID3TagFromDB(db, blob.sha256); + + if (cachedID3Tag) { + return { ...blob, id3: cachedID3Tag } as AudioBlob; + } + + const id3Tag = await id3.fromUrl(blob.url).catch(e => console.warn(e)); + if (id3Tag) { + const tagResult: ID3Tag = { + title: id3Tag.title || undefined, + artist: id3Tag.artist || undefined, + album: id3Tag.album || undefined, + year: id3Tag.year || undefined, + }; + + if (id3Tag.kind == 'v2') { + const id3v2 = id3Tag as ID3TagV2; + if (id3v2.images[0].data) { + tagResult.cover = await resizeImage(id3v2.images[0].data, 128, 128); + } + } + + console.log(blob.sha256, tagResult); + + await saveID3TagToDB(db, blob.sha256, tagResult); + return { ...blob, id3: tagResult }; + } + console.log('No ID3 tag found for ' + blob.sha256); + + return blob; // only when ID3 fails completely +}; diff --git a/src/utils/useFileMetaEvents.ts b/src/utils/useFileMetaEvents.ts index b0d66e0..171aa57 100644 --- a/src/utils/useFileMetaEvents.ts +++ b/src/utils/useFileMetaEvents.ts @@ -30,7 +30,7 @@ console.log(allXTags); const groupedByX = groupBy(allXTags, item => item.x); return mapValues(groupedByX, v => v.map(e => e.ev)); }, [fileMetaSub]); - console.log(fileMetaEventsByHash); + // console.log(fileMetaEventsByHash); return fileMetaEventsByHash; };