mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-19 14:08:45 -04:00
[ENG-631] TopBar improvements & misc fixes (#837)
* fix things * added back/forward buttons to settings * split top bar context into left and right * hook up path * fix background jobs hidden from job manager * core * fix type + quick preview transition * fix selected item color contrast * fix close button on quick preview * clean up job ui for light theme * Improve media view overscan --------- Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
@@ -37,7 +37,7 @@ const GridViewItem = memo(({ data, selected, index, ...props }: GridViewItemProp
|
||||
className={clsx(
|
||||
'mb-1 flex items-center justify-center justify-items-center rounded-lg border-2 border-transparent text-center active:translate-y-[1px]',
|
||||
{
|
||||
'bg-app-selected/20': selected
|
||||
'bg-app-selectedItem': selected
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -32,8 +32,8 @@ const MediaViewItem = memo(({ data, index }: MediaViewItemProps) => {
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'group relative flex aspect-square items-center justify-center hover:bg-app-selected/20',
|
||||
selected && 'bg-app-selected/20'
|
||||
'hover:bg-app-selectedItem group relative flex aspect-square items-center justify-center',
|
||||
selected && 'bg-app-selectedItem'
|
||||
)}
|
||||
>
|
||||
<FileThumb
|
||||
@@ -82,7 +82,7 @@ export default () => {
|
||||
measureElement: () => itemSize,
|
||||
paddingStart: gridPadding,
|
||||
paddingEnd: gridPadding,
|
||||
overscan: !dismissibleNoticeStore.mediaView ? 2 : 1
|
||||
overscan: !dismissibleNoticeStore.mediaView ? 8 : 4
|
||||
});
|
||||
|
||||
const columnVirtualizer = useVirtualizer({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { animated, useTransition } from '@react-spring/web';
|
||||
import { XCircle } from 'phosphor-react';
|
||||
import { X, XCircle } from 'phosphor-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { subscribeKey } from 'valtio/utils';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { getExplorerStore } from '~/hooks';
|
||||
import FileThumb from './File/Thumb';
|
||||
import { Button } from '@sd/ui';
|
||||
|
||||
const AnimatedDialogOverlay = animated(Dialog.Overlay);
|
||||
const AnimatedDialogContent = animated(Dialog.Content);
|
||||
@@ -42,12 +43,12 @@ export function QuickPreview({ transformOrigin }: QuickPreviewProps) {
|
||||
const transitions = useTransition(isOpen, {
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: `translateY(20px)`,
|
||||
transformOrigin: transformOrigin || 'bottom'
|
||||
transform: `translateY(20px) scale(0.9)`,
|
||||
transformOrigin: transformOrigin || 'center top'
|
||||
},
|
||||
enter: { opacity: 1, transform: `translateY(0px)` },
|
||||
leave: { opacity: 0, transform: `translateY(20px)` },
|
||||
config: { mass: 0.4, tension: 200, friction: 10, bounce: 0 }
|
||||
enter: { opacity: 1, transform: `translateY(0px) scale(1)` },
|
||||
leave: { opacity: 0, transform: `translateY(40px) scale(0.9)` },
|
||||
config: { mass: 0.2, tension: 300, friction: 20, bounce: 0 }
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -80,10 +81,13 @@ export function QuickPreview({ transformOrigin }: QuickPreviewProps) {
|
||||
<div className="!pointer-events-auto flex h-5/6 max-h-screen w-11/12 flex-col rounded-md border border-app-line bg-app-box text-ink shadow-app-shade">
|
||||
<nav className="flex w-full flex-row">
|
||||
<Dialog.Close
|
||||
className="ml-2 text-ink-dull"
|
||||
className="m-2"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XCircle size={16} />
|
||||
<Button size="icon" variant="outline" className='flex flex-row'>
|
||||
<X weight='bold' className=' h-3 w-3 text-ink-faint' />
|
||||
<span className='ml-1 text-tiny font-medium text-ink-faint'>ESC</span>
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Title className="mx-auto my-1 font-bold">
|
||||
Preview -{' '}
|
||||
|
||||
@@ -84,6 +84,7 @@ interface Props {
|
||||
hasNextPage?: boolean;
|
||||
isFetchingNextPage?: boolean;
|
||||
viewClassName?: string;
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default memo((props: Props) => {
|
||||
@@ -97,7 +98,7 @@ export default memo((props: Props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
ref={props.scrollRef || scrollRef}
|
||||
className={clsx(
|
||||
'custom-scroll explorer-scroll h-screen',
|
||||
layoutMode === 'grid' && 'overflow-x-hidden',
|
||||
@@ -110,7 +111,7 @@ export default memo((props: Props) => {
|
||||
<ViewContext.Provider
|
||||
value={{
|
||||
data: props.data,
|
||||
scrollRef: scrollRef,
|
||||
scrollRef: props.scrollRef || scrollRef,
|
||||
onLoadMore: props.onLoadMore,
|
||||
hasNextPage: props.hasNextPage,
|
||||
isFetchingNextPage: props.isFetchingNextPage
|
||||
|
||||
@@ -20,6 +20,7 @@ interface Props {
|
||||
children?: ReactNode;
|
||||
inspectorClassName?: string;
|
||||
explorerClassName?: string;
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default function Explorer(props: Props) {
|
||||
@@ -63,6 +64,7 @@ export default function Explorer(props: Props) {
|
||||
<ExplorerContextMenu>
|
||||
{props.items && (
|
||||
<View
|
||||
scrollRef={props.scrollRef}
|
||||
data={props.items}
|
||||
onLoadMore={props.onLoadMore}
|
||||
hasNextPage={props.hasNextPage}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
:root {
|
||||
--border-color: rgba(50, 51, 67, 1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--border-color: rgba(50, 51, 67, 0.192); // Use lighter color for light mode
|
||||
}
|
||||
}
|
||||
|
||||
ul.groupedjob {
|
||||
position: relative;
|
||||
&::before {
|
||||
@@ -6,7 +16,7 @@ ul.groupedjob {
|
||||
top: 37px;
|
||||
height: 48.75px;
|
||||
width: 11px;
|
||||
border-left: 1px solid rgba(50, 51, 67, 1);
|
||||
border-left: 1px solid var(--border-color);
|
||||
content: '';
|
||||
left: 25px;
|
||||
}
|
||||
@@ -26,7 +36,7 @@ ul.groupedjob {
|
||||
height: 37.5px;
|
||||
top: 0;
|
||||
width: 10px;
|
||||
border-left: 1px solid rgba(50, 51, 67, 1);
|
||||
border-left: 1px solid var(--border-color);
|
||||
content: '';
|
||||
left: 25px;
|
||||
}
|
||||
@@ -36,10 +46,10 @@ ul.groupedjob {
|
||||
top: 23.5px;
|
||||
height: 1em;
|
||||
width: 10px;
|
||||
border-bottom: 1px solid rgba(50, 51, 67, 1);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
content: '';
|
||||
display: inline-block;
|
||||
left: 10px;
|
||||
left: -14px;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
@@ -47,7 +57,7 @@ ul.groupedjob {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
width: 10px;
|
||||
border-left: 1px solid rgba(50, 51, 67, 1);
|
||||
border-left: 1px solid var(--border-color);
|
||||
content: '';
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
@@ -32,28 +32,31 @@ const getNiceData = (
|
||||
name: isGroup
|
||||
? 'Indexing paths'
|
||||
: job.metadata?.location_path
|
||||
? `Indexed paths at ${job.metadata?.location_path} `
|
||||
: `Processing added location...`,
|
||||
? `Indexed paths at ${job.metadata?.location_path} `
|
||||
: `Processing added location...`,
|
||||
icon: Folder,
|
||||
filesDiscovered: `${numberWithCommas(
|
||||
job.metadata?.total_paths || 0
|
||||
)} ${JobCountTextCondition(job, 'path')}`
|
||||
},
|
||||
thumbnailer: {
|
||||
name: `${
|
||||
job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Generating thumbnails'
|
||||
: 'Generated thumbnails'
|
||||
}`,
|
||||
name: `${job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Generating thumbnails'
|
||||
: 'Generated thumbnails'
|
||||
}`,
|
||||
icon: Camera,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||
},
|
||||
shallow_thumbnailer: {
|
||||
name: `Generating thumbnails for current directory`,
|
||||
icon: Camera,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||
},
|
||||
file_identifier: {
|
||||
name: `${
|
||||
job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Extracting metadata'
|
||||
: 'Extracted metadata'
|
||||
}`,
|
||||
name: `${job.status === 'Running' || job.status === 'Queued'
|
||||
? 'Extracting metadata'
|
||||
: 'Extracted metadata'
|
||||
}`,
|
||||
icon: Eye,
|
||||
filesDiscovered:
|
||||
job.message ||
|
||||
@@ -134,7 +137,7 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
|
||||
isGroup ? `joblistitem pr-3 pt-0` : 'p-3'
|
||||
)}
|
||||
>
|
||||
<div className="ml-7 flex">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<niceData.icon
|
||||
className={clsx(
|
||||
|
||||
@@ -72,10 +72,8 @@ function JobGroup({ data, clearJob }: JobGroupProps) {
|
||||
<div className="truncate">
|
||||
<p className="truncate font-semibold">
|
||||
{allJobsCompleted
|
||||
? `Added location "${
|
||||
data.metadata.init.location.name || ''
|
||||
}"`
|
||||
: 'Processing added location...'}
|
||||
? `Added location "${data.metadata.init.location.name || ''}"`
|
||||
: `Indexing "${data.metadata.init.location.name || ''}"`}
|
||||
</p>
|
||||
<p className="my-[2px] text-ink-faint">
|
||||
<b>{tasks.total} </b>
|
||||
|
||||
@@ -81,15 +81,15 @@ export function JobsManager() {
|
||||
</PopoverClose>
|
||||
</div>
|
||||
<div className="no-scrollbar h-full overflow-x-hidden">
|
||||
{runningIndividualJobs?.map((job) => (
|
||||
<Job key={job.id} job={job} />
|
||||
))}
|
||||
{groupedJobs.map((data) => (
|
||||
<JobGroup key={data.id} data={data} clearJob={clearJobHandler} />
|
||||
))}
|
||||
{orphanJobs?.map((job) => (
|
||||
<Job key={job?.id} job={job} />
|
||||
))}
|
||||
{runningIndividualJobs?.map((job) => (
|
||||
<Job key={job.id} job={job} />
|
||||
))}
|
||||
{individualJobs?.map((job) => (
|
||||
<Job clearJob={clearJobHandler} key={job.id} job={job} />
|
||||
))}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default () => {
|
||||
}
|
||||
// we override the sidebar dropdown item's hover styles
|
||||
// because the dark style clashes with the sidebar
|
||||
className="data-[side=bottom]:slide-in-from-top-2 mt-1 shadow-none dark:divide-menu-selected/30 dark:border-sidebar-line dark:bg-sidebar-box"
|
||||
className="mt-1 shadow-none data-[side=bottom]:slide-in-from-top-2 dark:divide-menu-selected/30 dark:border-sidebar-line dark:bg-sidebar-box"
|
||||
alignToTrigger
|
||||
>
|
||||
{libraries.data?.map((lib) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { MacTrafficLights, NavigationButtons } from '~/components';
|
||||
import { MacTrafficLights } from '~/components';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import Contents from './Contents';
|
||||
import Footer from './Footer';
|
||||
@@ -18,11 +18,7 @@ export default () => {
|
||||
)}
|
||||
>
|
||||
{showControls && <MacTrafficLights className="absolute left-[13px] top-[13px] z-50" />}
|
||||
{(os !== 'browser' || showControls) && (
|
||||
<div className="mt-[-4px] flex justify-end">
|
||||
<NavigationButtons />
|
||||
</div>
|
||||
)}
|
||||
{os === 'macOS' && <div data-tauri-drag-region className="h-5 w-full" />}
|
||||
<LibrariesDropdown />
|
||||
<Contents />
|
||||
<Footer />
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import { RefObject, createContext, useRef } from 'react';
|
||||
import { RefObject, createContext, useContext, useRef } from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
import TopBar from '.';
|
||||
|
||||
interface TopBarContext {
|
||||
topBarChildrenRef: RefObject<HTMLDivElement> | null;
|
||||
left: RefObject<HTMLDivElement>;
|
||||
right: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const TopBarContext = createContext<TopBarContext>({
|
||||
topBarChildrenRef: null
|
||||
});
|
||||
const TopBarContext = createContext<TopBarContext | null>(null);
|
||||
|
||||
export const Component = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const left = useRef<HTMLDivElement>(null);
|
||||
const right = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<TopBarContext.Provider value={{ topBarChildrenRef: ref }}>
|
||||
<TopBar ref={ref} />
|
||||
<TopBarContext.Provider value={{ left, right }}>
|
||||
<TopBar leftRef={left} rightRef={right} />
|
||||
<Outlet />
|
||||
</TopBarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useTopBarContext() {
|
||||
const ctx = useContext(TopBarContext);
|
||||
|
||||
if (!ctx) throw new Error('TopBarContext not found!');
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { ArrowLeft, ArrowRight } from 'phosphor-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button, Tooltip } from '@sd/ui';
|
||||
import { useSearchStore } from '~/hooks';
|
||||
import TopBarButton from './TopBarButton';
|
||||
|
||||
export const NavigationButtons = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -9,26 +10,26 @@ export const NavigationButtons = () => {
|
||||
const idx = history.state.idx as number;
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div data-tauri-drag-region className="flex">
|
||||
<Tooltip label="Navigate back">
|
||||
<Button
|
||||
size="icon"
|
||||
className="text-[14px] text-ink-dull"
|
||||
<TopBarButton
|
||||
rounding='left'
|
||||
// className="text-[14px] text-ink-dull"
|
||||
onClick={() => navigate(-1)}
|
||||
disabled={isFocused || idx === 0}
|
||||
>
|
||||
<ArrowLeft weight="bold" />
|
||||
</Button>
|
||||
<ArrowLeft size={14} className='m-[4px]' weight="bold" />
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Navigate forward">
|
||||
<Button
|
||||
size="icon"
|
||||
className="text-[14px] text-ink-dull"
|
||||
<TopBarButton
|
||||
rounding='right'
|
||||
// className="text-[14px] text-ink-dull"
|
||||
onClick={() => navigate(1)}
|
||||
disabled={isFocused || idx === history.length - 1}
|
||||
>
|
||||
<ArrowRight weight="bold" />
|
||||
</Button>
|
||||
<ArrowRight size={14} className='m-[4px]' weight="bold" />
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
18
interface/app/$libraryId/TopBar/Portal.tsx
Normal file
18
interface/app/$libraryId/TopBar/Portal.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTopBarContext } from './Layout';
|
||||
|
||||
interface Props {
|
||||
left?: ReactNode;
|
||||
right?: ReactNode;
|
||||
}
|
||||
export const TopBarPortal = ({ left, right }: Props) => {
|
||||
const ctx = useTopBarContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{left && ctx.left.current && createPortal(left, ctx.left.current)}
|
||||
{right && ctx.right.current && createPortal(right, ctx.right.current)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DotsThreeCircle } from 'phosphor-react';
|
||||
import React, { HTMLAttributes, forwardRef } from 'react';
|
||||
import { Popover } from '@sd/ui';
|
||||
import { TOP_BAR_ICON_STYLE, ToolOption } from '.';
|
||||
import TopBarButton, { TopBarButtonProps } from './TopBarButton';
|
||||
import { TOP_BAR_ICON_STYLE, ToolOption } from './TopBarOptions';
|
||||
|
||||
const GroupTool = forwardRef<
|
||||
HTMLButtonElement,
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import clsx from 'clsx';
|
||||
import { useContext, useLayoutEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { Popover, Tooltip } from '@sd/ui';
|
||||
import { ToolOption } from '.';
|
||||
import { TopBarContext } from './Layout';
|
||||
import TopBarButton from './TopBarButton';
|
||||
import TopBarMobile from './TopBarMobile';
|
||||
|
||||
interface TopBarChildrenProps {
|
||||
toolOptions?: ToolOption[][];
|
||||
export interface ToolOption {
|
||||
icon: JSX.Element;
|
||||
onClick?: () => void;
|
||||
individual?: boolean;
|
||||
toolTipLabel: string;
|
||||
topBarActive?: boolean;
|
||||
popOverComponent?: JSX.Element;
|
||||
showAtResolution: ShowAtResolution;
|
||||
}
|
||||
|
||||
export default ({ toolOptions }: TopBarChildrenProps) => {
|
||||
const ctx = useContext(TopBarContext);
|
||||
const target = ctx.topBarChildrenRef?.current;
|
||||
export type ShowAtResolution = 'sm:flex' | 'md:flex' | 'lg:flex' | 'xl:flex' | '2xl:flex';
|
||||
interface TopBarChildrenProps {
|
||||
options?: ToolOption[][];
|
||||
}
|
||||
|
||||
export const TOP_BAR_ICON_STYLE = 'm-0.5 w-[18px] h-[18px] text-ink-dull';
|
||||
|
||||
export default ({ options }: TopBarChildrenProps) => {
|
||||
const [windowSize, setWindowSize] = useState(0);
|
||||
const toolsNotSmFlex = toolOptions
|
||||
const toolsNotSmFlex = options
|
||||
?.flatMap((group) => group)
|
||||
.filter((t) => t.showAtResolution !== 'sm:flex');
|
||||
|
||||
@@ -28,14 +36,10 @@ export default ({ toolOptions }: TopBarChildrenProps) => {
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex w-full flex-row justify-end">
|
||||
<div data-tauri-drag-region className={`flex gap-0`}>
|
||||
{toolOptions?.map((group, groupIndex) => {
|
||||
{options?.map((group, groupIndex) => {
|
||||
return group.map(
|
||||
(
|
||||
{
|
||||
@@ -49,14 +53,14 @@ export default ({ toolOptions }: TopBarChildrenProps) => {
|
||||
},
|
||||
index
|
||||
) => {
|
||||
const groupCount = toolOptions.length;
|
||||
const groupCount = options.length;
|
||||
const roundingCondition = individual
|
||||
? 'both'
|
||||
: index === 0
|
||||
? 'left'
|
||||
: index === group.length - 1
|
||||
? 'right'
|
||||
: 'none';
|
||||
? 'left'
|
||||
: index === group.length - 1
|
||||
? 'right'
|
||||
: 'none';
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
@@ -109,12 +113,11 @@ export default ({ toolOptions }: TopBarChildrenProps) => {
|
||||
})}
|
||||
</div>
|
||||
<TopBarMobile
|
||||
toolOptions={toolOptions}
|
||||
className={`${
|
||||
toolOptions={options}
|
||||
className={
|
||||
windowSize <= 1279 && (toolsNotSmFlex?.length as number) > 0 ? 'flex' : 'hidden'
|
||||
}`}
|
||||
}
|
||||
/>
|
||||
</div>,
|
||||
target
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +1,33 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { RefObject } from 'react';
|
||||
import { NavigationButtons } from './NavigationButtons';
|
||||
import SearchBar from './SearchBar';
|
||||
|
||||
export interface ToolOption {
|
||||
icon: JSX.Element;
|
||||
onClick?: () => void;
|
||||
individual?: boolean;
|
||||
toolTipLabel: string;
|
||||
topBarActive?: boolean;
|
||||
popOverComponent?: JSX.Element;
|
||||
showAtResolution: ShowAtResolution;
|
||||
}
|
||||
|
||||
export type ShowAtResolution = 'sm:flex' | 'md:flex' | 'lg:flex' | 'xl:flex' | '2xl:flex';
|
||||
|
||||
export const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
|
||||
export const TOP_BAR_HEIGHT = 46;
|
||||
|
||||
const TopBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
interface Props {
|
||||
leftRef?: RefObject<HTMLDivElement>;
|
||||
rightRef?: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const TopBar = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="
|
||||
duration-250 top-bar-blur absolute left-0 top-0 z-50 flex
|
||||
h-[46px] w-full flex-row items-center justify-center overflow-hidden
|
||||
border-b border-sidebar-divider bg-app/90 px-5
|
||||
border-b border-sidebar-divider bg-app/90 px-3.5
|
||||
transition-[background-color,border-color] ease-out
|
||||
"
|
||||
>
|
||||
<div className="flex-1" />
|
||||
<div data-tauri-drag-region className="flex flex-1 flex-row items-center">
|
||||
<NavigationButtons />
|
||||
<div ref={props.leftRef} />
|
||||
</div>
|
||||
<SearchBar />
|
||||
<div className="flex-1" ref={ref} />
|
||||
<div className="flex-1" ref={props.rightRef} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default TopBar;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useKey } from 'rooks';
|
||||
import { z } from 'zod';
|
||||
import { useLibraryContext, useLibraryMutation, useRspcLibraryContext } from '@sd/client';
|
||||
import { dialogManager } from '@sd/ui';
|
||||
import { useLibraryContext, useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from '@sd/client';
|
||||
import { Folder, dialogManager } from '@sd/ui';
|
||||
import {
|
||||
getExplorerStore,
|
||||
useExplorerStore,
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
import Explorer from '../Explorer';
|
||||
import DeleteDialog from '../Explorer/File/DeleteDialog';
|
||||
import { useExplorerOrder, useExplorerSearchParams } from '../Explorer/util';
|
||||
import TopBarChildren from '../TopBar/TopBarChildren';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import TopBarOptions from '../TopBar/TopBarOptions';
|
||||
|
||||
const PARAMS = z.object({
|
||||
id: z.coerce.number()
|
||||
@@ -25,6 +26,8 @@ export const Component = () => {
|
||||
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
|
||||
useExplorerTopBarOptions();
|
||||
|
||||
const { data: location } = useLibraryQuery(['locations.get', location_id]);
|
||||
|
||||
// we destructure this since `mutate` is a stable reference but the object it's in is not
|
||||
const { mutate: quickRescan } = useLibraryMutation('locations.quickRescan');
|
||||
|
||||
@@ -55,8 +58,20 @@ export const Component = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarChildren
|
||||
toolOptions={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
||||
<TopBarPortal
|
||||
left={
|
||||
<>
|
||||
<Folder size={22} className="ml-3 mr-2 -mt-[1px] inline-block" />
|
||||
<span className="text-sm font-medium">
|
||||
{path ? getLastSectionOfPath(path) : location?.name}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
right={
|
||||
<TopBarOptions
|
||||
options={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="relative flex w-full flex-col">
|
||||
<Explorer
|
||||
@@ -109,3 +124,13 @@ const useItems = () => {
|
||||
|
||||
return { query, items };
|
||||
};
|
||||
|
||||
|
||||
function getLastSectionOfPath(path: string): string | undefined {
|
||||
if (path.endsWith('/')) {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
const sections = path.split('/');
|
||||
const lastSection = sections[sections.length - 1];
|
||||
return lastSection;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default ({ category, icon, items, selected, onClick }: CategoryButtonProp
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
'flex shrink-0 items-center rounded-md px-1.5 py-1 text-sm',
|
||||
selected && 'bg-app-selected/20'
|
||||
selected && 'bg-app-selectedItem'
|
||||
)}
|
||||
>
|
||||
<img src={icon} className="mr-3 h-12 w-12" />
|
||||
|
||||
@@ -16,7 +16,8 @@ import { useExplorerStore, useExplorerTopBarOptions, useIsDark } from '~/hooks';
|
||||
import Explorer from '../Explorer';
|
||||
import { SEARCH_PARAMS, useExplorerOrder } from '../Explorer/util';
|
||||
import { usePageLayout } from '../PageLayout';
|
||||
import TopBarChildren from '../TopBar/TopBarChildren';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import TopBarOptions from '../TopBar/TopBarOptions';
|
||||
import CategoryButton from '../overview/CategoryButton';
|
||||
import Statistics from '../overview/Statistics';
|
||||
|
||||
@@ -92,12 +93,12 @@ export const Component = () => {
|
||||
favorite: isFavoritesCategory ? true : undefined,
|
||||
...(explorerStore.layoutMode === 'media'
|
||||
? {
|
||||
kind: [5, 7].includes(kind)
|
||||
? [kind]
|
||||
: isFavoritesCategory
|
||||
kind: [5, 7].includes(kind)
|
||||
? [kind]
|
||||
: isFavoritesCategory
|
||||
? [5, 7]
|
||||
: [5, 7, kind]
|
||||
}
|
||||
}
|
||||
: { kind: isFavoritesCategory ? [] : [kind] })
|
||||
}
|
||||
}
|
||||
@@ -128,19 +129,25 @@ export const Component = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarChildren
|
||||
toolOptions={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
||||
<TopBarPortal
|
||||
right={
|
||||
<TopBarOptions
|
||||
options={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Explorer
|
||||
inspectorClassName="!pt-0 !fixed !top-[50px] !right-[10px] !w-[260px]"
|
||||
inspectorClassName="!pt-0 !fixed !top-[50px] !right-[10px] !w-[260px]"
|
||||
explorerClassName="!overflow-visible" // required to ensure categories are sticky, remove with caution
|
||||
viewClassName="!pl-0 !pt-0 !h-auto"
|
||||
items={items}
|
||||
onLoadMore={query.fetchNextPage}
|
||||
hasNextPage={query.hasNextPage}
|
||||
isFetchingNextPage={query.isFetchingNextPage}
|
||||
scrollRef={page?.ref}
|
||||
>
|
||||
<Statistics />
|
||||
<div className="no-scrollbar sticky top-0 z-50 mt-2 flex space-x-[1px] overflow-x-scroll bg-app/90 px-5 py-1.5 backdrop-blur">
|
||||
<div className="no-scrollbar sticky top-0 z-10 mt-2 flex space-x-[1px] overflow-x-scroll bg-app/90 px-5 py-1.5 backdrop-blur">
|
||||
{categories.data?.map((category) => {
|
||||
const iconString = CategoryToIcon[category.name] || 'Document';
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
} from '~/hooks';
|
||||
import Explorer from './Explorer';
|
||||
import { getExplorerItemData } from './Explorer/util';
|
||||
import TopBarChildren from './TopBar/TopBarChildren';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
import TopBarOptions from './TopBar/TopBarOptions';
|
||||
|
||||
const SEARCH_PARAMS = z.object({
|
||||
search: z.string().optional(),
|
||||
@@ -49,12 +50,16 @@ const ExplorerStuff = memo((props: { args: SearchArgs }) => {
|
||||
<>
|
||||
{items && items.length > 0 ? (
|
||||
<>
|
||||
<TopBarChildren
|
||||
toolOptions={[
|
||||
explorerViewOptions,
|
||||
explorerToolOptions,
|
||||
explorerControlOptions
|
||||
]}
|
||||
<TopBarPortal
|
||||
right={
|
||||
<TopBarOptions
|
||||
options={[
|
||||
explorerViewOptions,
|
||||
explorerToolOptions,
|
||||
explorerControlOptions
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Explorer items={items} />
|
||||
</>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { tw } from '@sd/ui';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import Icon from '../Layout/Sidebar/Icon';
|
||||
import SidebarLink from '../Layout/Sidebar/Link';
|
||||
import { NavigationButtons } from '../TopBar/NavigationButtons';
|
||||
|
||||
const Heading = tw.div`mb-1 ml-1 text-xs font-semibold text-gray-400`;
|
||||
const Section = tw.div`space-y-0.5`;
|
||||
@@ -26,10 +27,13 @@ export default () => {
|
||||
return (
|
||||
<div className="custom-scroll no-scrollbar h-full w-60 max-w-[180px] shrink-0 border-r border-app-line/50 pb-5">
|
||||
{os !== 'browser' ? (
|
||||
<div data-tauri-drag-region className="h-5 w-full" />
|
||||
<div data-tauri-drag-region className="mb-3 h-3 w-full p-3 pl-[14px] pt-[10px]">
|
||||
<NavigationButtons />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-3" />
|
||||
)}
|
||||
|
||||
<div className="space-y-6 px-4 py-3">
|
||||
<Section>
|
||||
<Heading>Client</Heading>
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from './ColorPicker';
|
||||
export * from './DismissibleNotice';
|
||||
export * from './DragRegion';
|
||||
export * from './ExternalObject';
|
||||
export * from './NavigationButtons';
|
||||
export * from './PasswordMeter';
|
||||
export * from './SubtleButton';
|
||||
export * from './TrafficLights';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
import { ExplorerItem, Ordering } from '@sd/client';
|
||||
import { ExplorerItem, FilePathSearchOrdering } from '@sd/client';
|
||||
import { resetStore } from '@sd/client/src/stores/util';
|
||||
|
||||
type UnionKeys<T> = T extends any ? keyof T : never;
|
||||
@@ -14,7 +14,7 @@ export enum ExplorerKind {
|
||||
|
||||
export type CutCopyType = 'Cut' | 'Copy';
|
||||
|
||||
export type ExplorerOrderByKeys = UnionKeys<Ordering> | 'none';
|
||||
export type ExplorerOrderByKeys = UnionKeys<FilePathSearchOrdering> | 'none';
|
||||
|
||||
export type ExplorerDirection = 'asc' | 'desc';
|
||||
|
||||
@@ -28,8 +28,6 @@ const state = {
|
||||
tagAssignMode: false,
|
||||
showInspector: false,
|
||||
multiSelectIndexes: [] as number[],
|
||||
contextMenuObjectId: null as number | null,
|
||||
contextMenuActiveObject: null as object | null,
|
||||
newThumbnails: {} as Record<string, boolean | undefined>,
|
||||
cutCopyState: {
|
||||
sourcePath: '', // this is used solely for preventing copy/cutting to the same path (as that will truncate the file)
|
||||
@@ -44,7 +42,7 @@ const state = {
|
||||
mediaAspectSquare: true,
|
||||
orderBy: 'dateCreated' as ExplorerOrderByKeys,
|
||||
orderByDirection: 'desc' as ExplorerDirection,
|
||||
groupBy: 'none'
|
||||
groupBy: 'none',
|
||||
};
|
||||
|
||||
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.
|
||||
|
||||
@@ -11,13 +11,16 @@ import {
|
||||
Tag
|
||||
} from 'phosphor-react';
|
||||
import OptionsPanel from '~/app/$libraryId/Explorer/OptionsPanel';
|
||||
import { TOP_BAR_ICON_STYLE, ToolOption } from '~/app/$libraryId/TopBar';
|
||||
import { TOP_BAR_ICON_STYLE, ToolOption } from '~/app/$libraryId/TopBar/TopBarOptions';
|
||||
import { KeyManager } from '../app/$libraryId/KeyManager';
|
||||
import { getExplorerStore, useExplorerStore } from './useExplorerStore';
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
|
||||
export const useExplorerTopBarOptions = () => {
|
||||
const explorerStore = useExplorerStore();
|
||||
|
||||
const reload = useLibraryMutation('locations.quickRescan');
|
||||
|
||||
const explorerViewOptions: ToolOption[] = [
|
||||
{
|
||||
toolTipLabel: 'Grid view',
|
||||
@@ -94,7 +97,12 @@ export const useExplorerTopBarOptions = () => {
|
||||
showAtResolution: 'xl:flex'
|
||||
},
|
||||
{
|
||||
toolTipLabel: 'Regenerate thumbs (temp)',
|
||||
toolTipLabel: 'Reload',
|
||||
onClick: () => {
|
||||
if (explorerStore.locationId) {
|
||||
reload.mutate({ location_id: explorerStore.locationId, sub_path: '' })
|
||||
}
|
||||
},
|
||||
icon: <ArrowClockwise className={TOP_BAR_ICON_STYLE} />,
|
||||
individual: true,
|
||||
showAtResolution: 'xl:flex'
|
||||
|
||||
Reference in New Issue
Block a user