mirror of
https://git.v0l.io/florian/bouquet.git
synced 2025-03-18 01:13:00 +01:00
feat: Added server add button
This commit is contained in:
parent
ec9d5d90b2
commit
820eb340cf
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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)} %
|
||||
{percent} % {showDescription ? description : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 && (
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user