Files
spacedrive/interface/app/$libraryId/Explorer/ExplorerPath.tsx
Jamie Pine fdf31fc3a8 [ENG-1511] Library Screens (#1903)
* init

* changes

* Now updating statistics once a minute

* More robust statistics updater

* Concurrency is hard

* improvements to stats

* refactor

* adjust setting back/forward padding so it matches top bar

* refactor sidebar

* rename

* setting up screens

* some changes

* Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com>

* yes

* yes2

* refactored explorerItem.ts

* important explorer code shouldn't be thrown away in a util moment

* support for multiple thumbnails in ExplorerItem

* clippy

* move debug

* yes

* label filters

* ts

* comment out unconnected stuff

* added .mid for midi files

---------

Co-authored-by: Ericson Fogo Soares <ericson.ds999@gmail.com>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2024-01-16 12:15:03 +00:00

176 lines
5.7 KiB
TypeScript

import { CaretRight } from '@phosphor-icons/react';
import clsx from 'clsx';
import { memo, useMemo } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import { getExplorerItemData, getIndexedItemFilePath, useLibraryQuery } from '@sd/client';
import { Icon } from '~/components';
import { useIsDark, useOperatingSystem } from '~/hooks';
import { useExplorerContext } from './Context';
import { FileThumb } from './FilePath/Thumb';
import { useExplorerDroppable } from './useExplorerDroppable';
import { useExplorerSearchParams } from './util';
export const PATH_BAR_HEIGHT = 32;
export const ExplorerPath = memo(() => {
const os = useOperatingSystem(true);
const navigate = useNavigate();
const [{ path: searchPath }] = useExplorerSearchParams();
const { parent: explorerParent, selectedItems } = useExplorerContext();
const location = explorerParent?.type === 'Location' ? explorerParent.location : undefined;
const selectedItem = useMemo(
() => (selectedItems.size === 1 ? [...selectedItems][0] : undefined),
[selectedItems]
);
const indexedFilePath = selectedItem && getIndexedItemFilePath(selectedItem);
const queryPath = !!indexedFilePath && (!searchPath || !location);
const { data: filePathname } = useLibraryQuery(['files.getPath', indexedFilePath?.id ?? -1], {
enabled: queryPath
});
const paths = useMemo(() => {
const pathSlash = os === 'windows' ? '\\' : '/';
// Replace all slashes with native slashes
// TODO: Fix returned path from query on windows as the location part of the path
// uses "/" instead of "\" -> C:\Users\sd-user\Documents\spacedrive\packages/assets/deps
let _filePathname = filePathname?.replaceAll(/[\\/]/g, pathSlash);
// Remove file name from the path
_filePathname = _filePathname?.slice(0, _filePathname.lastIndexOf(pathSlash) + 1);
const pathname = _filePathname ?? [location?.path, searchPath].filter(Boolean).join('');
const paths = [...(pathname.match(/[^\\/]+/g) ?? [])];
let locationPath = location?.path;
if (!locationPath && indexedFilePath?.materialized_path) {
if (indexedFilePath.materialized_path === '/') locationPath = pathname;
else {
let materializedPath = indexedFilePath.materialized_path;
// Replace all slashes with native slashes
if (os === 'windows') materializedPath = materializedPath.replaceAll('/', '\\');
// Extract location path from pathname
locationPath = pathname.slice(0, pathname.lastIndexOf(materializedPath));
}
}
const locationIndex = (locationPath ?? '').split(pathSlash).filter(Boolean).length - 1;
return paths.map((path, i) => {
const isLocation = locationIndex !== -1 && i >= locationIndex;
const _paths = [
...paths.slice(!isLocation ? 0 : locationIndex + 1, i),
i === locationIndex ? '' : path
];
let pathname = _paths.join(isLocation ? '/' : pathSlash);
// Wrap pathname in slashes if it's a location
if (isLocation) pathname = pathname ? `/${pathname}/` : '/';
// Add slash to the end of the pathname if it's the root of a drive on windows (C: -> C:\)
else if (os === 'windows' && _paths.length === 1) pathname += pathSlash;
// Add slash to the beginning of the ephemeral pathname (Users -> /Users)
else if (os !== 'windows') pathname = `/${pathname}`;
return {
name: path,
pathname,
locationId: isLocation ? indexedFilePath?.location_id ?? location?.id : undefined
};
});
}, [location, indexedFilePath, filePathname, searchPath, os]);
const handleOnClick = ({ pathname, locationId }: (typeof paths)[number]) => {
if (locationId === undefined) {
// TODO: Handle ephemeral volumes
navigate({
pathname: '../ephemeral/0-0',
search: `${createSearchParams({ path: pathname })}`
});
} else {
navigate({
pathname: `../location/${locationId}`,
search: pathname === '/' ? undefined : `${createSearchParams({ path: pathname })}`
});
}
};
return (
<div
className="group absolute inset-x-0 bottom-0 z-50 flex items-center border-t border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-dull backdrop-blur-lg"
style={{ height: PATH_BAR_HEIGHT }}
>
{paths.map((path) => (
<Path
key={path.pathname}
path={path}
onClick={() => handleOnClick(path)}
disabled={path.pathname === (searchPath ?? (location && '/'))}
/>
))}
{selectedItem && (!queryPath || filePathname) && (
<div className="ml-1 flex items-center gap-1">
<FileThumb data={selectedItem} size={16} frame frameClassName="!border" />
<span className="max-w-xs truncate">
{getExplorerItemData(selectedItem).fullName}
</span>
</div>
)}
</div>
);
});
interface PathProps {
path: { name: string; pathname: string; locationId?: number };
onClick: () => void;
disabled: boolean;
}
const Path = ({ path, onClick, disabled }: PathProps) => {
const isDark = useIsDark();
const { setDroppableRef, className, isDroppable } = useExplorerDroppable({
data: {
type: 'location',
path: path.pathname,
data: path.locationId ? { id: path.locationId, path: path.pathname } : undefined
},
allow: ['Path', 'NonIndexedPath', 'Object'],
navigateTo: onClick,
disabled
});
return (
<button
ref={setDroppableRef}
className={clsx(
'group flex items-center gap-1 rounded px-1 py-0.5',
isDroppable && [isDark ? 'bg-app-button/70' : 'bg-app-darkerBox'],
!disabled && [isDark ? 'hover:bg-app-button/70' : 'hover:bg-app-darkerBox'],
className
)}
disabled={disabled}
onClick={onClick}
tabIndex={-1}
>
<Icon name="Folder" size={16} alt="Folder" />
<span className="max-w-xs truncate text-ink-dull">{path.name}</span>
<CaretRight weight="bold" className="text-ink-dull group-last:hidden" size={10} />
</button>
);
};