mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-05 09:13:28 +00:00
[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:
parent
0b9005fdef
commit
677e1b63e9
|
@ -118,18 +118,6 @@ pub(crate) fn mount() -> impl RouterBuilderLike<Ctx> {
|
|||
.exec()
|
||||
.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());
|
||||
|
||||
for file_path in file_paths {
|
||||
|
@ -223,7 +211,7 @@ pub(crate) fn mount() -> impl RouterBuilderLike<Ctx> {
|
|||
|
||||
async_stream::stream! {
|
||||
let online = location_manager.get_online().await;
|
||||
dbg!(&online);
|
||||
// dbg!(&online);
|
||||
yield online;
|
||||
|
||||
while let Ok(locations) = rx.recv().await {
|
||||
|
|
|
@ -9,7 +9,7 @@ use tokio::fs::File;
|
|||
|
||||
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;
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FileDecryptorJobState {}
|
||||
|
@ -74,7 +74,7 @@ impl StatefulJob for FileDecryptorJob {
|
|||
|| {
|
||||
let mut path = info.fs_path.clone();
|
||||
let extension = path.extension().map_or("decrypted", |ext| {
|
||||
if ext == BYTES {
|
||||
if ext == BYTES_EXT {
|
||||
""
|
||||
} else {
|
||||
"decrypted"
|
||||
|
|
|
@ -15,7 +15,7 @@ use specta::Type;
|
|||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
use tracing::warn;
|
||||
|
||||
use super::{context_menu_fs_info, FsInfo};
|
||||
use super::{context_menu_fs_info, FsInfo, BYTES_EXT};
|
||||
|
||||
pub struct FileEncryptorJob;
|
||||
|
||||
|
@ -108,7 +108,7 @@ impl StatefulJob for FileEncryptorJob {
|
|||
"path contents when converted to string",
|
||||
),
|
||||
})?
|
||||
.to_string() + ".bytes",
|
||||
.to_string() + BYTES_EXT,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -30,6 +30,8 @@ pub enum ObjectType {
|
|||
Directory,
|
||||
}
|
||||
|
||||
pub const BYTES_EXT: &str = ".bytes";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FsInfo {
|
||||
pub path_data: file_path_with_object::Data,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import {
|
||||
ArrowBendUpRight,
|
||||
Clipboard,
|
||||
|
@ -209,11 +210,16 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
|
|||
);
|
||||
}
|
||||
|
||||
export interface FileItemContextMenuProps extends PropsWithChildren {
|
||||
export interface ExplorerItemContextMenuProps extends PropsWithChildren {
|
||||
data: ExplorerItem;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps) {
|
||||
export function ExplorerItemContextMenu({
|
||||
data,
|
||||
className,
|
||||
...props
|
||||
}: ExplorerItemContextMenuProps) {
|
||||
const { library } = useLibraryContext();
|
||||
const store = useExplorerStore();
|
||||
const params = useExplorerParams();
|
||||
|
@ -227,7 +233,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
|||
const copyFiles = useLibraryMutation('files.copyFiles');
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={clsx('relative', className)}>
|
||||
<CM.ContextMenu trigger={props.children}>
|
||||
<CM.Item
|
||||
label="Open"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { PropsWithChildren, useState } from 'react';
|
||||
import { Select, SelectOption } from '@sd/ui';
|
||||
import { getExplorerStore, useExplorerStore } from '../../hooks/useExplorerStore';
|
||||
import Slider from '../primitive/Slider';
|
||||
|
||||
function Heading({ children }: PropsWithChildren) {
|
||||
|
@ -22,13 +23,23 @@ const sortOptions = {
|
|||
export function ExplorerOptionsPanel() {
|
||||
const [sortBy, setSortBy] = useState('name');
|
||||
const [stackBy, setStackBy] = useState('kind');
|
||||
const [size, setSize] = useState([50]);
|
||||
|
||||
const explorerStore = useExplorerStore();
|
||||
|
||||
return (
|
||||
<div className="p-4 ">
|
||||
{/* <Heading>Explorer Appearance</Heading> */}
|
||||
<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="flex flex-col">
|
||||
<SubHeading>Sort by</SubHeading>
|
||||
|
|
|
@ -37,7 +37,7 @@ export interface TopBarButtonProps {
|
|||
// export const TopBarIcon = (icon: any) => tw(icon)`m-0.5 w-5 h-5 text-ink-dull`;
|
||||
|
||||
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: {
|
||||
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>(
|
||||
({ active, rounding, className, ...props }, ref) => {
|
||||
return (
|
||||
<Button {...props} ref={ref} className={topBarButtonStyle({ active, rounding, className })}>
|
||||
<Button
|
||||
// size="sm"
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={topBarButtonStyle({ active, rounding, className })}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
|
|
42
packages/interface/src/components/explorer/FileColumns.tsx
Normal file
42
packages/interface/src/components/explorer/FileColumns.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -2,9 +2,9 @@ import clsx from 'clsx';
|
|||
import { HTMLAttributes } from 'react';
|
||||
import { ExplorerItem, ObjectKind, isObject } from '@sd/client';
|
||||
import { cva, tw } from '@sd/ui';
|
||||
import { getExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { FileItemContextMenu } from './ExplorerContextMenu';
|
||||
import FileThumb from './FileThumb';
|
||||
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { ExplorerItemContextMenu } from './ExplorerContextMenu';
|
||||
import { FileThumb } from './FileThumb';
|
||||
|
||||
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 item = data.item;
|
||||
|
||||
const explorerStore = useExplorerStore();
|
||||
|
||||
return (
|
||||
<FileItemContextMenu data={data}>
|
||||
<ExplorerItemContextMenu data={data}>
|
||||
<div
|
||||
onContextMenu={(e) => {
|
||||
if (index != undefined) {
|
||||
|
@ -40,40 +42,22 @@ function FileItem({ data, selected, index, ...rest }: Props) {
|
|||
}}
|
||||
{...rest}
|
||||
draggable
|
||||
className={clsx('mb-3 inline-block w-[100px]', rest.className)}
|
||||
style={{ width: explorerStore.gridItemSize }}
|
||||
className={clsx('mb-3 inline-block', rest.className)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: getExplorerStore().gridItemSize,
|
||||
height: getExplorerStore().gridItemSize
|
||||
width: explorerStore.gridItemSize,
|
||||
height: explorerStore.gridItemSize
|
||||
}}
|
||||
className={clsx(
|
||||
'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
|
||||
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>
|
||||
<FileThumb data={data} size={explorerStore.gridItemSize} />
|
||||
</div>
|
||||
<NameArea>
|
||||
<span className={nameContainerStyles({ selected })}>
|
||||
|
@ -82,7 +66,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
|
|||
</span>
|
||||
</NameArea>
|
||||
</div>
|
||||
</FileItemContextMenu>
|
||||
</ExplorerItemContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import FileThumb from './FileThumb';
|
||||
import { ExplorerItem, ObjectKind, isObject, isPath } from '@sd/client';
|
||||
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> {
|
||||
data: ExplorerItem;
|
||||
|
@ -11,24 +18,26 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|||
|
||||
function FileRow({ data, index, selected, ...props }: Props) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(
|
||||
'table-body-row mr-2 flex w-full flex-row rounded-lg border-2',
|
||||
selected ? 'border-accent' : 'border-transparent',
|
||||
index % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
|
||||
)}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="table-body-cell flex items-center px-4 py-2 pr-2"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<RenderCell data={data} colKey={col.key} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ExplorerItemContextMenu className="w-full" data={data}>
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(
|
||||
'table-body-row mr-2 flex w-full flex-row rounded-lg border-2',
|
||||
selected ? 'border-accent' : 'border-transparent',
|
||||
index % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
|
||||
)}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="table-body-cell flex items-center px-4 py-2 pr-2"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<RenderCell data={data} colKey={col.key} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ExplorerItemContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -36,32 +45,44 @@ const RenderCell: React.FC<{
|
|||
colKey: ColumnKey;
|
||||
data: ExplorerItem;
|
||||
}> = ({ colKey, data }) => {
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
const { cas_id } = getExplorerItemData(data);
|
||||
|
||||
switch (colKey) {
|
||||
case 'name':
|
||||
return (
|
||||
<div className="flex flex-row items-center overflow-hidden">
|
||||
<div className="mr-3 flex h-6 w-6 shrink-0 items-center justify-center">
|
||||
<FileThumb data={data} size={0} />
|
||||
<div className="mr-3 flex h-6 w-12 shrink-0 items-center justify-center">
|
||||
<FileThumb data={data} size={35} />
|
||||
</div>
|
||||
{/* {colKey == 'name' &&
|
||||
(() => {
|
||||
switch (row.extension.toLowerCase()) {
|
||||
case 'mov' || 'mp4':
|
||||
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>
|
||||
<span className="truncate text-xs">
|
||||
{data.item.name}
|
||||
{data.item.extension && `.${data.item.extension}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
// case 'size_in_bytes':
|
||||
// return <span className="text-xs text-left">{byteSize(Number(value || 0))}</span>;
|
||||
case 'size':
|
||||
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':
|
||||
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':
|
||||
// return <span className="truncate">{value}</span>;
|
||||
// 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;
|
||||
|
|
|
@ -6,62 +6,114 @@ import Executable from '@sd/assets/images/Executable.png';
|
|||
import File from '@sd/assets/images/File.png';
|
||||
import Video from '@sd/assets/images/Video.png';
|
||||
import clsx from 'clsx';
|
||||
import { ExplorerItem, isObject, isPath } from '@sd/client';
|
||||
import { useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { CSSProperties } from 'react';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { Folder } from '../icons/Folder';
|
||||
import { getExplorerItemData } from './util';
|
||||
|
||||
interface Props {
|
||||
// const icons = import.meta.glob('../../../../assets/icons/*.svg');
|
||||
interface FileItemProps {
|
||||
data: ExplorerItem;
|
||||
size: number;
|
||||
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 Icon = useMemo(() => {
|
||||
// const icon = icons[`../../../../assets/icons/${item.extension}.svg`];
|
||||
|
||||
// const Icon = icon
|
||||
// ? lazy(() => icon().then((v) => ({ default: (v as any).ReactComponent })))
|
||||
// : undefined;
|
||||
// return Icon;
|
||||
// }, [item.extension]);
|
||||
if (isDir) return <Folder size={size * 0.7} />;
|
||||
|
||||
if (isPath(data) && data.item.is_dir) return <Folder size={props.size * 0.7} />;
|
||||
if (!cas_id) return <div></div>;
|
||||
const url = platform.getThumbnailUrlById(cas_id);
|
||||
|
||||
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>;
|
||||
|
||||
const url = platform.getThumbnailUrlById(cas_id);
|
||||
|
||||
if (url)
|
||||
return (
|
||||
<img
|
||||
style={props.style}
|
||||
decoding="async"
|
||||
// width={props.size}
|
||||
className={clsx('z-90 pointer-events-none', props.className)}
|
||||
src={url}
|
||||
/>
|
||||
);
|
||||
if (url && hasThumbnail) {
|
||||
return (
|
||||
<img
|
||||
style={{ ...imgStyle, maxWidth: size, width: size - 10 }}
|
||||
decoding="async"
|
||||
className={clsx('z-90 pointer-events-none bg-black', imgClassName)}
|
||||
src={url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let icon = File;
|
||||
// Hacky (and temporary) way to integrate thumbnails
|
||||
if (props.kind === 'Archive') icon = Archive;
|
||||
else if (props.kind === 'Video') icon = Video;
|
||||
else if (props.kind === 'Document' && data.item.extension === 'pdf') icon = DocumentPdf;
|
||||
else if (props.kind === 'Executable') icon = Executable;
|
||||
else if (props.kind === 'Encrypted') icon = Encrypted;
|
||||
else if (props.kind === 'Compressed') icon = Compressed;
|
||||
if (kind === 'Archive') icon = Archive;
|
||||
else if (kind === 'Video') icon = Video;
|
||||
else if (kind === 'Document' && extension === 'pdf') icon = DocumentPdf;
|
||||
else if (kind === 'Executable') icon = Executable;
|
||||
else if (kind === 'Encrypted') icon = Encrypted;
|
||||
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')} />;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { Button, tw } from '@sd/ui';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
import FileThumb from './FileThumb';
|
||||
import { FileThumb } from './FileThumb';
|
||||
import { Divider } from './inspector/Divider';
|
||||
import FavoriteButton from './inspector/FavoriteButton';
|
||||
import Note from './inspector/Note';
|
||||
|
@ -80,17 +80,10 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
|
|||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'mb-[10px] flex h-52 w-full items-center justify-center overflow-hidden rounded-lg',
|
||||
objectData?.kind === 7 && objectData?.has_thumbnail && 'bg-black'
|
||||
'mb-[10px] flex h-52 w-full items-center justify-center overflow-hidden rounded-lg'
|
||||
)}
|
||||
>
|
||||
<FileThumb
|
||||
iconClassNames="my-3 max-h-[150px]"
|
||||
size={230}
|
||||
kind={ObjectKind[objectData?.kind || 0]}
|
||||
className="flex shrink grow-0"
|
||||
data={data}
|
||||
/>
|
||||
<FileThumb size={240} data={data} />
|
||||
</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">
|
||||
<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>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<InfoPill>{isDir ? 'Folder' : ObjectKind[objectData?.kind || 0]}</InfoPill>
|
||||
{item && <InfoPill>{item.extension}</InfoPill>}
|
||||
{item?.extension && <InfoPill>{item.extension}</InfoPill>}
|
||||
{tags?.data?.map((tag) => (
|
||||
<InfoPill
|
||||
className="!text-white"
|
||||
|
|
|
@ -4,11 +4,12 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import { useKey, useOnWindowResize } from 'rooks';
|
||||
import { ExplorerContext, ExplorerItem, isPath } from '@sd/client';
|
||||
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { LIST_VIEW_HEADER_HEIGHT, ListViewHeader } from './FileColumns';
|
||||
import FileItem from './FileItem';
|
||||
import FileRow from './FileRow';
|
||||
|
||||
const TOP_BAR_HEIGHT = 46;
|
||||
const GRID_TEXT_AREA_HEIGHT = 25;
|
||||
// const GRID_TEXT_AREA_HEIGHT = 25;
|
||||
|
||||
interface Props {
|
||||
context: ExplorerContext;
|
||||
|
@ -36,7 +37,8 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
|
|||
}, [explorerStore.showInspector]);
|
||||
|
||||
// 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 =
|
||||
explorerStore.layoutMode === 'grid' ? Math.ceil(data.length / amountOfColumns) : data.length,
|
||||
itemSize =
|
||||
|
@ -92,28 +94,6 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
|
|||
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 (
|
||||
<div style={{ marginTop: -TOP_BAR_HEIGHT }} className="w-full cursor-default pl-4">
|
||||
<div
|
||||
|
@ -126,11 +106,12 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
|
|||
<div
|
||||
ref={innerRef}
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
marginTop: `${TOP_BAR_HEIGHT}px`
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
marginTop: TOP_BAR_HEIGHT + LIST_VIEW_HEADER_HEIGHT
|
||||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
<ListViewHeader />
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
|
||||
<div
|
||||
style={{
|
||||
|
@ -140,34 +121,32 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
|
|||
className="absolute top-0 left-0 flex w-full"
|
||||
key={virtualRow.key}
|
||||
>
|
||||
{explorerStore.layoutMode === 'list' ? (
|
||||
{explorerStore.layoutMode === 'list' && (
|
||||
<WrappedItem
|
||||
kind="list"
|
||||
isSelected={getExplorerStore().selectedRowIndex === virtualRow.index}
|
||||
isSelected={explorerStore.selectedRowIndex === virtualRow.index}
|
||||
index={virtualRow.index}
|
||||
item={data[virtualRow.index]!}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{explorerStore.layoutMode === 'grid' &&
|
||||
[...Array(amountOfColumns)].map((_, i) => {
|
||||
const index = virtualRow.index * amountOfColumns + i;
|
||||
const item = data[index];
|
||||
const isSelected = explorerStore.selectedRowIndex === index;
|
||||
return (
|
||||
<div key={index} className="">
|
||||
<div className="flex">
|
||||
{item && (
|
||||
<WrappedItem
|
||||
kind="grid"
|
||||
isSelected={isSelected}
|
||||
index={index}
|
||||
item={item}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div key={index} className="flex">
|
||||
{item && (
|
||||
<WrappedItem
|
||||
kind="grid"
|
||||
isSelected={isSelected}
|
||||
index={index}
|
||||
item={item}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -194,7 +173,6 @@ const WrappedItem = memo(({ item, index, isSelected, kind }: WrappedItemProps) =
|
|||
const onClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
getExplorerStore().selectedRowIndex = isSelected ? -1 : index;
|
||||
},
|
||||
[isSelected, index]
|
||||
|
|
13
packages/interface/src/components/explorer/util.ts
Normal file
13
packages/interface/src/components/explorer/util.ts
Normal 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
|
||||
};
|
||||
}
|
|
@ -52,11 +52,11 @@ const explorerStore = proxy({
|
|||
});
|
||||
|
||||
export function useExplorerStore() {
|
||||
const { library } = useLibraryContext();
|
||||
// const { library } = useLibraryContext();
|
||||
|
||||
useEffect(() => {
|
||||
explorerStore.reset();
|
||||
}, [library.uuid]);
|
||||
// useEffect(() => {
|
||||
// explorerStore.reset();
|
||||
// }, [library.uuid]);
|
||||
|
||||
return useSnapshot(explorerStore);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue