feat: Added server add button

This commit is contained in:
florian 2024-04-09 22:10:58 +02:00
parent ec9d5d90b2
commit 820eb340cf
8 changed files with 150 additions and 44 deletions

View File

@ -12,7 +12,7 @@
.blob-list .blob {
@apply p-1 hover:bg-zinc-700 rounded-md grid pr-4;
grid-template-columns: 2em auto 6em 10em 7em 3em;
grid-template-columns: 2em auto /*auto*/ 2em 6em 10em 7em 3em;
}
.blob-list .blob span {
@ -32,7 +32,7 @@
}
.blog-list-header button {
@apply bg-zinc-800 hover:bg-zinc-700 p-2 ml-2 my-2 text-white rounded-lg disabled:text-zinc-700 disabled:bg-zinc-900;
@apply bg-zinc-800 hover:bg-zinc-700 p-2 ml-2 my-2 text-white rounded-lg disabled:text-zinc-700 disabled:bg-zinc-900;
}
.blog-list-header button.selected {
@ -42,3 +42,7 @@
.blog-list-header svg {
@apply w-6 opacity-80 hover:opacity-100;
}
.blob-list .blob span a.pill {
@apply bg-zinc-700 p-1 px-2 rounded-2xl text-white;
}

View File

@ -1,6 +1,7 @@
import {
ClipboardDocumentIcon,
DocumentIcon,
ExclamationTriangleIcon,
FilmIcon,
ListBulletIcon,
MusicalNoteIcon,
@ -15,6 +16,7 @@ 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';
type ListMode = 'gallery' | 'list' | 'audio' | 'video' | 'docs';
@ -28,6 +30,7 @@ type AudioBlob = BlobDescriptor & { id3?: ID3Tag; imageData?: string };
const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
const [mode, setMode] = useState<ListMode>('list');
const { distribution } = useServerInfo();
const images = useMemo(
() => blobs.filter(b => b.type?.startsWith('image/')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
@ -100,18 +103,18 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
<div className={className}>
<span>
<a
className=" cursor-pointer"
className="cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(blob.url);
}}
>
<ClipboardDocumentIcon />
<ClipboardDocumentIcon title="Copy link to clipboard" />
</a>
</span>
{onDelete && (
<span>
<a onClick={() => onDelete(blob)} className=" cursor-pointer">
<TrashIcon />
<a onClick={() => onDelete(blob)} className="cursor-pointer">
<TrashIcon title="Delete this blob" />
</a>
</span>
)}
@ -289,6 +292,18 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
{blob.sha256}
</a>
</span>
{/*
<span>
<a className="pill">🌸 drive</a> <a className="pill">📝 post</a>
</span>
*/}
<span>
{distribution[blob.sha256].servers.length == 1 ? (
<ExclamationTriangleIcon title="Not distributed to any other server" />
) : (
''
)}
</span>
<span>{formatFileSize(blob.size)}</span>
<span>{blob.type && `${blob.type}`}</span>
<span>{formatDate(blob.created)}</span>

View File

@ -1,12 +1,15 @@
const ProgressBar = ({ value, max }: { value: number; max: number }) => {
const ProgressBar = ({ value, max, description = '' }: { value: number; max: number; description?: string }) => {
//value=11;max=100;description="4,5 MB/s"
const percent = Math.floor((value * 100) / max);
const showDescription = percent > 10 && percent < 100;
return (
<div className="w-full bg-gray-200 rounded-lg dark:bg-zinc-900">
{max !== undefined && value !== undefined && max > 0 && (
<div
className="bg-pink-600 text-sm font-medium text-pink-100 text-center p-1 leading-none rounded-lg"
style={{ width: `${Math.floor((value * 100) / max)}%` }}
className="bg-pink-600 text-sm font-medium text-pink-100 text-center p-1 leading-none rounded-lg text-nowrap"
style={{ width: `${percent}%` }}
>
{Math.floor((value * 100) / max)}&nbsp;%
{percent}&nbsp;% {showDescription ? description : ''}
</div>
)}
</div>

View File

@ -54,3 +54,24 @@
transform-origin: center;
animation: spin 3s linear infinite;
}
.server-list-header {
@apply flex flex-row mt-4;
}
.server-list-header h2 {
@apply flex-grow;
}
.server-list-header button {
@apply bg-zinc-800 hover:bg-zinc-700 p-2 ml-2 my-2 text-white rounded-lg disabled:text-zinc-700 disabled:bg-zinc-900;
}
.server-list-header button.selected {
@apply bg-pink-700 text-white;
}
.server-list-header svg {
@apply w-6 opacity-80 hover:opacity-100;
}

View File

@ -1,3 +1,4 @@
import { PlusIcon, ServerIcon } from '@heroicons/react/24/outline';
import { useServerInfo } from '../../utils/useServerInfo';
import { Server as ServerType } from '../../utils/useServers';
import Server from './Server';
@ -10,29 +11,50 @@ type ServerListProps = {
onTransfer?: (server: string) => void;
onCancel?: () => void;
onCheck?: (server: string) => void;
title?: React.ReactElement;
showAddButton?: boolean;
};
export const ServerList = ({ servers, selectedServer, setSelectedServer, onTransfer, onCancel }: ServerListProps) => {
export const ServerList = ({
servers,
selectedServer,
setSelectedServer,
onTransfer,
onCancel,
title,
showAddButton = false
}: ServerListProps) => {
const { serverInfo, distribution } = useServerInfo();
const blobsWithOnlyOneOccurance = Object.values(distribution)
.filter(d => d.servers.length == 1)
.map(d => ({ ...d.blob, server: d.servers[0] }));
return (
<div className="server-list">
{servers.map(server => (
<Server
key={server.name}
serverInfo={serverInfo[server.name]}
server={server}
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
onTransfer={onTransfer}
onCancel={onCancel}
/* onCheck={onCheck} */
blobsOnlyOnThisServer={blobsWithOnlyOneOccurance.filter(b => b.server == server.name).length}
></Server>
))}
</div>
<>
<div className={`server-list-header ${!title ? 'justify-end' : ''}`}>
{title && <h2>{title}</h2>}
{showAddButton && <div className="content-center">
<button onClick={() => {}} className='flex flex-row gap-2' title="Add server">
<PlusIcon/><ServerIcon />
</button>
</div>}
</div>
<div className="server-list">
{servers.map(server => (
<Server
key={server.name}
serverInfo={serverInfo[server.name]}
server={server}
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
onTransfer={onTransfer}
onCancel={onCancel}
/* onCheck={onCheck} */
blobsOnlyOnThisServer={blobsWithOnlyOneOccurance.filter(b => b.server == server.name).length}
></Server>
))}
</div>
</>
);
};

View File

@ -54,13 +54,13 @@ function Home() {
return (
<>
<h2>Servers</h2>
<ServerList
servers={Object.values(serverInfo).sort()}
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
onTransfer={() => navigate('/transfer/' + selectedServer)}
onCheck={() => navigate('/check/' + selectedServer)}
title={<>Servers</>}
></ServerList>
{selectedServer && serverInfo[selectedServer] && selectedServerBlobs && (

View File

@ -103,22 +103,18 @@ export const Transfer = () => {
return (
transferSource && (
<>
<h2>
<ArrowUpOnSquareIcon /> Transfer Source
</h2>
<ServerList
servers={Object.values(serverInfo).filter(s => s.name == transferSource)}
onCancel={() => closeTransferMode()}
title={<><ArrowUpOnSquareIcon /> Transfer Source</>}
></ServerList>
<h2>
<ArrowDownOnSquareIcon /> Transfer Target
</h2>
<ServerList
servers={Object.values(serverInfo)
.filter(s => s.name != transferSource)
.sort()}
selectedServer={transferTarget}
setSelectedServer={setTransferTarget}
title={<><ArrowDownOnSquareIcon /> Transfer Target</>}
></ServerList>
{transferTarget && transferJobs && transferJobs.length > 0 ? (
<>

View File

@ -4,7 +4,7 @@ import { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useServerInfo } from '../utils/useServerInfo';
import { useQueryClient } from '@tanstack/react-query';
import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
import { ArrowUpOnSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
import ProgressBar from '../components/ProgressBar/ProgressBar';
import { removeExifData } from '../exif';
import CheckBox from '../components/CheckBox/CheckBox';
@ -25,9 +25,30 @@ function Upload() {
const [transfers, setTransfers] = useState<{ [key: string]: TransferStats }>({});
const [files, setFiles] = useState<File[]>([]);
const [cleanPrivateData, setCleanPrivateData] = useState(true);
const [transferSpeed, setTransferSpeed] = useState<number | undefined>();
// const [resizeImages, setResizeImages] = useState(false);
// const [publishToNostr, setPublishToNostr] = useState(false);
type ImageSize = {
width: number;
height: number;
};
const getImageSize = async (imageFile: File): Promise<ImageSize> => {
const img = new Image();
const objectUrl = URL.createObjectURL(imageFile);
const promise = new Promise<ImageSize>((resolve, reject) => {
img.onload = () => {
resolve({ width: img.width, height: img.height });
URL.revokeObjectURL(objectUrl);
};
img.onerror = () => reject();
});
img.src = objectUrl;
return promise;
};
async function uploadBlob(
server: string,
file: File,
@ -59,6 +80,12 @@ function Upload() {
// TODO use https://github.com/davejm/client-compress
// for image resizing
for (const file of filesToUpload) {
if (file.type.startsWith('image/')) {
const dimensions = await getImageSize(file);
console.log(dimensions);
}
}
if (filesToUpload && filesToUpload.length) {
// sum files sizes
@ -85,6 +112,7 @@ function Upload() {
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
const newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
setTransferSpeed(progressEvent.rate);
setTransfers(ut => ({
...ut,
[server.name]: { ...ut[server.name], transferred: serverTransferred + progressEvent.loaded },
@ -101,6 +129,7 @@ function Upload() {
}
queryClient.invalidateQueries({ queryKey: ['blobs', server.name] });
setFiles([]);
// TODO reset input control value??
}
}
};
@ -118,6 +147,7 @@ function Upload() {
if (selectedFiles && selectedFiles.length > 0) {
const newFiles = Array.from(selectedFiles);
setFiles(prevFiles => [...prevFiles, ...newFiles]);
clearTransfers();
}
};
@ -127,6 +157,7 @@ function Upload() {
if (droppedFiles && droppedFiles.length > 0) {
const newFiles = Array.from(droppedFiles);
setFiles(prevFiles => [...prevFiles, ...newFiles]);
clearTransfers();
}
};
@ -145,7 +176,6 @@ function Upload() {
>
<ArrowUpOnSquareIcon className="w-8 inline" /> Browse or drag & drop
</label>
<h3 className="text-lg text-white">Servers</h3>
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1.5em 20em auto' }}>
{servers.map(s => (
@ -157,14 +187,17 @@ function Upload() {
label={s.name}
></CheckBox>
{transfers[s.name]?.enabled ? (
<ProgressBar value={transfers[s.name].transferred} max={transfers[s.name].size} />
<ProgressBar
value={transfers[s.name].transferred}
max={transfers[s.name].size}
description={transferSpeed ? '' + formatFileSize(transferSpeed) + '/s' : ''}
/>
) : (
<div></div>
)}
</>
))}
</div>
<h3 className="text-lg text-white">Options</h3>
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1.5em auto' }}>
<CheckBox
@ -188,13 +221,25 @@ function Upload() {
></CheckBox>
*/}
</div>
<button
className="p-2 px-4 bg-zinc-600 hover:bg-pink-700 text-white rounded-lg w-2/6 disabled:text-zinc-800 disabled:bg-zinc-900 "
onClick={() => upload()}
disabled={files.length == 0}
>
Upload{files.length > 0 ? (files.length == 1 ? ` 1 file` : ` ${files.length} files` ) : ''} / {formatFileSize(sizeOfFilesToUpload)}
</button>
<div className="flex flex-row gap-2">
<button
className="p-2 px-4 bg-zinc-600 hover:bg-pink-700 text-white rounded-lg w-3/12 disabled:text-zinc-800 disabled:bg-zinc-900 "
onClick={() => upload()}
disabled={files.length == 0}
>
Upload{files.length > 0 ? (files.length == 1 ? ` 1 file` : ` ${files.length} files`) : ''} /{' '}
{formatFileSize(sizeOfFilesToUpload)}
</button>
<button
className="p-2 px-4 bg-zinc-600 hover:bg-pink-700 text-white rounded-lg "
onClick={() => {
clearTransfers();
setFiles([]);
}}
>
<TrashIcon className="w-6" />
</button>
</div>
</div>
</>
);