feat: PDF and audio views

This commit is contained in:
florian 2024-03-30 23:18:41 +01:00
parent 502b50350c
commit cc1ec2da2f
8 changed files with 201 additions and 55 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

View File

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

View File

@ -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,48 +24,189 @@ 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' }}>
<a href={blob.url} target="_blank">
<div
className=""
style={{
width: 200,
height: 200,
cursor: 'pointer',
display: 'inline-block',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url(https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${blob.url})`,
}}
></div>
</a>
<div className="flex flex-row text-xs">
<span>{formatFileSize(blob.size)}</span>
<span className=" flex-grow text-right">{formatDate(blob.created)}</span>
</div>
<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=""
style={{
width: 200,
height: 200,
cursor: 'pointer',
display: 'inline-block',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url(https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${blob.url})`,
}}
></div>
</a>
<div className="flex flex-row text-xs">
<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;

View File

@ -13,13 +13,20 @@ 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 />}>
<Route path="/" element={<Home />} />
<Route path="/transfer/:source" element={<Transfer />} />
<Route path="/upload" element={<Upload/>} />
<Route path="/check/:source" element={<Check/>} />
<Route path="/upload" element={<Upload />} />
<Route path="/check/:source" element={<Check />} />
</Route>
)
);

View File

@ -16,7 +16,6 @@ function Check() {
servers={Object.values(serverInfo).filter(s => s.name == source)}
onCancel={() => navigate('/')}
></ServerList>
</>
);
}

View File

@ -29,7 +29,7 @@ type TransferStatus = {
export const Transfer = () => {
const { source: transferSource } = useParams();
const navigate = useNavigate();
const {serverInfo} = useServerInfo();
const { serverInfo } = useServerInfo();
const [transferTarget, setTransferTarget] = useState<string | undefined>();
const { signEventTemplate } = useNDK();
const queryClient = useQueryClient();

View File

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