forked from florian/bouquet
feat: delete selected
This commit is contained in:
parent
36a3b6a854
commit
50ef5fa0aa
@ -8,6 +8,7 @@ import {
|
||||
MusicalNoteIcon,
|
||||
PhotoIcon,
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { formatFileSize, formatDate } from '../../utils/utils';
|
||||
import ImageBlobList from '../ImageBlobList/ImageBlobList';
|
||||
@ -23,7 +24,7 @@ import { useBlobSelection } from './useBlobSelection';
|
||||
|
||||
type BlobListProps = {
|
||||
blobs: BlobDescriptor[];
|
||||
onDelete?: (blob: BlobDescriptor) => void;
|
||||
onDelete?: (blobs: BlobDescriptor[]) => void;
|
||||
title?: string;
|
||||
className?: string;
|
||||
};
|
||||
@ -32,7 +33,7 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) =>
|
||||
const [mode, setMode] = useState<ListMode>('list');
|
||||
const { distribution } = useServerInfo();
|
||||
const fileMetaEventsByHash = useFileMetaEventsByHash();
|
||||
const { handleSelectBlob, selectedBlobs } = useBlobSelection(blobs);
|
||||
const { handleSelectBlob, selectedBlobs, setSelectedBlobs } = useBlobSelection(blobs);
|
||||
const images = useMemo(
|
||||
() => blobs.filter(b => b.type?.startsWith('image/')).sort((a, b) => (a.uploaded > b.uploaded ? -1 : 1)), // descending
|
||||
[blobs]
|
||||
@ -66,13 +67,6 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) =>
|
||||
<ClipboardDocumentIcon />
|
||||
</a>
|
||||
</span>
|
||||
{onDelete && (
|
||||
<span>
|
||||
<a onClick={() => onDelete(blob)} className="link link-primary tooltip" data-tip="Delete this blob">
|
||||
<TrashIcon />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -100,7 +94,24 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) =>
|
||||
{title && <h2>{title}</h2>}
|
||||
|
||||
{selectedCount > 0 && (
|
||||
<div className="flex bg-base-200 rounded-box gap-1 mr-2 py-4 px-8">{selectedCount} blobs selected </div>
|
||||
<div className="flex bg-base-200 rounded-box gap-2 mr-2 py-2 px-8 align-middle items-center">
|
||||
{selectedCount} blobs selected
|
||||
{onDelete && (
|
||||
<button
|
||||
className="btn btn-icon btn-primary btn-sm tooltip"
|
||||
onClick={async () => {
|
||||
await onDelete(blobs.filter(b => selectedBlobs[b.sha256]));
|
||||
setSelectedBlobs({});
|
||||
}}
|
||||
data-tip="Delete the selected blobs"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-icon btn-sm" onClick={() => setSelectedBlobs({})}>
|
||||
<XMarkIcon className="h-6 w-6 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<BlobListTypeMenu
|
||||
mode={mode}
|
||||
@ -113,19 +124,14 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) =>
|
||||
</div>
|
||||
|
||||
{mode == 'gallery' && (
|
||||
<ImageBlobList
|
||||
images={images}
|
||||
onDelete={onDelete}
|
||||
selectedBlobs={selectedBlobs}
|
||||
handleSelectBlob={handleSelectBlob}
|
||||
/>
|
||||
<ImageBlobList images={images} selectedBlobs={selectedBlobs} handleSelectBlob={handleSelectBlob} />
|
||||
)}
|
||||
|
||||
{mode == 'video' && <VideoBlobList videos={videos} onDelete={onDelete} />}
|
||||
{mode == 'video' && <VideoBlobList videos={videos} />}
|
||||
|
||||
{mode == 'audio' && <AudioBlobList audioFiles={audioFiles} onDelete={onDelete} />}
|
||||
{mode == 'audio' && <AudioBlobList audioFiles={audioFiles} />}
|
||||
|
||||
{mode == 'docs' && <DocumentBlobList docs={docs} onDelete={onDelete} />}
|
||||
{mode == 'docs' && <DocumentBlobList docs={docs} />}
|
||||
|
||||
{mode == 'list' && (
|
||||
<div className="blob-list">
|
||||
@ -147,15 +153,17 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) =>
|
||||
key={blob.sha256}
|
||||
onClick={e => handleSelectBlob(blob.sha256, e)}
|
||||
>
|
||||
<td className="whitespace-nowrap">
|
||||
<td className="whitespace-nowrap w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary checkbox-sm mr-2"
|
||||
checked={selectedBlobs[blob.sha256]}
|
||||
checked={!!selectedBlobs[blob.sha256]}
|
||||
onChange={e => handleSelectBlob(blob.sha256, e)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
{getMimeTypeIcon(blob.type)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap">
|
||||
<a className="link link-primary" href={blob.url} target="_blank">
|
||||
{blob.sha256.slice(0, 15)}
|
||||
</a>
|
||||
|
@ -1,49 +1,58 @@
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export type HandleSelectBlobType = (
|
||||
sha256: string,
|
||||
event: React.MouseEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement>
|
||||
) => void;
|
||||
export const useBlobSelection = (blobs: BlobDescriptor[]) => {
|
||||
const [selectedBlobs, setSelectedBlobs] = useState<{ [key: string]: boolean }>({});
|
||||
const [selectedBlobs, setSelectedBlobs] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const handleSelectBlob: HandleSelectBlobType = useCallback(
|
||||
(sha256: string, event: React.MouseEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
const handleSelectBlob: HandleSelectBlobType = (
|
||||
sha256: string,
|
||||
event: React.MouseEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isMouseEvent = (event: any): event is React.MouseEvent<HTMLTableRowElement> => {
|
||||
return event.ctrlKey !== undefined || event.metaKey !== undefined;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isMouseEvent = (event: any): event is React.MouseEvent<HTMLTableRowElement> => {
|
||||
return event.ctrlKey !== undefined || event.metaKey !== undefined;
|
||||
};
|
||||
|
||||
if (isMouseEvent(event)) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setSelectedBlobs(prev => ({
|
||||
...prev,
|
||||
[sha256]: !prev[sha256]
|
||||
}));
|
||||
} else if (event.shiftKey) {
|
||||
const lastSelectedIndex = blobs.findIndex(blob => blob.sha256 === Object.keys(selectedBlobs).find(key => selectedBlobs[key]));
|
||||
const currentIndex = blobs.findIndex(blob => blob.sha256 === sha256);
|
||||
const [start, end] = [lastSelectedIndex, currentIndex].sort((a, b) => a - b);
|
||||
const newSelection = blobs.slice(start, end + 1).reduce((acc, blob) => {
|
||||
if (isMouseEvent(event)) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setSelectedBlobs(prev => ({
|
||||
...prev,
|
||||
[sha256]: !prev[sha256],
|
||||
}));
|
||||
} else if (event.shiftKey) {
|
||||
const lastSelectedIndex = blobs.findIndex(
|
||||
blob => blob.sha256 === Object.keys(selectedBlobs).find(key => selectedBlobs[key])
|
||||
);
|
||||
const currentIndex = blobs.findIndex(blob => blob.sha256 === sha256);
|
||||
const [start, end] = [lastSelectedIndex, currentIndex].sort((a, b) => a - b);
|
||||
const newSelection = blobs.slice(start, end + 1).reduce(
|
||||
(acc, blob) => {
|
||||
acc[blob.sha256] = true;
|
||||
return acc;
|
||||
}, {} as { [key: string]: boolean });
|
||||
setSelectedBlobs(prev => ({
|
||||
...prev,
|
||||
...newSelection
|
||||
}));
|
||||
} else {
|
||||
setSelectedBlobs({ [sha256]: true });
|
||||
}
|
||||
},
|
||||
{} as { [key: string]: boolean }
|
||||
);
|
||||
setSelectedBlobs(prev => ({
|
||||
...prev,
|
||||
...newSelection,
|
||||
}));
|
||||
} else {
|
||||
setSelectedBlobs({ [sha256]: true });
|
||||
setSelectedBlobs(prev => ({
|
||||
...prev,
|
||||
[sha256]: !prev[sha256],
|
||||
}));
|
||||
}
|
||||
},
|
||||
[blobs, selectedBlobs]
|
||||
);
|
||||
} else {
|
||||
setSelectedBlobs(prev => ({
|
||||
...prev,
|
||||
[sha256]: !prev[sha256],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return { handleSelectBlob, selectedBlobs, setSelectedBlobs };
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { formatFileSize, formatDate } from '../../utils/utils';
|
||||
import { ClipboardDocumentIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
|
||||
@ -8,7 +8,7 @@ type DocumentBlobListProps = {
|
||||
onDelete?: (blob: BlobDescriptor) => void;
|
||||
};
|
||||
|
||||
const DocumentBlobList = ({ docs, onDelete }: DocumentBlobListProps) => (
|
||||
const DocumentBlobList = ({ docs }: DocumentBlobListProps) => (
|
||||
<div className="blob-list grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-2 justify-center">
|
||||
{docs.map(blob => (
|
||||
<div key={blob.sha256} className="p-4 rounded-lg bg-base-300 relative flex flex-col">
|
||||
@ -31,13 +31,6 @@ const DocumentBlobList = ({ docs, onDelete }: DocumentBlobListProps) => (
|
||||
<ClipboardDocumentIcon />
|
||||
</a>
|
||||
</span>
|
||||
{onDelete && (
|
||||
<span>
|
||||
<a onClick={() => onDelete(blob)} className="link link-primary tooltip" data-tip="Delete this blob">
|
||||
<TrashIcon />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { formatFileSize, formatDate } from '../../utils/utils';
|
||||
import { ClipboardDocumentIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { HandleSelectBlobType } from '../BlobList/useBlobSelection';
|
||||
|
||||
type ImageBlobListProps = {
|
||||
images: BlobDescriptor[];
|
||||
onDelete?: (blob: BlobDescriptor) => void;
|
||||
handleSelectBlob: HandleSelectBlobType;
|
||||
selectedBlobs: { [key: string]: boolean };
|
||||
};
|
||||
|
||||
const ImageBlobList = ({ images, onDelete, handleSelectBlob, selectedBlobs }: ImageBlobListProps) => (
|
||||
const ImageBlobList = ({ images, handleSelectBlob, selectedBlobs }: ImageBlobListProps) => (
|
||||
<div className="blob-list grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2 justify-center">
|
||||
{images.map(blob => (
|
||||
<div
|
||||
@ -47,13 +46,6 @@ const ImageBlobList = ({ images, onDelete, handleSelectBlob, selectedBlobs }: Im
|
||||
<ClipboardDocumentIcon />
|
||||
</a>
|
||||
</span>
|
||||
{onDelete && (
|
||||
<span>
|
||||
<a onClick={() => onDelete(blob)} className="link link-primary tooltip" data-tip="Delete this blob">
|
||||
<TrashIcon />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { formatFileSize, formatDate } from '../../utils/utils';
|
||||
import { ClipboardDocumentIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
|
||||
type VideoBlobListProps = {
|
||||
videos: BlobDescriptor[];
|
||||
onDelete?: (blob: BlobDescriptor) => void;
|
||||
};
|
||||
|
||||
const VideoBlobList = ({ videos, onDelete }: VideoBlobListProps) => (
|
||||
const VideoBlobList = ({ videos }: VideoBlobListProps) => (
|
||||
<div className="blob-list grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-2 justify-center">
|
||||
{videos.map(blob => (
|
||||
<div key={blob.sha256} className="p-4 rounded-lg bg-base-300 relative text-center">
|
||||
@ -26,13 +25,6 @@ const VideoBlobList = ({ videos, onDelete }: VideoBlobListProps) => (
|
||||
<ClipboardDocumentIcon />
|
||||
</a>
|
||||
</span>
|
||||
{onDelete && (
|
||||
<span>
|
||||
<a onClick={() => onDelete(blob)} className="link link-primary tooltip" data-tip="Delete this blob">
|
||||
<TrashIcon />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -72,13 +72,15 @@ function Home() {
|
||||
className="mt-4"
|
||||
title={`Content on ${serverInfo[selectedServer].name}`}
|
||||
blobs={selectedServerBlobs}
|
||||
onDelete={blob =>
|
||||
deleteBlob.mutate({
|
||||
serverName: serverInfo[selectedServer].name,
|
||||
serverUrl: serverInfo[selectedServer].url,
|
||||
hash: blob.sha256,
|
||||
})
|
||||
}
|
||||
onDelete={async blobs => {
|
||||
for (const blob of blobs) {
|
||||
await deleteBlob.mutateAsync({
|
||||
serverName: serverInfo[selectedServer].name,
|
||||
serverUrl: serverInfo[selectedServer].url,
|
||||
hash: blob.sha256,
|
||||
});
|
||||
}
|
||||
}}
|
||||
></BlobList>
|
||||
)}
|
||||
</>
|
||||
|
Loading…
Reference in New Issue
Block a user