mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-05 22:03:16 -04:00
[ENG-1142] Path bar shortcut (#1377)
* Path bar shortcut * Add option to popover and tweak UI * increase size of folder icon + frame thumbs * view adjustments * truncate --------- Co-authored-by: nikec <nikec.job@gmail.com>
This commit is contained in:
@@ -42,6 +42,7 @@ export interface ThumbProps {
|
||||
mediaControls?: boolean;
|
||||
pauseVideo?: boolean;
|
||||
className?: string;
|
||||
frameClassName?: string;
|
||||
childClassName?: string | ((type: ThumbType | `${ThumbType}`) => string | undefined);
|
||||
}
|
||||
|
||||
@@ -62,6 +63,7 @@ export const FileThumb = memo((props: ThumbProps) => {
|
||||
const childClassName = 'max-h-full max-w-full object-contain';
|
||||
const frameClassName = clsx(
|
||||
'rounded-sm border-2 border-app-line bg-app-darkBox',
|
||||
props.frameClassName,
|
||||
isDark ? classes.checkers : classes.checkersLight
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RadixCheckbox, Select, SelectOption, Slider, tw, z } from '@sd/ui';
|
||||
import { getExplorerLayoutStore, useExplorerLayoutStore } from '~/../packages/client/src';
|
||||
import { SortOrderSchema } from '~/app/route-schemas';
|
||||
|
||||
import { useExplorerContext } from './Context';
|
||||
@@ -15,6 +16,7 @@ const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`;
|
||||
export default () => {
|
||||
const explorerStore = useExplorerStore();
|
||||
const explorer = useExplorerContext();
|
||||
const layoutStore = useExplorerLayoutStore();
|
||||
|
||||
const settings = explorer.useSettingsSnapshot();
|
||||
|
||||
@@ -117,44 +119,58 @@ export default () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{settings.layoutMode === 'grid' && (
|
||||
<div>
|
||||
<Subheading>Explorer</Subheading>
|
||||
<div className="flex flex-row flex-wrap justify-between gap-1">
|
||||
<RadixCheckbox
|
||||
checked={settings.showBytesInGridView}
|
||||
label="Show Object size"
|
||||
name="showBytesInGridView"
|
||||
checked={layoutStore.showPathBar}
|
||||
label="Show Path Bar"
|
||||
name="showPathBar"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
getExplorerLayoutStore().showPathBar = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
{settings.layoutMode === 'grid' && (
|
||||
<RadixCheckbox
|
||||
checked={settings.showBytesInGridView}
|
||||
label="Show Object size"
|
||||
name="showBytesInGridView"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
|
||||
explorer.settingsStore.showBytesInGridView = value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RadixCheckbox
|
||||
checked={settings.showHiddenFiles}
|
||||
label="Show Hidden Files"
|
||||
name="showHiddenFiles"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
|
||||
explorer.settingsStore.showBytesInGridView = value;
|
||||
explorer.settingsStore.showHiddenFiles = value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RadixCheckbox
|
||||
checked={settings.showHiddenFiles}
|
||||
label="Show Hidden Files"
|
||||
name="showHiddenFiles"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
{settings.layoutMode === 'media' && (
|
||||
<RadixCheckbox
|
||||
checked={settings.mediaAspectSquare}
|
||||
label="Show square thumbnails"
|
||||
name="mediaAspectSquare"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
|
||||
explorer.settingsStore.showHiddenFiles = value;
|
||||
}}
|
||||
/>
|
||||
explorer.settingsStore.mediaAspectSquare = value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settings.layoutMode === 'media' && (
|
||||
<RadixCheckbox
|
||||
checked={settings.mediaAspectSquare}
|
||||
label="Show square thumbnails"
|
||||
name="mediaAspectSquare"
|
||||
onCheckedChange={(value) => {
|
||||
if (typeof value !== 'boolean') return;
|
||||
|
||||
explorer.settingsStore.mediaAspectSquare = value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Subheading>Double click action</Subheading>
|
||||
<Select
|
||||
|
||||
@@ -3,14 +3,16 @@ import { getIcon } from '@sd/assets/util';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { ExplorerItem, getExplorerLayoutStore, useExplorerLayoutStore } from '@sd/client';
|
||||
import { SearchParamsSchema } from '~/app/route-schemas';
|
||||
import { useIsDark, useZodSearchParams } from '~/hooks';
|
||||
import { useIsDark, useKeyBind, useKeyMatcher, useZodSearchParams } from '~/hooks';
|
||||
|
||||
import { useExplorerContext } from '../Context';
|
||||
import { FileThumb } from '../FilePath/Thumb';
|
||||
import { useExplorerSearchParams } from '../util';
|
||||
|
||||
export const PATH_BAR_HEIGHT = 32;
|
||||
|
||||
export const ExplorerPath = memo(() => {
|
||||
const location = useLocation();
|
||||
const isDark = useIsDark();
|
||||
@@ -18,6 +20,8 @@ export const ExplorerPath = memo(() => {
|
||||
|
||||
const [data, setData] = useState<{ kind: string; name: string }[] | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<ExplorerItem | undefined>(undefined);
|
||||
const metaCtrlKey = useKeyMatcher('Meta').key;
|
||||
const layoutStore = useExplorerLayoutStore();
|
||||
|
||||
const explorerContext = useExplorerContext();
|
||||
const [{ path }] = useExplorerSearchParams();
|
||||
@@ -79,10 +83,17 @@ export const ExplorerPath = memo(() => {
|
||||
} else setSelectedItem(undefined);
|
||||
}, [pathInfo, explorerContext.selectedItems, formatPathData]);
|
||||
|
||||
useKeyBind([metaCtrlKey, 'p'], (e) => {
|
||||
e.stopPropagation();
|
||||
getExplorerLayoutStore().showPathBar = !layoutStore.showPathBar;
|
||||
});
|
||||
|
||||
if (!layoutStore.showPathBar) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed bottom-0 flex h-8 w-full items-center gap-1 border-t
|
||||
border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-faint backdrop-blur-lg"
|
||||
className="absolute inset-x-0 bottom-0 flex items-center gap-1 border-t border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-faint backdrop-blur-lg"
|
||||
style={{ height: PATH_BAR_HEIGHT }}
|
||||
>
|
||||
{data?.map((p, index) => {
|
||||
return (
|
||||
@@ -94,8 +105,8 @@ export const ExplorerPath = memo(() => {
|
||||
index !== data.length - 1 && ' cursor-pointer hover:brightness-125'
|
||||
)}
|
||||
>
|
||||
<img src={getIcon('Folder', isDark)} alt="folder" className="h-3 w-3" />
|
||||
<p className="truncate">{p.name}</p>
|
||||
<img src={getIcon('Folder', isDark)} alt="folder" className="h-4 w-4" />
|
||||
<span className="max-w-xs truncate">{p.name}</span>
|
||||
{index !== (data?.length as number) - 1 && (
|
||||
<CaretRight weight="bold" size={10} />
|
||||
)}
|
||||
@@ -105,8 +116,10 @@ export const ExplorerPath = memo(() => {
|
||||
{selectedItem && (
|
||||
<div className="pointer-events-none flex items-center gap-1">
|
||||
{data && data.length > 0 && <CaretRight weight="bold" size={10} />}
|
||||
<FileThumb size={12} data={selectedItem} />
|
||||
{'name' in selectedItem.item && <p>{selectedItem.item.name}</p>}
|
||||
<FileThumb size={16} frame frameClassName="!border" data={selectedItem} />
|
||||
{'name' in selectedItem.item && (
|
||||
<span className="max-w-xs truncate">{selectedItem.item.name}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -123,6 +123,8 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
const itemDetailsHeight = settings.gridItemSize / 4 + (settings.showBytesInGridView ? 20 : 0);
|
||||
const itemHeight = settings.gridItemSize + itemDetailsHeight;
|
||||
|
||||
const padding = settings.layoutMode === 'grid' ? 12 : 0;
|
||||
|
||||
const grid = useGridList({
|
||||
ref: explorerView.ref,
|
||||
count: explorer.items?.length ?? 0,
|
||||
@@ -142,7 +144,14 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
[explorer.items]
|
||||
),
|
||||
getItemData: useCallback((index: number) => explorer.items?.[index], [explorer.items]),
|
||||
padding: explorerView.padding ?? settings.layoutMode === 'grid' ? 12 : undefined,
|
||||
padding: {
|
||||
...explorerView.padding,
|
||||
bottom: explorerView.bottom
|
||||
? (explorerView.padding?.bottom ?? padding) + explorerView.bottom
|
||||
: undefined,
|
||||
x: padding,
|
||||
y: padding
|
||||
},
|
||||
gap:
|
||||
explorerView.gap ||
|
||||
(settings.layoutMode === 'grid' ? explorerStore.gridGap : undefined),
|
||||
@@ -326,8 +335,6 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
explorerView.ref.current &&
|
||||
(e.key === 'ArrowUp' || e.key === 'ArrowDown')
|
||||
) {
|
||||
const paddingTop = parseInt(getComputedStyle(explorer.scrollRef.current).paddingTop);
|
||||
|
||||
const viewRect = explorerView.ref.current.getBoundingClientRect();
|
||||
|
||||
const itemRect = newSelectedItem.rect;
|
||||
@@ -335,7 +342,9 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
const itemBottom = itemRect.bottom + viewRect.top;
|
||||
|
||||
const scrollRect = explorer.scrollRef.current.getBoundingClientRect();
|
||||
const scrollTop = paddingTop + (explorerView.top || 0) + 1;
|
||||
const scrollTop =
|
||||
(explorerView.top ??
|
||||
parseInt(getComputedStyle(explorer.scrollRef.current).paddingTop)) + 1;
|
||||
const scrollBottom = scrollRect.height - (os !== 'windows' && os !== 'browser' ? 2 : 1);
|
||||
|
||||
if (itemTop < scrollTop) {
|
||||
@@ -343,17 +352,18 @@ export default ({ children }: { children: RenderItem }) => {
|
||||
top:
|
||||
itemTop -
|
||||
scrollTop -
|
||||
(newSelectedItem.row === 0 ? grid.padding.y : 0) -
|
||||
(newSelectedItem.row !== 0 ? grid.gap.y / 2 : 0),
|
||||
(newSelectedItem.row === 0 ? grid.padding.top : grid.gap.y / 2),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} else if (itemBottom > scrollBottom) {
|
||||
} else if (itemBottom > scrollBottom - (explorerView.bottom ?? 0)) {
|
||||
explorer.scrollRef.current.scrollBy({
|
||||
top:
|
||||
itemBottom -
|
||||
scrollBottom +
|
||||
(newSelectedItem.row === grid.rowCount - 1 ? grid.padding.y : 0) +
|
||||
(newSelectedItem.row !== grid.rowCount - 1 ? grid.gap.y / 2 : 0),
|
||||
(explorerView.bottom ?? 0) +
|
||||
(newSelectedItem.row === grid.rowCount - 1
|
||||
? grid.padding.bottom
|
||||
: grid.gap.y / 2),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,13 +28,15 @@ import {
|
||||
} from '../../store';
|
||||
import { uniqueId } from '../../util';
|
||||
import { useExplorerViewContext } from '../../ViewContext';
|
||||
import { useExplorerViewPadding } from '../util';
|
||||
import { ViewItem } from '../ViewItem';
|
||||
import { getRangeDirection, Range, useRanges } from './util/ranges';
|
||||
import { useTable } from './util/table';
|
||||
|
||||
interface ListViewItemProps {
|
||||
row: Row<ExplorerItem>;
|
||||
paddingX: number;
|
||||
paddingLeft: number;
|
||||
paddingRight: number;
|
||||
// Props below are passed to trigger a rerender
|
||||
// TODO: Find a better solution
|
||||
columnSizing: ColumnSizingState;
|
||||
@@ -48,7 +50,7 @@ const ListViewItem = memo((props: ListViewItemProps) => {
|
||||
<ViewItem
|
||||
data={props.row.original}
|
||||
className="relative flex h-full items-center"
|
||||
style={{ paddingLeft: props.paddingX, paddingRight: props.paddingX }}
|
||||
style={{ paddingLeft: props.paddingLeft, paddingRight: props.paddingRight }}
|
||||
>
|
||||
{props.row.getVisibleCells().map((cell) => (
|
||||
<div
|
||||
@@ -86,6 +88,8 @@ const HeaderColumnName = ({ name }: { name: string }) => {
|
||||
};
|
||||
|
||||
const ROW_HEIGHT = 45;
|
||||
const PADDING_X = 16;
|
||||
const PADDING_Y = 12;
|
||||
|
||||
export default () => {
|
||||
const layout = useLayoutContext();
|
||||
@@ -115,17 +119,21 @@ export default () => {
|
||||
rows: rowsById
|
||||
});
|
||||
|
||||
const viewPadding = useExplorerViewPadding(explorerView.padding);
|
||||
|
||||
const padding = {
|
||||
x: explorerView.padding?.x ?? 16,
|
||||
y: explorerView.padding?.y ?? 12
|
||||
top: viewPadding.top ?? PADDING_Y,
|
||||
bottom: viewPadding.bottom ?? PADDING_Y,
|
||||
left: viewPadding.left ?? PADDING_X,
|
||||
right: viewPadding.right ?? PADDING_X
|
||||
};
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: explorer.count ?? rows.length,
|
||||
getScrollElement: useCallback(() => explorer.scrollRef.current, [explorer.scrollRef]),
|
||||
estimateSize: useCallback(() => ROW_HEIGHT, []),
|
||||
paddingStart: padding.y,
|
||||
paddingEnd: padding.y,
|
||||
paddingStart: padding.top,
|
||||
paddingEnd: padding.bottom + (explorerView.bottom ?? 0),
|
||||
scrollMargin: listOffset,
|
||||
overscan: explorer.overscan ?? 10
|
||||
});
|
||||
@@ -442,7 +450,8 @@ export default () => {
|
||||
);
|
||||
|
||||
const tableWidth = tableRef.current.offsetWidth;
|
||||
const columnsWidth = Object.values(sizing).reduce((a, b) => a + b, 0) + padding.x * 2;
|
||||
const columnsWidth =
|
||||
Object.values(sizing).reduce((a, b) => a + b, 0) + (padding.left + padding.right);
|
||||
|
||||
if (columnsWidth < tableWidth) {
|
||||
const nameWidth = (sizing.name ?? 0) + (tableWidth - columnsWidth);
|
||||
@@ -463,7 +472,7 @@ export default () => {
|
||||
}
|
||||
|
||||
setSized(true);
|
||||
}, [columnSizing, columnVisibility, padding.x, sized, table]);
|
||||
}, [columnSizing, columnVisibility, padding.left, padding.right, sized, table]);
|
||||
|
||||
// Load more items
|
||||
useEffect(() => {
|
||||
@@ -671,37 +680,36 @@ export default () => {
|
||||
}
|
||||
} else explorer.resetSelectedItems([item]);
|
||||
|
||||
if (explorer.scrollRef.current) {
|
||||
const tableBodyRect = tableBodyRef.current?.getBoundingClientRect();
|
||||
if (explorer.scrollRef.current && tableBodyRef.current) {
|
||||
const scrollRect = explorer.scrollRef.current.getBoundingClientRect();
|
||||
|
||||
const paddingTop = parseInt(getComputedStyle(explorer.scrollRef.current).paddingTop);
|
||||
|
||||
const top =
|
||||
(explorerView.top ? paddingTop + explorerView.top : paddingTop) +
|
||||
const tableTop =
|
||||
scrollRect.top +
|
||||
(explorer.scrollRef.current.scrollTop > listOffset ? 36 : 0);
|
||||
(explorerView.top ??
|
||||
parseInt(getComputedStyle(explorer.scrollRef.current).paddingTop)) +
|
||||
(explorer.scrollRef.current.scrollTop > top ? 36 : 0);
|
||||
|
||||
const rowTop =
|
||||
scrollRect.top +
|
||||
nextRow.index * ROW_HEIGHT +
|
||||
rowVirtualizer.options.paddingStart +
|
||||
(tableBodyRect?.top || 0) +
|
||||
scrollRect.top;
|
||||
tableBodyRef.current.getBoundingClientRect().top;
|
||||
|
||||
const rowBottom = rowTop + ROW_HEIGHT;
|
||||
|
||||
if (rowTop < top) {
|
||||
const scrollBy = rowTop - top - (nextRow.index === 0 ? padding.y : 0);
|
||||
if (rowTop < tableTop) {
|
||||
const scrollBy = rowTop - tableTop - (nextRow.index === 0 ? padding.top : 0);
|
||||
|
||||
explorer.scrollRef.current.scrollBy({
|
||||
top: scrollBy,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} else if (rowBottom > scrollRect.bottom) {
|
||||
} else if (rowBottom > scrollRect.height - (explorerView.bottom ?? 0)) {
|
||||
const scrollBy =
|
||||
rowBottom -
|
||||
scrollRect.height +
|
||||
(nextRow.index === rows.length - 1 ? padding.y : 0);
|
||||
(explorerView.bottom ?? 0) +
|
||||
(nextRow.index === rows.length - 1 ? padding.bottom : 0);
|
||||
|
||||
explorer.scrollRef.current.scrollBy({
|
||||
top: scrollBy,
|
||||
@@ -735,7 +743,8 @@ export default () => {
|
||||
{} as ColumnSizingState
|
||||
);
|
||||
|
||||
const columnsWidth = Object.values(sizing).reduce((a, b) => a + b, 0) + padding.x * 2;
|
||||
const columnsWidth =
|
||||
Object.values(sizing).reduce((a, b) => a + b, 0) + (padding.left + padding.right);
|
||||
|
||||
if (locked) {
|
||||
const newNameSize = (sizing.name ?? 0) + (width - columnsWidth);
|
||||
@@ -830,7 +839,8 @@ export default () => {
|
||||
width:
|
||||
i === 0 ||
|
||||
i === headerGroup.headers.length - 1
|
||||
? size + padding.x
|
||||
? size +
|
||||
padding[i ? 'right' : 'left']
|
||||
: size
|
||||
}}
|
||||
onClick={() => {
|
||||
@@ -966,7 +976,10 @@ export default () => {
|
||||
selectedNext &&
|
||||
'rounded-b-none border-b-0 border-b-transparent'
|
||||
)}
|
||||
style={{ right: padding.x, left: padding.x }}
|
||||
style={{
|
||||
right: padding.right,
|
||||
left: padding.left
|
||||
}}
|
||||
>
|
||||
{selectedPrior && (
|
||||
<div className="absolute inset-x-3 top-0 h-px bg-accent/10" />
|
||||
@@ -975,7 +988,8 @@ export default () => {
|
||||
|
||||
<ListViewItem
|
||||
row={row}
|
||||
paddingX={padding.x}
|
||||
paddingLeft={padding.left}
|
||||
paddingRight={padding.right}
|
||||
columnSizing={columnSizing}
|
||||
columnVisibility={columnVisibility}
|
||||
isCut={cut}
|
||||
|
||||
@@ -25,12 +25,21 @@ import { useQuickPreviewContext } from '../QuickPreview/Context';
|
||||
import { useQuickPreviewStore } from '../QuickPreview/store';
|
||||
import { getExplorerStore } from '../store';
|
||||
import { ViewContext, type ExplorerViewContext } from '../ViewContext';
|
||||
import { ExplorerPath } from './ExplorerPath';
|
||||
import GridView from './GridView';
|
||||
import ListView from './ListView';
|
||||
import MediaView from './MediaView';
|
||||
import { useExplorerViewPadding } from './util';
|
||||
import { useViewItemDoubleClick } from './ViewItem';
|
||||
|
||||
export interface ExplorerViewPadding {
|
||||
x?: number;
|
||||
y?: number;
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
}
|
||||
|
||||
export interface ExplorerViewProps
|
||||
extends Omit<
|
||||
ExplorerViewContext,
|
||||
@@ -39,7 +48,7 @@ export interface ExplorerViewProps
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
emptyNotice?: JSX.Element;
|
||||
padding?: number | { x?: number; y?: number };
|
||||
padding?: number | ExplorerViewPadding;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
@@ -60,6 +69,8 @@ export default memo(
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
const viewPadding = useExplorerViewPadding(padding);
|
||||
|
||||
useKeyDownHandlers({
|
||||
disabled: isRenaming || quickPreviewStore.open
|
||||
});
|
||||
@@ -101,10 +112,7 @@ export default memo(
|
||||
isRenaming,
|
||||
setIsRenaming,
|
||||
ref,
|
||||
padding: {
|
||||
x: typeof padding === 'object' ? padding.x : padding,
|
||||
y: typeof padding === 'object' ? padding.y : padding
|
||||
}
|
||||
padding: viewPadding
|
||||
}}
|
||||
>
|
||||
{layoutMode === 'grid' && <GridView />}
|
||||
@@ -117,7 +125,6 @@ export default memo(
|
||||
) : (
|
||||
emptyNotice
|
||||
)}
|
||||
<ExplorerPath />
|
||||
</div>
|
||||
|
||||
{quickPreview.ref && createPortal(<QuickPreview />, quickPreview.ref)}
|
||||
|
||||
17
interface/app/$libraryId/Explorer/View/util.ts
Normal file
17
interface/app/$libraryId/Explorer/View/util.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ExplorerViewPadding } from '.';
|
||||
|
||||
export const useExplorerViewPadding = (padding?: number | ExplorerViewPadding) => {
|
||||
const getPadding = useCallback(
|
||||
(key: keyof ExplorerViewPadding) => (typeof padding === 'object' ? padding[key] : padding),
|
||||
[padding]
|
||||
);
|
||||
|
||||
return {
|
||||
top: getPadding('top') ?? getPadding('y'),
|
||||
bottom: getPadding('bottom') ?? getPadding('y'),
|
||||
left: getPadding('left') ?? getPadding('x'),
|
||||
right: getPadding('right') ?? getPadding('x')
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,16 @@
|
||||
import { createContext, useContext, type ReactNode, type RefObject } from 'react';
|
||||
|
||||
import { ExplorerViewPadding } from './View';
|
||||
|
||||
export interface ExplorerViewContext {
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
contextMenu?: ReactNode;
|
||||
setIsContextMenuOpen?: (isOpen: boolean) => void;
|
||||
isRenaming: boolean;
|
||||
setIsRenaming: (isRenaming: boolean) => void;
|
||||
padding?: { x?: number; y?: number };
|
||||
padding?: Omit<ExplorerViewPadding, 'x' | 'y'>;
|
||||
gap?: number | { x?: number; y?: number };
|
||||
selectable: boolean;
|
||||
listViewOptions?: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FolderNotchOpen } from '@phosphor-icons/react';
|
||||
import { useEffect, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import { useLibrarySubscription } from '@sd/client';
|
||||
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import { useExplorerLayoutStore, useLibrarySubscription } from '@sd/client';
|
||||
|
||||
import { TOP_BAR_HEIGHT } from '../TopBar';
|
||||
import { useExplorerContext } from './Context';
|
||||
@@ -9,8 +9,8 @@ import DismissibleNotice from './DismissibleNotice';
|
||||
import { Inspector, INSPECTOR_WIDTH } from './Inspector';
|
||||
import ExplorerContextMenu from './ParentContextMenu';
|
||||
import { useExplorerStore } from './store';
|
||||
import { useExplorerSearchParams } from './util';
|
||||
import View, { EmptyNotice, ExplorerViewProps } from './View';
|
||||
import { ExplorerPath, PATH_BAR_HEIGHT } from './View/ExplorerPath';
|
||||
|
||||
interface Props {
|
||||
emptyNotice?: ExplorerViewProps['emptyNotice'];
|
||||
@@ -24,6 +24,7 @@ interface Props {
|
||||
export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
const explorerStore = useExplorerStore();
|
||||
const explorer = useExplorerContext();
|
||||
const { showPathBar } = useExplorerLayoutStore();
|
||||
|
||||
// Can we put this somewhere else -_-
|
||||
useLibrarySubscription(['jobs.newThumbnail'], {
|
||||
@@ -45,10 +46,16 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
<div
|
||||
ref={explorer.scrollRef}
|
||||
className="custom-scroll explorer-scroll h-screen overflow-x-hidden"
|
||||
style={{
|
||||
paddingTop: TOP_BAR_HEIGHT,
|
||||
paddingRight: explorerStore.showInspector ? INSPECTOR_WIDTH : 0
|
||||
}}
|
||||
style={
|
||||
{
|
||||
'--scrollbar-margin-top': `${TOP_BAR_HEIGHT}px`,
|
||||
'--scrollbar-margin-bottom': `${
|
||||
showPathBar ? PATH_BAR_HEIGHT + 2 : 0 // TODO: Fix for web app
|
||||
}px`,
|
||||
'paddingTop': TOP_BAR_HEIGHT,
|
||||
'paddingRight': explorerStore.showInspector ? INSPECTOR_WIDTH : 0
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{explorer.items && explorer.items.length > 0 && <DismissibleNotice />}
|
||||
|
||||
@@ -63,15 +70,21 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
)
|
||||
}
|
||||
listViewOptions={{ hideHeaderBorder: true }}
|
||||
bottom={showPathBar ? PATH_BAR_HEIGHT : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ExplorerContextMenu>
|
||||
|
||||
<ExplorerPath />
|
||||
|
||||
{explorerStore.showInspector && (
|
||||
<Inspector
|
||||
className="no-scrollbar absolute inset-y-0 right-1.5 pb-3 pl-3 pr-1.5"
|
||||
style={{ paddingTop: TOP_BAR_HEIGHT + 12 }}
|
||||
className="no-scrollbar absolute right-1.5 top-0 pb-3 pl-3 pr-1.5"
|
||||
style={{
|
||||
paddingTop: TOP_BAR_HEIGHT + 12,
|
||||
bottom: showPathBar ? PATH_BAR_HEIGHT : 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -69,6 +69,10 @@ const shortcutCategories: Record<string, Shortcut[]> = {
|
||||
action: 'Show inspector',
|
||||
key: [[[ModifierKeys.Control], ['i']]]
|
||||
},
|
||||
{
|
||||
action: 'Show path bar',
|
||||
key: [[[ModifierKeys.Control], ['p']]]
|
||||
},
|
||||
{
|
||||
action: 'Rename file or folder',
|
||||
key: [[[], ['Enter']]]
|
||||
|
||||
@@ -101,7 +101,9 @@ body {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
@apply mt-[46px] rounded-[6px] bg-transparent;
|
||||
@apply rounded-[6px] bg-transparent;
|
||||
margin-top: var(--scrollbar-margin-top);
|
||||
margin-bottom: var(--scrollbar-margin-bottom);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply rounded-[6px] bg-app-explorerScrollbar;
|
||||
|
||||
@@ -11,6 +11,8 @@ import React, {
|
||||
} from 'react';
|
||||
import { useMutationObserver } from 'rooks';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { ExplorerViewPadding } from '~/app/$libraryId/Explorer/View';
|
||||
import { useExplorerViewPadding } from '~/app/$libraryId/Explorer/View/util';
|
||||
|
||||
type ItemData = any | undefined;
|
||||
type ItemId = number | string;
|
||||
@@ -28,7 +30,7 @@ export interface UseGridListProps<IdT extends ItemId = number, DataT extends Ite
|
||||
count: number;
|
||||
totalCount?: number;
|
||||
ref: RefObject<HTMLElement>;
|
||||
padding?: number | { x?: number; y?: number };
|
||||
padding?: number | ExplorerViewPadding;
|
||||
gap?: number | { x?: number; y?: number };
|
||||
overscan?: number;
|
||||
top?: number;
|
||||
@@ -53,8 +55,12 @@ export const useGridList = <IdT extends ItemId = number, DataT extends ItemData
|
||||
|
||||
const count = props.totalCount ?? props.count;
|
||||
|
||||
const paddingX = (typeof padding === 'object' ? padding.x : padding) || 0;
|
||||
const paddingY = (typeof padding === 'object' ? padding.y : padding) || 0;
|
||||
const gridPadding = useExplorerViewPadding(padding);
|
||||
|
||||
const paddingTop = gridPadding.top ?? 0;
|
||||
const paddingBottom = gridPadding.bottom ?? 0;
|
||||
const paddingLeft = gridPadding.left ?? 0;
|
||||
const paddingRight = gridPadding.right ?? 0;
|
||||
|
||||
const gapX = (typeof gap === 'object' ? gap.x : gap) || 0;
|
||||
const gapY = (typeof gap === 'object' ? gap.y : gap) || 0;
|
||||
@@ -62,7 +68,7 @@ export const useGridList = <IdT extends ItemId = number, DataT extends ItemData
|
||||
const itemWidth = size ? (typeof size === 'object' ? size.width : size) : undefined;
|
||||
const itemHeight = size ? (typeof size === 'object' ? size.height : size) : undefined;
|
||||
|
||||
const gridWidth = width ? width - (paddingX || 0) * 2 : 0;
|
||||
const gridWidth = width ? width - (paddingLeft + paddingRight) : 0;
|
||||
|
||||
let columnCount = columns || 0;
|
||||
|
||||
@@ -91,8 +97,8 @@ export const useGridList = <IdT extends ItemId = number, DataT extends ItemData
|
||||
const column = index % columnCount;
|
||||
const row = Math.floor(index / columnCount);
|
||||
|
||||
const x = paddingX + (column !== 0 ? gapX : 0) * column + virtualItemWidth * column;
|
||||
const y = paddingY + (row !== 0 ? gapY : 0) * row + virtualItemHeight * row;
|
||||
const x = paddingLeft + (column !== 0 ? gapX : 0) * column + virtualItemWidth * column;
|
||||
const y = paddingTop + (row !== 0 ? gapY : 0) * row + virtualItemHeight * row;
|
||||
|
||||
const item: GridListItem<typeof id, DataT> = {
|
||||
index,
|
||||
@@ -121,8 +127,8 @@ export const useGridList = <IdT extends ItemId = number, DataT extends ItemData
|
||||
gapY,
|
||||
getItemId,
|
||||
getItemData,
|
||||
paddingX,
|
||||
paddingY,
|
||||
paddingLeft,
|
||||
paddingTop,
|
||||
virtualItemHeight,
|
||||
virtualItemWidth
|
||||
]
|
||||
@@ -133,7 +139,7 @@ export const useGridList = <IdT extends ItemId = number, DataT extends ItemData
|
||||
rowCount,
|
||||
totalRowCount,
|
||||
width: gridWidth,
|
||||
padding: { x: paddingX, y: paddingY },
|
||||
padding: { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight },
|
||||
gap: { x: gapX, y: gapY },
|
||||
itemHeight,
|
||||
itemWidth,
|
||||
@@ -169,8 +175,8 @@ export const GridList = ({ grid, children, scrollRef }: GridListProps) => {
|
||||
count: grid.totalRowCount,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: getHeight,
|
||||
paddingStart: grid.padding.y,
|
||||
paddingEnd: grid.padding.y,
|
||||
paddingStart: grid.padding.top,
|
||||
paddingEnd: grid.padding.bottom,
|
||||
overscan: grid.overscan,
|
||||
scrollMargin: listOffset
|
||||
});
|
||||
@@ -180,8 +186,8 @@ export const GridList = ({ grid, children, scrollRef }: GridListProps) => {
|
||||
count: grid.columnCount,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: getWidth,
|
||||
paddingStart: grid.padding.x,
|
||||
paddingEnd: grid.padding.x
|
||||
paddingStart: grid.padding.left,
|
||||
paddingEnd: grid.padding.right
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './useThemeStore';
|
||||
export * from './useNotifications';
|
||||
export * from './useForceUpdate';
|
||||
export * from './useUnitFormatStore';
|
||||
export * from './useExplorerLayoutStore';
|
||||
|
||||
14
packages/client/src/hooks/useExplorerLayoutStore.ts
Normal file
14
packages/client/src/hooks/useExplorerLayoutStore.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { valtioPersist } from "../lib";
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
const explorerLayoutStore = valtioPersist('sd-explorer-layout', {
|
||||
showPathBar: true,
|
||||
})
|
||||
|
||||
export function useExplorerLayoutStore() {
|
||||
return useSnapshot(explorerLayoutStore);
|
||||
}
|
||||
|
||||
export function getExplorerLayoutStore() {
|
||||
return explorerLayoutStore;
|
||||
}
|
||||
Reference in New Issue
Block a user