[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:
Jamie Pine
2023-05-20 17:17:27 -07:00
committed by GitHub
parent ce1cf7f495
commit 158366b69e
30 changed files with 270 additions and 182 deletions

View File

@@ -167,9 +167,7 @@ impl JobManager {
for worker in self.running_workers.read().await.values() {
let report = worker.lock().await.report();
if !report.is_background {
ret.push(report);
}
ret.push(report);
}
ret
}

View File

@@ -428,25 +428,26 @@ pub async fn light_scan_location(
}
let location_base_data = location::Data::from(&location);
// removed grouping for background jobs, they don't need to be grouped as only running ones are shown to the user
library
.spawn_job(
Job::new_with_action(
ShallowIndexerJobInit {
location,
sub_path: sub_path.clone(),
},
"light_scan_location",
)
.queue_next(ShallowFileIdentifierJobInit {
location: location_base_data.clone(),
sub_path: sub_path.clone(),
})
.queue_next(ShallowThumbnailerJobInit {
location: location_base_data,
sub_path,
}),
)
.spawn_job(ShallowIndexerJobInit {
location,
sub_path: sub_path.clone(),
})
.await
.unwrap_or(());
library
.spawn_job(ShallowFileIdentifierJobInit {
location: location_base_data.clone(),
sub_path: sub_path.clone(),
})
.await
.unwrap_or(());
library
.spawn_job(ShallowThumbnailerJobInit {
location: location_base_data.clone(),
sub_path: sub_path.clone(),
})
.await
}

View File

@@ -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
}
)}
>

View File

@@ -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({

View File

@@ -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 -{' '}

View File

@@ -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

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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>

View File

@@ -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} />
))}

View File

@@ -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) => (

View File

@@ -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 />

View File

@@ -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;
}

View File

@@ -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>
);

View 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)}
</>
);
};

View File

@@ -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,

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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" />

View File

@@ -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 (

View File

@@ -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} />
</>

View File

@@ -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>

View File

@@ -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';

View File

@@ -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.

View File

@@ -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'

View File

@@ -103,6 +103,8 @@ export type NodeConfig = { id: string; name: string; p2p_port: number | null; p2
export type CategoryItem = { name: string; count: number }
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
/**
* This denotes the `StoredKey` version.
*/
@@ -117,12 +119,12 @@ export type EncryptedKey = number[]
export type PeerId = string
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
export type GenerateThumbsForLocationArgs = { id: number; path: string }
export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig }
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
/**
* These parameters define the password-hashing level.
*
@@ -199,19 +201,13 @@ export type ObjectSearchArgs = { take?: number | null; tagId?: number | null; cu
export type SetNoteArgs = { id: number; note: string | null }
export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: any | null; is_background: boolean; errors_text: string[]; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; message: string }
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
export type FilePathSearchOrdering = { name: boolean } | { sizeInBytes: boolean } | { dateCreated: boolean } | { dateModified: boolean } | { dateIndexed: boolean } | { object: ObjectSearchOrdering }
export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null }
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null }
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
export type BuildInfo = { version: string; commit: string }
@@ -224,12 +220,14 @@ export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
export type ObjectSearchOrdering = { dateAccessed: boolean }
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
export type OwnedOperationItem = { id: any; data: OwnedOperationData }
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
/**
* TODO: P2P event for the frontend
*/
@@ -239,6 +237,8 @@ export type SpacedropArgs = { peer_id: PeerId; file_path: string[] }
export type RenameFileArgs = { location_id: number; file_name: string; new_file_name: string }
export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: any | null; is_background: boolean; errors_text: string[]; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; message: string }
export type OwnedOperation = { model: string; items: OwnedOperationItem[] }
export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null; file_paths: FilePath[] }
@@ -289,6 +289,8 @@ export type OwnedOperationData = { Create: { [key: string]: any } } | { CreateMa
export type SharedOperationData = SharedOperationCreateData | { field: string; value: any } | null
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
export type SearchData<T> = { cursor: number[] | null; items: T[] }
export type OptionalRange<T> = { from: T | null; to: T | null }
@@ -301,13 +303,13 @@ export type TagAssignArgs = { object_id: number; tag_id: number; unassign: boole
export type ChangeNodeNameArgs = { name: string }
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null }
/**
* This defines all available password hashing algorithms.
*/
export type HashingAlgorithm = { name: "Argon2id"; params: Params } | { name: "BalloonBlake3"; params: Params }
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" | "CompletedWithErrors"
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string; object: Object | null }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; indexer_rules: ({ indexer_rule: IndexerRule })[] }
@@ -323,12 +325,10 @@ export type AutomountUpdateArgs = { uuid: string; status: boolean }
export type Protected<T> = T
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" | "CompletedWithErrors"
export type RestoreBackupArgs = { password: Protected<string>; secret_key: Protected<string>; path: string }
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
export type RelationOperation = { relation_item: string; relation_group: string; relation: string; data: RelationOperationData }
/**

View File

@@ -34,6 +34,7 @@
--color-app-button: var(--dark-hue), 15%, 23%;
--color-app-hover: var(--dark-hue), 15%, 25%;
--color-app-selected: var(--dark-hue), 15%, 26%;
--color-app-selected-item: var(--dark-hue), 15%, 18%;
--color-app-active: var(--dark-hue), 15%, 30%;
--color-app-shade: var(--dark-hue), 15%, 0%;
--color-app-frame: var(--dark-hue), 15%, 25%;
@@ -71,7 +72,7 @@
// main
--color-app: var(--light-hue), 5%, 100%;
--color-app-box: var(--light-hue), 5%, 98%;
--color-app-dark-box: var(--light-hue), 5%, 93%;
--color-app-dark-box: var(--light-hue), 5%, 97%;
--color-app-light-box: var(--light-hue), 5%, 100%;
--color-app-overlay: var(--light-hue), 5%, 100%;
--color-app-input: var(--light-hue), 5%, 100%;
@@ -80,6 +81,7 @@
--color-app-button: var(--light-hue), 5%, 100%;
--color-app-divider: var(--light-hue), 5%, 80%;
--color-app-selected: var(--light-hue), 5%, 93%;
--color-app-selected-item: var(--light-hue), 5%, 96%;
--color-app-hover: var(--light-hue), 5%, 97%;
--color-app-active: var(--light-hue), 5%, 87%;
--color-app-shade: var(--light-hue), 15%, 50%;

View File

@@ -73,6 +73,7 @@ module.exports = function (app, options) {
divider: alpha('--color-app-divider'),
button: alpha('--color-app-button'),
selected: alpha('--color-app-selected'),
selectedItem: alpha('--color-app-selected-item'),
hover: alpha('--color-app-hover'),
active: alpha('--color-app-active'),
shade: alpha('--color-app-shade'),