[ENG-379] Explorer item resizer (#580)

* added item resizer
fixed explorer store bug
refactored file image component

* better sizing for videos

* fixed inspector width issue

* remove console.log

* added column titles to list view + extra details

* moved util

* remove imports

* Update packages/interface/src/components/explorer/FileColumns.tsx

Co-authored-by: Brendan Allan <brendonovich@outlook.com>

* address issues

* fix extension in file list name

* Update packages/interface/src/components/explorer/FileColumns.tsx

---------

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
Jamie Pine 2023-02-24 22:16:57 -08:00 committed by GitHub
parent 0b9005fdef
commit 677e1b63e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 284 additions and 208 deletions

View file

@ -118,18 +118,6 @@ pub(crate) fn mount() -> impl RouterBuilderLike<Ctx> {
.exec() .exec()
.await?; .await?;
// library
// .queue_job(Job::new(
// ThumbnailJobInit {
// location_id: location.id,
// // recursive: false, // TODO: do this
// root_path: PathBuf::from(&directory.materialized_path),
// background: true,
// },
// ThumbnailJob {},
// ))
// .await;
let mut items = Vec::with_capacity(file_paths.len()); let mut items = Vec::with_capacity(file_paths.len());
for file_path in file_paths { for file_path in file_paths {
@ -223,7 +211,7 @@ pub(crate) fn mount() -> impl RouterBuilderLike<Ctx> {
async_stream::stream! { async_stream::stream! {
let online = location_manager.get_online().await; let online = location_manager.get_online().await;
dbg!(&online); // dbg!(&online);
yield online; yield online;
while let Ok(locations) = rx.recv().await { while let Ok(locations) = rx.recv().await {

View file

@ -9,7 +9,7 @@ use tokio::fs::File;
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}; use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
use super::{context_menu_fs_info, FsInfo, BYTES}; use super::{context_menu_fs_info, FsInfo, BYTES_EXT};
pub struct FileDecryptorJob; pub struct FileDecryptorJob;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct FileDecryptorJobState {} pub struct FileDecryptorJobState {}
@ -74,7 +74,7 @@ impl StatefulJob for FileDecryptorJob {
|| { || {
let mut path = info.fs_path.clone(); let mut path = info.fs_path.clone();
let extension = path.extension().map_or("decrypted", |ext| { let extension = path.extension().map_or("decrypted", |ext| {
if ext == BYTES { if ext == BYTES_EXT {
"" ""
} else { } else {
"decrypted" "decrypted"

View file

@ -15,7 +15,7 @@ use specta::Type;
use tokio::{fs::File, io::AsyncReadExt}; use tokio::{fs::File, io::AsyncReadExt};
use tracing::warn; use tracing::warn;
use super::{context_menu_fs_info, FsInfo}; use super::{context_menu_fs_info, FsInfo, BYTES_EXT};
pub struct FileEncryptorJob; pub struct FileEncryptorJob;
@ -108,7 +108,7 @@ impl StatefulJob for FileEncryptorJob {
"path contents when converted to string", "path contents when converted to string",
), ),
})? })?
.to_string() + ".bytes", .to_string() + BYTES_EXT,
) )
}, },
)?; )?;

View file

@ -30,6 +30,8 @@ pub enum ObjectType {
Directory, Directory,
} }
pub const BYTES_EXT: &str = ".bytes";
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FsInfo { pub struct FsInfo {
pub path_data: file_path_with_object::Data, pub path_data: file_path_with_object::Data,

View file

@ -1,3 +1,4 @@
import clsx from 'clsx';
import { import {
ArrowBendUpRight, ArrowBendUpRight,
Clipboard, Clipboard,
@ -209,11 +210,16 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
); );
} }
export interface FileItemContextMenuProps extends PropsWithChildren { export interface ExplorerItemContextMenuProps extends PropsWithChildren {
data: ExplorerItem; data: ExplorerItem;
className?: string;
} }
export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps) { export function ExplorerItemContextMenu({
data,
className,
...props
}: ExplorerItemContextMenuProps) {
const { library } = useLibraryContext(); const { library } = useLibraryContext();
const store = useExplorerStore(); const store = useExplorerStore();
const params = useExplorerParams(); const params = useExplorerParams();
@ -227,7 +233,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
const copyFiles = useLibraryMutation('files.copyFiles'); const copyFiles = useLibraryMutation('files.copyFiles');
return ( return (
<div className="relative"> <div className={clsx('relative', className)}>
<CM.ContextMenu trigger={props.children}> <CM.ContextMenu trigger={props.children}>
<CM.Item <CM.Item
label="Open" label="Open"

View file

@ -1,5 +1,6 @@
import { PropsWithChildren, useState } from 'react'; import { PropsWithChildren, useState } from 'react';
import { Select, SelectOption } from '@sd/ui'; import { Select, SelectOption } from '@sd/ui';
import { getExplorerStore, useExplorerStore } from '../../hooks/useExplorerStore';
import Slider from '../primitive/Slider'; import Slider from '../primitive/Slider';
function Heading({ children }: PropsWithChildren) { function Heading({ children }: PropsWithChildren) {
@ -22,13 +23,23 @@ const sortOptions = {
export function ExplorerOptionsPanel() { export function ExplorerOptionsPanel() {
const [sortBy, setSortBy] = useState('name'); const [sortBy, setSortBy] = useState('name');
const [stackBy, setStackBy] = useState('kind'); const [stackBy, setStackBy] = useState('kind');
const [size, setSize] = useState([50]);
const explorerStore = useExplorerStore();
return ( return (
<div className="p-4 "> <div className="p-4 ">
{/* <Heading>Explorer Appearance</Heading> */} {/* <Heading>Explorer Appearance</Heading> */}
<SubHeading>Item size</SubHeading> <SubHeading>Item size</SubHeading>
<Slider defaultValue={size} step={10} /> <Slider
onValueChange={(value) => {
getExplorerStore().gridItemSize = value[0] || 100;
console.log({ value: value, gridItemSize: explorerStore.gridItemSize });
}}
defaultValue={[explorerStore.gridItemSize]}
max={200}
step={10}
min={60}
/>
<div className="my-2 mt-4 grid grid-cols-2 gap-2"> <div className="my-2 mt-4 grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<SubHeading>Sort by</SubHeading> <SubHeading>Sort by</SubHeading>

View file

@ -37,7 +37,7 @@ export interface TopBarButtonProps {
// export const TopBarIcon = (icon: any) => tw(icon)`m-0.5 w-5 h-5 text-ink-dull`; // export const TopBarIcon = (icon: any) => tw(icon)`m-0.5 w-5 h-5 text-ink-dull`;
const topBarButtonStyle = cva( const topBarButtonStyle = cva(
'text-ink hover:text-ink text-md hover:bg-app-selected radix-state-open:bg-app-selected mr-[1px] flex border-none p-0.5 font-medium outline-none transition-colors duration-100', 'text-ink hover:text-ink text-md hover:bg-app-selected radix-state-open:bg-app-selected mr-[1px] flex border-none !p-0.5 font-medium outline-none transition-colors duration-100',
{ {
variants: { variants: {
active: { active: {
@ -63,7 +63,12 @@ const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>( const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>(
({ active, rounding, className, ...props }, ref) => { ({ active, rounding, className, ...props }, ref) => {
return ( return (
<Button {...props} ref={ref} className={topBarButtonStyle({ active, rounding, className })}> <Button
// size="sm"
{...props}
ref={ref}
className={topBarButtonStyle({ active, rounding, className })}
>
{props.children} {props.children}
</Button> </Button>
); );

View file

@ -0,0 +1,42 @@
export interface IColumn {
column: string;
key: string;
width: number;
}
export const LIST_VIEW_HEADER_HEIGHT = 40;
// Function ensure no types are lost, but guarantees that they are Column[]
export function ensureIsColumns<T extends IColumn[]>(data: T) {
return data;
}
export const columns = [
{ column: 'Name', key: 'name', width: 280 },
// { column: 'Size', key: 'size_in_bytes', width: 120 },
{ column: 'Type', key: 'extension', width: 150 },
{ column: 'Size', key: 'size', width: 100 },
{ column: 'Date Created', key: 'date_created', width: 150 },
{ column: 'Content ID', key: 'cas_id', width: 150 }
] as const satisfies Readonly<IColumn[]>;
export type ColumnKey = (typeof columns)[number]['key'];
export function ListViewHeader() {
return (
<div
style={{ height: LIST_VIEW_HEADER_HEIGHT }}
className="mr-2 flex w-full flex-row rounded-lg border-2 border-transparent"
>
{columns.map((col) => (
<div
key={col.column}
className="flex items-center px-4 py-2 pr-2"
style={{ width: col.width, marginTop: -LIST_VIEW_HEADER_HEIGHT * 2 }}
>
<span className="text-xs font-medium ">{col.column}</span>
</div>
))}
</div>
);
}

View file

@ -2,9 +2,9 @@ import clsx from 'clsx';
import { HTMLAttributes } from 'react'; import { HTMLAttributes } from 'react';
import { ExplorerItem, ObjectKind, isObject } from '@sd/client'; import { ExplorerItem, ObjectKind, isObject } from '@sd/client';
import { cva, tw } from '@sd/ui'; import { cva, tw } from '@sd/ui';
import { getExplorerStore } from '~/hooks/useExplorerStore'; import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
import { FileItemContextMenu } from './ExplorerContextMenu'; import { ExplorerItemContextMenu } from './ExplorerContextMenu';
import FileThumb from './FileThumb'; import { FileThumb } from './FileThumb';
const NameArea = tw.div`flex justify-center`; const NameArea = tw.div`flex justify-center`;
@ -30,8 +30,10 @@ function FileItem({ data, selected, index, ...rest }: Props) {
const isVid = ObjectKind[objectData?.kind || 0] === 'Video'; const isVid = ObjectKind[objectData?.kind || 0] === 'Video';
const item = data.item; const item = data.item;
const explorerStore = useExplorerStore();
return ( return (
<FileItemContextMenu data={data}> <ExplorerItemContextMenu data={data}>
<div <div
onContextMenu={(e) => { onContextMenu={(e) => {
if (index != undefined) { if (index != undefined) {
@ -40,40 +42,22 @@ function FileItem({ data, selected, index, ...rest }: Props) {
}} }}
{...rest} {...rest}
draggable draggable
className={clsx('mb-3 inline-block w-[100px]', rest.className)} style={{ width: explorerStore.gridItemSize }}
className={clsx('mb-3 inline-block', rest.className)}
> >
<div <div
style={{ style={{
width: getExplorerStore().gridItemSize, width: explorerStore.gridItemSize,
height: getExplorerStore().gridItemSize height: explorerStore.gridItemSize
}} }}
className={clsx( className={clsx(
'mb-1 rounded-lg border-2 border-transparent text-center active:translate-y-[1px]', 'mb-1 rounded-lg border-2 border-transparent text-center active:translate-y-[1px]',
{ {
'bg-app-selected/30': selected 'bg-app-selected/20': selected
} }
)} )}
> >
<div <FileThumb data={data} size={explorerStore.gridItemSize} />
className={clsx(
'relative flex h-full shrink-0 items-center justify-center rounded border-2 border-transparent p-1'
)}
>
<FileThumb
className={clsx(
'border-app-line max-h-full w-auto max-w-full overflow-hidden rounded-sm border-2 object-cover shadow shadow-black/40',
isVid && 'rounded border-x-0 border-y-[7px] !border-black'
)}
data={data}
kind={ObjectKind[objectData?.kind || 0]}
size={getExplorerStore().gridItemSize}
/>
{item.extension && isVid && (
<div className="absolute bottom-4 right-2 rounded bg-black/60 py-0.5 px-1 text-[9px] font-semibold uppercase opacity-70">
{item.extension}
</div>
)}
</div>
</div> </div>
<NameArea> <NameArea>
<span className={nameContainerStyles({ selected })}> <span className={nameContainerStyles({ selected })}>
@ -82,7 +66,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
</span> </span>
</NameArea> </NameArea>
</div> </div>
</FileItemContextMenu> </ExplorerItemContextMenu>
); );
} }

View file

@ -1,7 +1,14 @@
import byteSize from 'byte-size';
import clsx from 'clsx'; import clsx from 'clsx';
import dayjs from 'dayjs';
import { HTMLAttributes } from 'react'; import { HTMLAttributes } from 'react';
import { ExplorerItem } from '@sd/client'; import { ExplorerItem, ObjectKind, isObject, isPath } from '@sd/client';
import FileThumb from './FileThumb'; import { getExplorerStore } from '../../hooks/useExplorerStore';
import { ExplorerItemContextMenu } from './ExplorerContextMenu';
import { ColumnKey, columns } from './FileColumns';
import { FileThumb } from './FileThumb';
import { InfoPill } from './Inspector';
import { getExplorerItemData } from './util';
interface Props extends HTMLAttributes<HTMLDivElement> { interface Props extends HTMLAttributes<HTMLDivElement> {
data: ExplorerItem; data: ExplorerItem;
@ -11,6 +18,7 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
function FileRow({ data, index, selected, ...props }: Props) { function FileRow({ data, index, selected, ...props }: Props) {
return ( return (
<ExplorerItemContextMenu className="w-full" data={data}>
<div <div
{...props} {...props}
className={clsx( className={clsx(
@ -29,6 +37,7 @@ function FileRow({ data, index, selected, ...props }: Props) {
</div> </div>
))} ))}
</div> </div>
</ExplorerItemContextMenu>
); );
} }
@ -36,32 +45,44 @@ const RenderCell: React.FC<{
colKey: ColumnKey; colKey: ColumnKey;
data: ExplorerItem; data: ExplorerItem;
}> = ({ colKey, data }) => { }> = ({ colKey, data }) => {
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
const { cas_id } = getExplorerItemData(data);
switch (colKey) { switch (colKey) {
case 'name': case 'name':
return ( return (
<div className="flex flex-row items-center overflow-hidden"> <div className="flex flex-row items-center overflow-hidden">
<div className="mr-3 flex h-6 w-6 shrink-0 items-center justify-center"> <div className="mr-3 flex h-6 w-12 shrink-0 items-center justify-center">
<FileThumb data={data} size={0} /> <FileThumb data={data} size={35} />
</div> </div>
{/* {colKey == 'name' && <span className="truncate text-xs">
(() => { {data.item.name}
switch (row.extension.toLowerCase()) { {data.item.extension && `.${data.item.extension}`}
case 'mov' || 'mp4': </span>
return <FilmIcon className="flex-shrink-0 w-5 h-5 mr-3 text-gray-300" />;
default:
if (row.is_dir)
return <FolderIcon className="flex-shrink-0 w-5 h-5 mr-3 text-gray-300" />;
return <DocumentIcon className="flex-shrink-0 w-5 h-5 mr-3 text-gray-300" />;
}
})()} */}
<span className="truncate text-xs">{data.item[colKey]}</span>
</div> </div>
); );
// case 'size_in_bytes': case 'size':
// return <span className="text-xs text-left">{byteSize(Number(value || 0))}</span>; return (
<span className="text-ink-dull text-left text-xs font-medium">
{byteSize(Number(objectData?.size_in_bytes || 0)).toString()}
</span>
);
case 'date_created':
return (
<span className="text-ink-dull text-left text-xs font-medium">
{dayjs(data.item?.date_created).format('MMM Do YYYY')}
</span>
);
case 'cas_id':
return <span className="text-ink-dull truncate text-left text-xs font-medium">{cas_id}</span>;
case 'extension': case 'extension':
return <span className="text-left text-xs">{data.item[colKey]}</span>; return (
<div className="flex flex-row items-center space-x-3">
<InfoPill className="bg-app-button/50">
{isPath(data) && data.item.is_dir ? 'Folder' : ObjectKind[objectData?.kind || 0]}
</InfoPill>
</div>
);
// case 'meta_integrity_hash': // case 'meta_integrity_hash':
// return <span className="truncate">{value}</span>; // return <span className="truncate">{value}</span>;
// case 'tags': // case 'tags':
@ -72,23 +93,4 @@ const RenderCell: React.FC<{
} }
}; };
interface IColumn {
column: string;
key: string;
width: number;
}
// Function ensure no types are lost, but guarantees that they are Column[]
function ensureIsColumns<T extends IColumn[]>(data: T) {
return data;
}
const columns = ensureIsColumns([
{ column: 'Name', key: 'name', width: 280 } as const,
// { column: 'Size', key: 'size_in_bytes', width: 120 } as const,
{ column: 'Type', key: 'extension', width: 100 } as const
]);
type ColumnKey = (typeof columns)[number]['key'];
export default FileRow; export default FileRow;

View file

@ -6,49 +6,101 @@ import Executable from '@sd/assets/images/Executable.png';
import File from '@sd/assets/images/File.png'; import File from '@sd/assets/images/File.png';
import Video from '@sd/assets/images/Video.png'; import Video from '@sd/assets/images/Video.png';
import clsx from 'clsx'; import clsx from 'clsx';
import { ExplorerItem, isObject, isPath } from '@sd/client'; import { CSSProperties } from 'react';
import { useExplorerStore } from '~/hooks/useExplorerStore'; import { ExplorerItem } from '@sd/client';
import { usePlatform } from '~/util/Platform'; import { usePlatform } from '~/util/Platform';
import { Folder } from '../icons/Folder'; import { Folder } from '../icons/Folder';
import { getExplorerItemData } from './util';
interface Props { // const icons = import.meta.glob('../../../../assets/icons/*.svg');
interface FileItemProps {
data: ExplorerItem; data: ExplorerItem;
size: number; size: number;
className?: string; className?: string;
style?: React.CSSProperties;
iconClassNames?: string;
kind?: string;
} }
// const icons = import.meta.glob('../../../../assets/icons/*.svg'); export function FileThumb({ data, size, className }: FileItemProps) {
const { cas_id, isDir, kind, hasThumbnail, extension } = getExplorerItemData(data);
export default function FileThumb({ data, ...props }: Props) { // 10 percent of the size
const videoBarsHeight = Math.floor(size / 10);
// calculate 16:9 ratio for height from size
const videoHeight = Math.floor((size * 9) / 16) + videoBarsHeight * 2;
return (
<div
className={clsx(
'relative flex h-full shrink-0 items-center justify-center border-2 border-transparent',
className
)}
>
<FileThumbImg
size={size}
hasThumbnail={hasThumbnail}
isDir={isDir}
cas_id={cas_id}
extension={extension}
kind={kind}
imgClassName={clsx(
hasThumbnail &&
'max-h-full w-auto max-w-full rounded-sm object-cover shadow shadow-black/30',
kind === 'Image' && size > 60 && 'border-app-line border-2',
kind === 'Video' && 'rounded border-x-0 !border-black'
)}
imgStyle={
kind === 'Video'
? {
borderTopWidth: videoBarsHeight,
borderBottomWidth: videoBarsHeight,
width: size,
height: videoHeight
}
: {}
}
/>
{extension && kind === 'Video' && size > 80 && (
<div className="absolute bottom-[22%] right-2 rounded bg-black/60 py-0.5 px-1 text-[9px] font-semibold uppercase opacity-70">
{extension}
</div>
)}
</div>
);
}
interface FileThumbImgProps {
isDir: boolean;
cas_id: string | null;
kind: string | null;
extension: string | null;
size: number;
hasThumbnail: boolean;
imgClassName?: string;
imgStyle?: CSSProperties;
}
export function FileThumbImg({
isDir,
cas_id,
kind,
size,
hasThumbnail,
extension,
imgClassName,
imgStyle
}: FileThumbImgProps) {
const platform = usePlatform(); const platform = usePlatform();
// const Icon = useMemo(() => {
// const icon = icons[`../../../../assets/icons/${item.extension}.svg`];
// const Icon = icon if (isDir) return <Folder size={size * 0.7} />;
// ? lazy(() => icon().then((v) => ({ default: (v as any).ReactComponent })))
// : undefined;
// return Icon;
// }, [item.extension]);
if (isPath(data) && data.item.is_dir) return <Folder size={props.size * 0.7} />;
if (data.has_thumbnail) {
const cas_id = isObject(data) ? data.item.file_paths[0]?.cas_id : data.item.cas_id;
if (!cas_id) return <div></div>; if (!cas_id) return <div></div>;
const url = platform.getThumbnailUrlById(cas_id); const url = platform.getThumbnailUrlById(cas_id);
if (url) if (url && hasThumbnail) {
return ( return (
<img <img
style={props.style} style={{ ...imgStyle, maxWidth: size, width: size - 10 }}
decoding="async" decoding="async"
// width={props.size} className={clsx('z-90 pointer-events-none bg-black', imgClassName)}
className={clsx('z-90 pointer-events-none', props.className)}
src={url} src={url}
/> />
); );
@ -56,12 +108,12 @@ export default function FileThumb({ data, ...props }: Props) {
let icon = File; let icon = File;
// Hacky (and temporary) way to integrate thumbnails // Hacky (and temporary) way to integrate thumbnails
if (props.kind === 'Archive') icon = Archive; if (kind === 'Archive') icon = Archive;
else if (props.kind === 'Video') icon = Video; else if (kind === 'Video') icon = Video;
else if (props.kind === 'Document' && data.item.extension === 'pdf') icon = DocumentPdf; else if (kind === 'Document' && extension === 'pdf') icon = DocumentPdf;
else if (props.kind === 'Executable') icon = Executable; else if (kind === 'Executable') icon = Executable;
else if (props.kind === 'Encrypted') icon = Encrypted; else if (kind === 'Encrypted') icon = Encrypted;
else if (props.kind === 'Compressed') icon = Compressed; else if (kind === 'Compressed') icon = Compressed;
return <img src={icon} className={clsx('h-full overflow-hidden', props.iconClassNames)} />; return <img src={icon} className={clsx('h-full overflow-hidden')} />;
} }

View file

@ -14,7 +14,7 @@ import {
import { Button, tw } from '@sd/ui'; import { Button, tw } from '@sd/ui';
import { DefaultProps } from '../primitive/types'; import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip'; import { Tooltip } from '../tooltip/Tooltip';
import FileThumb from './FileThumb'; import { FileThumb } from './FileThumb';
import { Divider } from './inspector/Divider'; import { Divider } from './inspector/Divider';
import FavoriteButton from './inspector/FavoriteButton'; import FavoriteButton from './inspector/FavoriteButton';
import Note from './inspector/Note'; import Note from './inspector/Note';
@ -80,17 +80,10 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
<> <>
<div <div
className={clsx( className={clsx(
'mb-[10px] flex h-52 w-full items-center justify-center overflow-hidden rounded-lg', 'mb-[10px] flex h-52 w-full items-center justify-center overflow-hidden rounded-lg'
objectData?.kind === 7 && objectData?.has_thumbnail && 'bg-black'
)} )}
> >
<FileThumb <FileThumb size={240} data={data} />
iconClassNames="my-3 max-h-[150px]"
size={230}
kind={ObjectKind[objectData?.kind || 0]}
className="flex shrink grow-0"
data={data}
/>
</div> </div>
<div className="bg-app-box shadow-app-shade/10 border-app-line flex w-full select-text flex-col overflow-hidden rounded-lg border py-0.5"> <div className="bg-app-box shadow-app-shade/10 border-app-line flex w-full select-text flex-col overflow-hidden rounded-lg border py-0.5">
<h3 className="truncate px-3 pt-2 pb-1 text-base font-bold"> <h3 className="truncate px-3 pt-2 pb-1 text-base font-bold">
@ -126,7 +119,7 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
<MetaContainer> <MetaContainer>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
<InfoPill>{isDir ? 'Folder' : ObjectKind[objectData?.kind || 0]}</InfoPill> <InfoPill>{isDir ? 'Folder' : ObjectKind[objectData?.kind || 0]}</InfoPill>
{item && <InfoPill>{item.extension}</InfoPill>} {item?.extension && <InfoPill>{item.extension}</InfoPill>}
{tags?.data?.map((tag) => ( {tags?.data?.map((tag) => (
<InfoPill <InfoPill
className="!text-white" className="!text-white"

View file

@ -4,11 +4,12 @@ import { useSearchParams } from 'react-router-dom';
import { useKey, useOnWindowResize } from 'rooks'; import { useKey, useOnWindowResize } from 'rooks';
import { ExplorerContext, ExplorerItem, isPath } from '@sd/client'; import { ExplorerContext, ExplorerItem, isPath } from '@sd/client';
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
import { LIST_VIEW_HEADER_HEIGHT, ListViewHeader } from './FileColumns';
import FileItem from './FileItem'; import FileItem from './FileItem';
import FileRow from './FileRow'; import FileRow from './FileRow';
const TOP_BAR_HEIGHT = 46; const TOP_BAR_HEIGHT = 46;
const GRID_TEXT_AREA_HEIGHT = 25; // const GRID_TEXT_AREA_HEIGHT = 25;
interface Props { interface Props {
context: ExplorerContext; context: ExplorerContext;
@ -36,7 +37,8 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
}, [explorerStore.showInspector]); }, [explorerStore.showInspector]);
// sizing calculations // sizing calculations
const amountOfColumns = Math.floor(width / explorerStore.gridItemSize) || 8, const GRID_TEXT_AREA_HEIGHT = explorerStore.gridItemSize / 4;
const amountOfColumns = Math.floor(width / explorerStore.gridItemSize) || 4,
amountOfRows = amountOfRows =
explorerStore.layoutMode === 'grid' ? Math.ceil(data.length / amountOfColumns) : data.length, explorerStore.layoutMode === 'grid' ? Math.ceil(data.length / amountOfColumns) : data.length,
itemSize = itemSize =
@ -92,28 +94,6 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
getExplorerStore().selectedRowIndex = explorerStore.selectedRowIndex + 1; getExplorerStore().selectedRowIndex = explorerStore.selectedRowIndex + 1;
}); });
// const Header = () => (
// <div>
// {props.context.name && (
// <h1 className="pt-20 pl-4 text-xl font-bold ">{props.context.name}</h1>
// )}
// <div className="table-head">
// <div className="flex flex-row p-2 table-head-row">
// {columns.map((col) => (
// <div
// key={col.key}
// className="relative flex flex-row items-center pl-2 table-head-cell group"
// style={{ width: col.width }}
// >
// <EllipsisHorizontalIcon className="absolute hidden w-5 h-5 -ml-5 cursor-move group-hover:block drag-handle opacity-10" />
// <span className="text-sm font-medium text-gray-500">{col.column}</span>
// </div>
// ))}
// </div>
// </div>
// </div>
// );
return ( return (
<div style={{ marginTop: -TOP_BAR_HEIGHT }} className="w-full cursor-default pl-4"> <div style={{ marginTop: -TOP_BAR_HEIGHT }} className="w-full cursor-default pl-4">
<div <div
@ -126,11 +106,12 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
<div <div
ref={innerRef} ref={innerRef}
style={{ style={{
height: `${rowVirtualizer.getTotalSize()}px`, height: rowVirtualizer.getTotalSize(),
marginTop: `${TOP_BAR_HEIGHT}px` marginTop: TOP_BAR_HEIGHT + LIST_VIEW_HEADER_HEIGHT
}} }}
className="relative w-full" className="relative w-full"
> >
<ListViewHeader />
{rowVirtualizer.getVirtualItems().map((virtualRow) => ( {rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div <div
style={{ style={{
@ -140,21 +121,21 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
className="absolute top-0 left-0 flex w-full" className="absolute top-0 left-0 flex w-full"
key={virtualRow.key} key={virtualRow.key}
> >
{explorerStore.layoutMode === 'list' ? ( {explorerStore.layoutMode === 'list' && (
<WrappedItem <WrappedItem
kind="list" kind="list"
isSelected={getExplorerStore().selectedRowIndex === virtualRow.index} isSelected={explorerStore.selectedRowIndex === virtualRow.index}
index={virtualRow.index} index={virtualRow.index}
item={data[virtualRow.index]!} item={data[virtualRow.index]!}
/> />
) : ( )}
{explorerStore.layoutMode === 'grid' &&
[...Array(amountOfColumns)].map((_, i) => { [...Array(amountOfColumns)].map((_, i) => {
const index = virtualRow.index * amountOfColumns + i; const index = virtualRow.index * amountOfColumns + i;
const item = data[index]; const item = data[index];
const isSelected = explorerStore.selectedRowIndex === index; const isSelected = explorerStore.selectedRowIndex === index;
return ( return (
<div key={index} className=""> <div key={index} className="flex">
<div className="flex">
{item && ( {item && (
<WrappedItem <WrappedItem
kind="grid" kind="grid"
@ -164,10 +145,8 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
/> />
)} )}
</div> </div>
</div>
); );
}) })}
)}
</div> </div>
))} ))}
</div> </div>
@ -194,7 +173,6 @@ const WrappedItem = memo(({ item, index, isSelected, kind }: WrappedItemProps) =
const onClick = useCallback( const onClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => { (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
getExplorerStore().selectedRowIndex = isSelected ? -1 : index; getExplorerStore().selectedRowIndex = isSelected ? -1 : index;
}, },
[isSelected, index] [isSelected, index]

View file

@ -0,0 +1,13 @@
import { ExplorerItem, ObjectKind, isObject, isPath } from '@sd/client';
export function getExplorerItemData(data: ExplorerItem) {
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
return {
cas_id: (isObject(data) ? data.item.file_paths[0]?.cas_id : data.item.cas_id) || null,
isDir: isPath(data) && data.item.is_dir,
kind: ObjectKind[objectData?.kind || 0] || null,
hasThumbnail: data.has_thumbnail,
extension: data.item.extension
};
}

View file

@ -52,11 +52,11 @@ const explorerStore = proxy({
}); });
export function useExplorerStore() { export function useExplorerStore() {
const { library } = useLibraryContext(); // const { library } = useLibraryContext();
useEffect(() => { // useEffect(() => {
explorerStore.reset(); // explorerStore.reset();
}, [library.uuid]); // }, [library.uuid]);
return useSnapshot(explorerStore); return useSnapshot(explorerStore);
} }