mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 07:12:49 +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()
|
.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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,24 +18,26 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
|
||||||
function FileRow({ data, index, selected, ...props }: Props) {
|
function FileRow({ data, index, selected, ...props }: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<ExplorerItemContextMenu className="w-full" data={data}>
|
||||||
{...props}
|
<div
|
||||||
className={clsx(
|
{...props}
|
||||||
'table-body-row mr-2 flex w-full flex-row rounded-lg border-2',
|
className={clsx(
|
||||||
selected ? 'border-accent' : 'border-transparent',
|
'table-body-row mr-2 flex w-full flex-row rounded-lg border-2',
|
||||||
index % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
|
selected ? 'border-accent' : 'border-transparent',
|
||||||
)}
|
index % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
|
||||||
>
|
)}
|
||||||
{columns.map((col) => (
|
>
|
||||||
<div
|
{columns.map((col) => (
|
||||||
key={col.key}
|
<div
|
||||||
className="table-body-cell flex items-center px-4 py-2 pr-2"
|
key={col.key}
|
||||||
style={{ width: col.width }}
|
className="table-body-cell flex items-center px-4 py-2 pr-2"
|
||||||
>
|
style={{ width: col.width }}
|
||||||
<RenderCell data={data} colKey={col.key} />
|
>
|
||||||
</div>
|
<RenderCell data={data} colKey={col.key} />
|
||||||
))}
|
</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;
|
||||||
|
|
|
@ -6,62 +6,114 @@ 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 (!cas_id) return <div></div>;
|
||||||
|
const url = platform.getThumbnailUrlById(cas_id);
|
||||||
|
|
||||||
if (data.has_thumbnail) {
|
if (url && hasThumbnail) {
|
||||||
const cas_id = isObject(data) ? data.item.file_paths[0]?.cas_id : data.item.cas_id;
|
return (
|
||||||
|
<img
|
||||||
if (!cas_id) return <div></div>;
|
style={{ ...imgStyle, maxWidth: size, width: size - 10 }}
|
||||||
|
decoding="async"
|
||||||
const url = platform.getThumbnailUrlById(cas_id);
|
className={clsx('z-90 pointer-events-none bg-black', imgClassName)}
|
||||||
|
src={url}
|
||||||
if (url)
|
/>
|
||||||
return (
|
);
|
||||||
<img
|
|
||||||
style={props.style}
|
|
||||||
decoding="async"
|
|
||||||
// width={props.size}
|
|
||||||
className={clsx('z-90 pointer-events-none', props.className)}
|
|
||||||
src={url}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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')} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,34 +121,32 @@ 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"
|
isSelected={isSelected}
|
||||||
isSelected={isSelected}
|
index={index}
|
||||||
index={index}
|
item={item}
|
||||||
item={item}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</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]
|
||||||
|
|
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() {
|
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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue