feat: PDF and audio views
This commit is contained in:
parent
502b50350c
commit
cc1ec2da2f
@ -19,12 +19,14 @@
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "^2.2.8",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@tanstack/react-query-devtools": "^5.28.6",
|
||||
"add": "^2.0.6",
|
||||
"axios": "^1.6.8",
|
||||
"blossom-client-sdk": "^0.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"nostr-tools": "^2.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-pdf": "^7.7.1",
|
||||
"react-router-dom": "^6.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.blog-list-header button {
|
||||
@apply bg-neutral-800 hover:bg-neutral-700 p-2 ml-2 my-2 rounded-lg;
|
||||
@apply bg-neutral-800 hover:bg-neutral-700 p-2 ml-2 my-2 text-white rounded-lg disabled:text-neutral-700 disabled:bg-neutral-900;
|
||||
}
|
||||
|
||||
.blog-list-header button.selected {
|
||||
@ -40,5 +40,5 @@
|
||||
}
|
||||
|
||||
.blog-list-header svg {
|
||||
@apply w-6 text-white opacity-80 hover:opacity-100;
|
||||
@apply w-6 opacity-80 hover:opacity-100;
|
||||
}
|
||||
|
@ -1,10 +1,19 @@
|
||||
import { ClipboardDocumentIcon, DocumentIcon, ListBulletIcon, PhotoIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ClipboardDocumentIcon,
|
||||
DocumentIcon,
|
||||
FilmIcon,
|
||||
ListBulletIcon,
|
||||
MusicalNoteIcon,
|
||||
PhotoIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { formatDate, formatFileSize } from '../../utils';
|
||||
import './BlobList.css';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
|
||||
type ListMode = 'gallery' | 'list';
|
||||
type ListMode = 'gallery' | 'list' | 'audio' | 'video' | 'docs';
|
||||
|
||||
type BlobListProps = {
|
||||
blobs: BlobDescriptor[];
|
||||
@ -15,27 +24,112 @@ type BlobListProps = {
|
||||
const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
|
||||
const [mode, setMode] = useState<ListMode>('list');
|
||||
|
||||
const images = useMemo(
|
||||
() => blobs.filter(b => b.type?.startsWith('image/')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
|
||||
[blobs]
|
||||
);
|
||||
|
||||
const videos = useMemo(
|
||||
() => blobs.filter(b => b.type?.startsWith('video/')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
|
||||
[blobs]
|
||||
);
|
||||
|
||||
const audio = useMemo(
|
||||
() => blobs.filter(b => b.type?.startsWith('audio/')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
|
||||
[blobs]
|
||||
);
|
||||
|
||||
const docs = useMemo(
|
||||
() => blobs.filter(b => b.type?.startsWith('application/pdf')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
|
||||
[blobs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
switch (mode) {
|
||||
case 'video':
|
||||
if (videos.length == 0) setMode('list');
|
||||
break;
|
||||
case 'audio':
|
||||
if (audio.length == 0) setMode('list');
|
||||
break;
|
||||
case 'gallery':
|
||||
if (images.length == 0) setMode('list');
|
||||
break;
|
||||
case 'docs':
|
||||
if (docs.length == 0) setMode('list');
|
||||
break;
|
||||
}
|
||||
}, [videos, images, audio, mode, docs]);
|
||||
|
||||
const Actions = ({ blob, className }: { blob: BlobDescriptor; className?: string }) => (
|
||||
<div className={className}>
|
||||
<span>
|
||||
<a
|
||||
className=" cursor-pointer"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(blob.url);
|
||||
}}
|
||||
>
|
||||
<ClipboardDocumentIcon />
|
||||
</a>
|
||||
</span>
|
||||
{onDelete && (
|
||||
<span>
|
||||
<a onClick={() => onDelete(blob)} className=" cursor-pointer">
|
||||
<TrashIcon />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`blog-list-header ${!title ? 'justify-end' : ''}`}>
|
||||
{title && <h2>{title}</h2>}
|
||||
<div className=" content-center">
|
||||
<button onClick={() => setMode('list')} className={mode == 'list' ? 'selected' : ''}>
|
||||
<button onClick={() => setMode('list')} className={mode == 'list' ? 'selected' : ''} title="All content">
|
||||
<ListBulletIcon />
|
||||
</button>
|
||||
<button onClick={() => setMode('gallery')} className={mode == 'gallery' ? 'selected' : ''}>
|
||||
<button
|
||||
onClick={() => setMode('gallery')}
|
||||
disabled={images.length == 0}
|
||||
className={mode == 'gallery' ? 'selected' : ''}
|
||||
title="Images"
|
||||
>
|
||||
<PhotoIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('audio')}
|
||||
disabled={audio.length == 0}
|
||||
className={mode == 'audio' ? 'selected' : ''}
|
||||
title="Music"
|
||||
>
|
||||
<MusicalNoteIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('video')}
|
||||
disabled={videos.length == 0}
|
||||
className={mode == 'video' ? 'selected' : ''}
|
||||
title="Video"
|
||||
>
|
||||
<FilmIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('docs')}
|
||||
disabled={videos.length == 0}
|
||||
className={mode == 'docs' ? 'selected' : ''}
|
||||
title="PDF Documents"
|
||||
>
|
||||
<DocumentIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode == 'gallery' && (
|
||||
<div className="blob-list flex flex-wrap justify-center">
|
||||
{blobs
|
||||
.filter(b => b.type?.startsWith('image/'))
|
||||
.sort((a, b) => (a.created > b.created ? -1 : 1)) // descending
|
||||
.map(blob => (
|
||||
<div className="p-2 rounded-lg bg-neutral-900 m-2" style={{ display: 'inline-block' }}>
|
||||
<div className="blob-list flex flex-wrap justify-center flex-grow">
|
||||
{images.map(blob => (
|
||||
<div className="p-2 rounded-lg bg-neutral-900 m-2 relative inline-block text-center">
|
||||
<a href={blob.url} target="_blank">
|
||||
<div
|
||||
className=""
|
||||
@ -55,6 +149,62 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
|
||||
<span>{formatFileSize(blob.size)}</span>
|
||||
<span className=" flex-grow text-right">{formatDate(blob.created)}</span>
|
||||
</div>
|
||||
<Actions blob={blob} className="actions absolute bottom-8 right-0"></Actions>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode == 'video' && (
|
||||
<div className="blob-list flex flex-wrap justify-center">
|
||||
{videos.map(blob => (
|
||||
<div className="p-4 rounded-lg bg-neutral-900 m-2 relative flex flex-col" style={{ width: '340px' }}>
|
||||
<video src={blob.url} preload="metadata" width={320} controls playsInline></video>
|
||||
<div className="flex flex-grow flex-row text-xs pt-12 items-end">
|
||||
<span>{formatFileSize(blob.size)}</span>
|
||||
<span className=" flex-grow text-right">{formatDate(blob.created)}</span>
|
||||
</div>
|
||||
<Actions blob={blob} className="actions absolute bottom-10 right-2 " />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode == 'audio' && (
|
||||
<div className="blob-list flex flex-wrap justify-center">
|
||||
{audio.map(blob => (
|
||||
<div className="p-4 rounded-lg bg-neutral-900 m-2 relative flex flex-col" style={{ width: '22em' }}>
|
||||
<audio src={blob.url} controls></audio>
|
||||
<div className="flex flex-grow flex-row text-xs pt-12 items-end">
|
||||
<span>{formatFileSize(blob.size)}</span>
|
||||
<span className=" flex-grow text-right">{formatDate(blob.created)}</span>
|
||||
</div>
|
||||
<Actions blob={blob} className="actions absolute bottom-10 right-2 " />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode == 'docs' && (
|
||||
<div className="blob-list flex flex-wrap justify-center">
|
||||
{docs.map(blob => (
|
||||
<div className="p-4 rounded-lg bg-neutral-900 m-2 relative flex flex-col" style={{ width: '22em' }}>
|
||||
<a href={blob.url} target="_blank" className="block overflow-clip text-ellipsis py-2">
|
||||
<Document file={blob.url}>
|
||||
<Page
|
||||
pageIndex={0}
|
||||
width={300}
|
||||
renderTextLayer={false}
|
||||
renderAnnotationLayer={false}
|
||||
renderForms={false}
|
||||
/>
|
||||
</Document>
|
||||
</a>
|
||||
<div className="flex flex-grow flex-row text-xs pt-12 items-end">
|
||||
<span>{formatFileSize(blob.size)}</span>
|
||||
<span className=" flex-grow text-right">{formatDate(blob.created)}</span>
|
||||
</div>
|
||||
<Actions blob={blob} className="actions absolute bottom-10 right-2 " />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -75,28 +225,13 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
|
||||
<span>{formatFileSize(blob.size)}</span>
|
||||
<span>{blob.type && `${blob.type}`}</span>
|
||||
<span>{formatDate(blob.created)}</span>
|
||||
<div>
|
||||
<span>
|
||||
<a onClick={() => {navigator.clipboard.writeText(blob.url)}}>
|
||||
<ClipboardDocumentIcon />
|
||||
</a>
|
||||
</span>
|
||||
{onDelete && (
|
||||
|
||||
<span>
|
||||
<a onClick={() => onDelete(blob)}>
|
||||
<TrashIcon />
|
||||
</a>
|
||||
</span> )}
|
||||
</div>
|
||||
|
||||
<Actions blob={blob}></Actions>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default BlobList;
|
||||
|
@ -13,6 +13,13 @@ import Check from './pages/Check.tsx';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
import { pdfjs } from 'react-pdf';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.js',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route element={<Layout />}>
|
||||
|
@ -16,7 +16,6 @@ function Check() {
|
||||
servers={Object.values(serverInfo).filter(s => s.name == source)}
|
||||
onCancel={() => navigate('/')}
|
||||
></ServerList>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChangeEvent, DragEvent, useEffect, useState } from 'react';
|
||||
import { ChangeEvent, DragEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { useServers } from '../utils/useServers';
|
||||
import { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
|
||||
import { useNDK } from '../ndk';
|
||||
@ -9,6 +9,7 @@ import ProgressBar from '../components/ProgressBar/ProgressBar';
|
||||
import { removeExifData } from '../exif';
|
||||
import CheckBox from '../components/CheckBox/CheckBox';
|
||||
import axios, { AxiosProgressEvent } from 'axios';
|
||||
import { formatFileSize } from '../utils';
|
||||
|
||||
type TransferStats = {
|
||||
enabled: boolean;
|
||||
@ -129,6 +130,8 @@ function Upload() {
|
||||
}
|
||||
};
|
||||
|
||||
const sizeOfFilesToUpload = useMemo(() => files.reduce((acc, file) => (acc += file.size), 0), [files]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Upload</h2>
|
||||
@ -190,7 +193,7 @@ function Upload() {
|
||||
onClick={() => upload()}
|
||||
disabled={files.length == 0}
|
||||
>
|
||||
Upload{files.length > 0 ? (files.length == 1 ? ` 1 file` : ` ${files.length} files`) : ''}
|
||||
Upload{files.length > 0 ? (files.length == 1 ? ` 1 file` : ` ${files.length} files` ) : ''} / {formatFileSize(sizeOfFilesToUpload)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
Loading…
Reference in New Issue
Block a user