mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-22 15:40:07 -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:
@@ -167,9 +167,7 @@ impl JobManager {
|
|||||||
|
|
||||||
for worker in self.running_workers.read().await.values() {
|
for worker in self.running_workers.read().await.values() {
|
||||||
let report = worker.lock().await.report();
|
let report = worker.lock().await.report();
|
||||||
if !report.is_background {
|
ret.push(report);
|
||||||
ret.push(report);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -428,25 +428,26 @@ pub async fn light_scan_location(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let location_base_data = location::Data::from(&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
|
library
|
||||||
.spawn_job(
|
.spawn_job(ShallowIndexerJobInit {
|
||||||
Job::new_with_action(
|
location,
|
||||||
ShallowIndexerJobInit {
|
sub_path: sub_path.clone(),
|
||||||
location,
|
})
|
||||||
sub_path: sub_path.clone(),
|
.await
|
||||||
},
|
.unwrap_or(());
|
||||||
"light_scan_location",
|
library
|
||||||
)
|
.spawn_job(ShallowFileIdentifierJobInit {
|
||||||
.queue_next(ShallowFileIdentifierJobInit {
|
location: location_base_data.clone(),
|
||||||
location: location_base_data.clone(),
|
sub_path: sub_path.clone(),
|
||||||
sub_path: sub_path.clone(),
|
})
|
||||||
})
|
.await
|
||||||
.queue_next(ShallowThumbnailerJobInit {
|
.unwrap_or(());
|
||||||
location: location_base_data,
|
library
|
||||||
sub_path,
|
.spawn_job(ShallowThumbnailerJobInit {
|
||||||
}),
|
location: location_base_data.clone(),
|
||||||
)
|
sub_path: sub_path.clone(),
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const GridViewItem = memo(({ data, selected, index, ...props }: GridViewItemProp
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'mb-1 flex items-center justify-center justify-items-center rounded-lg border-2 border-transparent text-center active:translate-y-[1px]',
|
'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
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'group relative flex aspect-square items-center justify-center hover:bg-app-selected/20',
|
'hover:bg-app-selectedItem group relative flex aspect-square items-center justify-center',
|
||||||
selected && 'bg-app-selected/20'
|
selected && 'bg-app-selectedItem'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FileThumb
|
<FileThumb
|
||||||
@@ -82,7 +82,7 @@ export default () => {
|
|||||||
measureElement: () => itemSize,
|
measureElement: () => itemSize,
|
||||||
paddingStart: gridPadding,
|
paddingStart: gridPadding,
|
||||||
paddingEnd: gridPadding,
|
paddingEnd: gridPadding,
|
||||||
overscan: !dismissibleNoticeStore.mediaView ? 2 : 1
|
overscan: !dismissibleNoticeStore.mediaView ? 8 : 4
|
||||||
});
|
});
|
||||||
|
|
||||||
const columnVirtualizer = useVirtualizer({
|
const columnVirtualizer = useVirtualizer({
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { animated, useTransition } from '@react-spring/web';
|
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 { useEffect, useRef, useState } from 'react';
|
||||||
import { subscribeKey } from 'valtio/utils';
|
import { subscribeKey } from 'valtio/utils';
|
||||||
import { ExplorerItem } from '@sd/client';
|
import { ExplorerItem } from '@sd/client';
|
||||||
import { getExplorerStore } from '~/hooks';
|
import { getExplorerStore } from '~/hooks';
|
||||||
import FileThumb from './File/Thumb';
|
import FileThumb from './File/Thumb';
|
||||||
|
import { Button } from '@sd/ui';
|
||||||
|
|
||||||
const AnimatedDialogOverlay = animated(Dialog.Overlay);
|
const AnimatedDialogOverlay = animated(Dialog.Overlay);
|
||||||
const AnimatedDialogContent = animated(Dialog.Content);
|
const AnimatedDialogContent = animated(Dialog.Content);
|
||||||
@@ -42,12 +43,12 @@ export function QuickPreview({ transformOrigin }: QuickPreviewProps) {
|
|||||||
const transitions = useTransition(isOpen, {
|
const transitions = useTransition(isOpen, {
|
||||||
from: {
|
from: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transform: `translateY(20px)`,
|
transform: `translateY(20px) scale(0.9)`,
|
||||||
transformOrigin: transformOrigin || 'bottom'
|
transformOrigin: transformOrigin || 'center top'
|
||||||
},
|
},
|
||||||
enter: { opacity: 1, transform: `translateY(0px)` },
|
enter: { opacity: 1, transform: `translateY(0px) scale(1)` },
|
||||||
leave: { opacity: 0, transform: `translateY(20px)` },
|
leave: { opacity: 0, transform: `translateY(40px) scale(0.9)` },
|
||||||
config: { mass: 0.4, tension: 200, friction: 10, bounce: 0 }
|
config: { mass: 0.2, tension: 300, friction: 20, bounce: 0 }
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<nav className="flex w-full flex-row">
|
||||||
<Dialog.Close
|
<Dialog.Close
|
||||||
className="ml-2 text-ink-dull"
|
className="m-2"
|
||||||
aria-label="Close"
|
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.Close>
|
||||||
<Dialog.Title className="mx-auto my-1 font-bold">
|
<Dialog.Title className="mx-auto my-1 font-bold">
|
||||||
Preview -{' '}
|
Preview -{' '}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ interface Props {
|
|||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
isFetchingNextPage?: boolean;
|
isFetchingNextPage?: boolean;
|
||||||
viewClassName?: string;
|
viewClassName?: string;
|
||||||
|
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo((props: Props) => {
|
export default memo((props: Props) => {
|
||||||
@@ -97,7 +98,7 @@ export default memo((props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={props.scrollRef || scrollRef}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'custom-scroll explorer-scroll h-screen',
|
'custom-scroll explorer-scroll h-screen',
|
||||||
layoutMode === 'grid' && 'overflow-x-hidden',
|
layoutMode === 'grid' && 'overflow-x-hidden',
|
||||||
@@ -110,7 +111,7 @@ export default memo((props: Props) => {
|
|||||||
<ViewContext.Provider
|
<ViewContext.Provider
|
||||||
value={{
|
value={{
|
||||||
data: props.data,
|
data: props.data,
|
||||||
scrollRef: scrollRef,
|
scrollRef: props.scrollRef || scrollRef,
|
||||||
onLoadMore: props.onLoadMore,
|
onLoadMore: props.onLoadMore,
|
||||||
hasNextPage: props.hasNextPage,
|
hasNextPage: props.hasNextPage,
|
||||||
isFetchingNextPage: props.isFetchingNextPage
|
isFetchingNextPage: props.isFetchingNextPage
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface Props {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
inspectorClassName?: string;
|
inspectorClassName?: string;
|
||||||
explorerClassName?: string;
|
explorerClassName?: string;
|
||||||
|
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Explorer(props: Props) {
|
export default function Explorer(props: Props) {
|
||||||
@@ -63,6 +64,7 @@ export default function Explorer(props: Props) {
|
|||||||
<ExplorerContextMenu>
|
<ExplorerContextMenu>
|
||||||
{props.items && (
|
{props.items && (
|
||||||
<View
|
<View
|
||||||
|
scrollRef={props.scrollRef}
|
||||||
data={props.items}
|
data={props.items}
|
||||||
onLoadMore={props.onLoadMore}
|
onLoadMore={props.onLoadMore}
|
||||||
hasNextPage={props.hasNextPage}
|
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 {
|
ul.groupedjob {
|
||||||
position: relative;
|
position: relative;
|
||||||
&::before {
|
&::before {
|
||||||
@@ -6,7 +16,7 @@ ul.groupedjob {
|
|||||||
top: 37px;
|
top: 37px;
|
||||||
height: 48.75px;
|
height: 48.75px;
|
||||||
width: 11px;
|
width: 11px;
|
||||||
border-left: 1px solid rgba(50, 51, 67, 1);
|
border-left: 1px solid var(--border-color);
|
||||||
content: '';
|
content: '';
|
||||||
left: 25px;
|
left: 25px;
|
||||||
}
|
}
|
||||||
@@ -26,7 +36,7 @@ ul.groupedjob {
|
|||||||
height: 37.5px;
|
height: 37.5px;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
border-left: 1px solid rgba(50, 51, 67, 1);
|
border-left: 1px solid var(--border-color);
|
||||||
content: '';
|
content: '';
|
||||||
left: 25px;
|
left: 25px;
|
||||||
}
|
}
|
||||||
@@ -36,10 +46,10 @@ ul.groupedjob {
|
|||||||
top: 23.5px;
|
top: 23.5px;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
border-bottom: 1px solid rgba(50, 51, 67, 1);
|
border-bottom: 1px solid var(--border-color);
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
left: 10px;
|
left: -14px;
|
||||||
}
|
}
|
||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -47,7 +57,7 @@ ul.groupedjob {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
border-left: 1px solid rgba(50, 51, 67, 1);
|
border-left: 1px solid var(--border-color);
|
||||||
content: '';
|
content: '';
|
||||||
left: 25px;
|
left: 25px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,28 +32,31 @@ const getNiceData = (
|
|||||||
name: isGroup
|
name: isGroup
|
||||||
? 'Indexing paths'
|
? 'Indexing paths'
|
||||||
: job.metadata?.location_path
|
: job.metadata?.location_path
|
||||||
? `Indexed paths at ${job.metadata?.location_path} `
|
? `Indexed paths at ${job.metadata?.location_path} `
|
||||||
: `Processing added location...`,
|
: `Processing added location...`,
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
filesDiscovered: `${numberWithCommas(
|
filesDiscovered: `${numberWithCommas(
|
||||||
job.metadata?.total_paths || 0
|
job.metadata?.total_paths || 0
|
||||||
)} ${JobCountTextCondition(job, 'path')}`
|
)} ${JobCountTextCondition(job, 'path')}`
|
||||||
},
|
},
|
||||||
thumbnailer: {
|
thumbnailer: {
|
||||||
name: `${
|
name: `${job.status === 'Running' || job.status === 'Queued'
|
||||||
job.status === 'Running' || job.status === 'Queued'
|
? 'Generating thumbnails'
|
||||||
? 'Generating thumbnails'
|
: 'Generated thumbnails'
|
||||||
: 'Generated thumbnails'
|
}`,
|
||||||
}`,
|
icon: Camera,
|
||||||
|
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||||
|
},
|
||||||
|
shallow_thumbnailer: {
|
||||||
|
name: `Generating thumbnails for current directory`,
|
||||||
icon: Camera,
|
icon: Camera,
|
||||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||||
},
|
},
|
||||||
file_identifier: {
|
file_identifier: {
|
||||||
name: `${
|
name: `${job.status === 'Running' || job.status === 'Queued'
|
||||||
job.status === 'Running' || job.status === 'Queued'
|
? 'Extracting metadata'
|
||||||
? 'Extracting metadata'
|
: 'Extracted metadata'
|
||||||
: 'Extracted metadata'
|
}`,
|
||||||
}`,
|
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
filesDiscovered:
|
filesDiscovered:
|
||||||
job.message ||
|
job.message ||
|
||||||
@@ -134,7 +137,7 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
|
|||||||
isGroup ? `joblistitem pr-3 pt-0` : 'p-3'
|
isGroup ? `joblistitem pr-3 pt-0` : 'p-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="ml-7 flex">
|
<div className="flex">
|
||||||
<div>
|
<div>
|
||||||
<niceData.icon
|
<niceData.icon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -72,10 +72,8 @@ function JobGroup({ data, clearJob }: JobGroupProps) {
|
|||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<p className="truncate font-semibold">
|
<p className="truncate font-semibold">
|
||||||
{allJobsCompleted
|
{allJobsCompleted
|
||||||
? `Added location "${
|
? `Added location "${data.metadata.init.location.name || ''}"`
|
||||||
data.metadata.init.location.name || ''
|
: `Indexing "${data.metadata.init.location.name || ''}"`}
|
||||||
}"`
|
|
||||||
: 'Processing added location...'}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="my-[2px] text-ink-faint">
|
<p className="my-[2px] text-ink-faint">
|
||||||
<b>{tasks.total} </b>
|
<b>{tasks.total} </b>
|
||||||
|
|||||||
@@ -81,15 +81,15 @@ export function JobsManager() {
|
|||||||
</PopoverClose>
|
</PopoverClose>
|
||||||
</div>
|
</div>
|
||||||
<div className="no-scrollbar h-full overflow-x-hidden">
|
<div className="no-scrollbar h-full overflow-x-hidden">
|
||||||
|
{runningIndividualJobs?.map((job) => (
|
||||||
|
<Job key={job.id} job={job} />
|
||||||
|
))}
|
||||||
{groupedJobs.map((data) => (
|
{groupedJobs.map((data) => (
|
||||||
<JobGroup key={data.id} data={data} clearJob={clearJobHandler} />
|
<JobGroup key={data.id} data={data} clearJob={clearJobHandler} />
|
||||||
))}
|
))}
|
||||||
{orphanJobs?.map((job) => (
|
{orphanJobs?.map((job) => (
|
||||||
<Job key={job?.id} job={job} />
|
<Job key={job?.id} job={job} />
|
||||||
))}
|
))}
|
||||||
{runningIndividualJobs?.map((job) => (
|
|
||||||
<Job key={job.id} job={job} />
|
|
||||||
))}
|
|
||||||
{individualJobs?.map((job) => (
|
{individualJobs?.map((job) => (
|
||||||
<Job clearJob={clearJobHandler} key={job.id} job={job} />
|
<Job clearJob={clearJobHandler} key={job.id} job={job} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default () => {
|
|||||||
}
|
}
|
||||||
// we override the sidebar dropdown item's hover styles
|
// we override the sidebar dropdown item's hover styles
|
||||||
// because the dark style clashes with the sidebar
|
// 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
|
alignToTrigger
|
||||||
>
|
>
|
||||||
{libraries.data?.map((lib) => (
|
{libraries.data?.map((lib) => (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { MacTrafficLights, NavigationButtons } from '~/components';
|
import { MacTrafficLights } from '~/components';
|
||||||
import { useOperatingSystem } from '~/hooks';
|
import { useOperatingSystem } from '~/hooks';
|
||||||
import Contents from './Contents';
|
import Contents from './Contents';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
@@ -18,11 +18,7 @@ export default () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showControls && <MacTrafficLights className="absolute left-[13px] top-[13px] z-50" />}
|
{showControls && <MacTrafficLights className="absolute left-[13px] top-[13px] z-50" />}
|
||||||
{(os !== 'browser' || showControls) && (
|
{os === 'macOS' && <div data-tauri-drag-region className="h-5 w-full" />}
|
||||||
<div className="mt-[-4px] flex justify-end">
|
|
||||||
<NavigationButtons />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<LibrariesDropdown />
|
<LibrariesDropdown />
|
||||||
<Contents />
|
<Contents />
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import { RefObject, createContext, useRef } from 'react';
|
import { RefObject, createContext, useContext, useRef } from 'react';
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
import TopBar from '.';
|
import TopBar from '.';
|
||||||
|
|
||||||
interface TopBarContext {
|
interface TopBarContext {
|
||||||
topBarChildrenRef: RefObject<HTMLDivElement> | null;
|
left: RefObject<HTMLDivElement>;
|
||||||
|
right: RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopBarContext = createContext<TopBarContext>({
|
const TopBarContext = createContext<TopBarContext | null>(null);
|
||||||
topBarChildrenRef: null
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const left = useRef<HTMLDivElement>(null);
|
||||||
|
const right = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopBarContext.Provider value={{ topBarChildrenRef: ref }}>
|
<TopBarContext.Provider value={{ left, right }}>
|
||||||
<TopBar ref={ref} />
|
<TopBar leftRef={left} rightRef={right} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</TopBarContext.Provider>
|
</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 { ArrowLeft, ArrowRight } from 'phosphor-react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { Button, Tooltip } from '@sd/ui';
|
|
||||||
import { useSearchStore } from '~/hooks';
|
import { useSearchStore } from '~/hooks';
|
||||||
|
import TopBarButton from './TopBarButton';
|
||||||
|
|
||||||
export const NavigationButtons = () => {
|
export const NavigationButtons = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -9,26 +10,26 @@ export const NavigationButtons = () => {
|
|||||||
const idx = history.state.idx as number;
|
const idx = history.state.idx as number;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div data-tauri-drag-region className="flex">
|
||||||
<Tooltip label="Navigate back">
|
<Tooltip label="Navigate back">
|
||||||
<Button
|
<TopBarButton
|
||||||
size="icon"
|
rounding='left'
|
||||||
className="text-[14px] text-ink-dull"
|
// className="text-[14px] text-ink-dull"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
disabled={isFocused || idx === 0}
|
disabled={isFocused || idx === 0}
|
||||||
>
|
>
|
||||||
<ArrowLeft weight="bold" />
|
<ArrowLeft size={14} className='m-[4px]' weight="bold" />
|
||||||
</Button>
|
</TopBarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Navigate forward">
|
<Tooltip label="Navigate forward">
|
||||||
<Button
|
<TopBarButton
|
||||||
size="icon"
|
rounding='right'
|
||||||
className="text-[14px] text-ink-dull"
|
// className="text-[14px] text-ink-dull"
|
||||||
onClick={() => navigate(1)}
|
onClick={() => navigate(1)}
|
||||||
disabled={isFocused || idx === history.length - 1}
|
disabled={isFocused || idx === history.length - 1}
|
||||||
>
|
>
|
||||||
<ArrowRight weight="bold" />
|
<ArrowRight size={14} className='m-[4px]' weight="bold" />
|
||||||
</Button>
|
</TopBarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</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 { DotsThreeCircle } from 'phosphor-react';
|
||||||
import React, { HTMLAttributes, forwardRef } from 'react';
|
import React, { HTMLAttributes, forwardRef } from 'react';
|
||||||
import { Popover } from '@sd/ui';
|
import { Popover } from '@sd/ui';
|
||||||
import { TOP_BAR_ICON_STYLE, ToolOption } from '.';
|
|
||||||
import TopBarButton, { TopBarButtonProps } from './TopBarButton';
|
import TopBarButton, { TopBarButtonProps } from './TopBarButton';
|
||||||
|
import { TOP_BAR_ICON_STYLE, ToolOption } from './TopBarOptions';
|
||||||
|
|
||||||
const GroupTool = forwardRef<
|
const GroupTool = forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useContext, useLayoutEffect, useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { Popover, Tooltip } from '@sd/ui';
|
import { Popover, Tooltip } from '@sd/ui';
|
||||||
import { ToolOption } from '.';
|
|
||||||
import { TopBarContext } from './Layout';
|
|
||||||
import TopBarButton from './TopBarButton';
|
import TopBarButton from './TopBarButton';
|
||||||
import TopBarMobile from './TopBarMobile';
|
import TopBarMobile from './TopBarMobile';
|
||||||
|
|
||||||
interface TopBarChildrenProps {
|
export interface ToolOption {
|
||||||
toolOptions?: ToolOption[][];
|
icon: JSX.Element;
|
||||||
|
onClick?: () => void;
|
||||||
|
individual?: boolean;
|
||||||
|
toolTipLabel: string;
|
||||||
|
topBarActive?: boolean;
|
||||||
|
popOverComponent?: JSX.Element;
|
||||||
|
showAtResolution: ShowAtResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ toolOptions }: TopBarChildrenProps) => {
|
export type ShowAtResolution = 'sm:flex' | 'md:flex' | 'lg:flex' | 'xl:flex' | '2xl:flex';
|
||||||
const ctx = useContext(TopBarContext);
|
interface TopBarChildrenProps {
|
||||||
const target = ctx.topBarChildrenRef?.current;
|
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 [windowSize, setWindowSize] = useState(0);
|
||||||
const toolsNotSmFlex = toolOptions
|
const toolsNotSmFlex = options
|
||||||
?.flatMap((group) => group)
|
?.flatMap((group) => group)
|
||||||
.filter((t) => t.showAtResolution !== 'sm:flex');
|
.filter((t) => t.showAtResolution !== 'sm:flex');
|
||||||
|
|
||||||
@@ -28,14 +36,10 @@ export default ({ toolOptions }: TopBarChildrenProps) => {
|
|||||||
return () => window.removeEventListener('resize', handleResize);
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!target) {
|
return (
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<div data-tauri-drag-region className="flex w-full flex-row justify-end">
|
<div data-tauri-drag-region className="flex w-full flex-row justify-end">
|
||||||
<div data-tauri-drag-region className={`flex gap-0`}>
|
<div data-tauri-drag-region className={`flex gap-0`}>
|
||||||
{toolOptions?.map((group, groupIndex) => {
|
{options?.map((group, groupIndex) => {
|
||||||
return group.map(
|
return group.map(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -49,14 +53,14 @@ export default ({ toolOptions }: TopBarChildrenProps) => {
|
|||||||
},
|
},
|
||||||
index
|
index
|
||||||
) => {
|
) => {
|
||||||
const groupCount = toolOptions.length;
|
const groupCount = options.length;
|
||||||
const roundingCondition = individual
|
const roundingCondition = individual
|
||||||
? 'both'
|
? 'both'
|
||||||
: index === 0
|
: index === 0
|
||||||
? 'left'
|
? 'left'
|
||||||
: index === group.length - 1
|
: index === group.length - 1
|
||||||
? 'right'
|
? 'right'
|
||||||
: 'none';
|
: 'none';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
@@ -109,12 +113,11 @@ export default ({ toolOptions }: TopBarChildrenProps) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<TopBarMobile
|
<TopBarMobile
|
||||||
toolOptions={toolOptions}
|
toolOptions={options}
|
||||||
className={`${
|
className={
|
||||||
windowSize <= 1279 && (toolsNotSmFlex?.length as number) > 0 ? 'flex' : 'hidden'
|
windowSize <= 1279 && (toolsNotSmFlex?.length as number) > 0 ? 'flex' : 'hidden'
|
||||||
}`}
|
}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>
|
||||||
target
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,37 +1,33 @@
|
|||||||
import { forwardRef } from 'react';
|
import { RefObject } from 'react';
|
||||||
|
import { NavigationButtons } from './NavigationButtons';
|
||||||
import SearchBar from './SearchBar';
|
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;
|
export const TOP_BAR_HEIGHT = 46;
|
||||||
|
|
||||||
const TopBar = forwardRef<HTMLDivElement>((_, ref) => {
|
interface Props {
|
||||||
|
leftRef?: RefObject<HTMLDivElement>;
|
||||||
|
rightRef?: RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopBar = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="
|
className="
|
||||||
duration-250 top-bar-blur absolute left-0 top-0 z-50 flex
|
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
|
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
|
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 />
|
<SearchBar />
|
||||||
<div className="flex-1" ref={ref} />
|
<div className="flex-1" ref={props.rightRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default TopBar;
|
export default TopBar;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useInfiniteQuery } from '@tanstack/react-query';
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useKey } from 'rooks';
|
import { useKey } from 'rooks';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useLibraryContext, useLibraryMutation, useRspcLibraryContext } from '@sd/client';
|
import { useLibraryContext, useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from '@sd/client';
|
||||||
import { dialogManager } from '@sd/ui';
|
import { Folder, dialogManager } from '@sd/ui';
|
||||||
import {
|
import {
|
||||||
getExplorerStore,
|
getExplorerStore,
|
||||||
useExplorerStore,
|
useExplorerStore,
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
import Explorer from '../Explorer';
|
import Explorer from '../Explorer';
|
||||||
import DeleteDialog from '../Explorer/File/DeleteDialog';
|
import DeleteDialog from '../Explorer/File/DeleteDialog';
|
||||||
import { useExplorerOrder, useExplorerSearchParams } from '../Explorer/util';
|
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({
|
const PARAMS = z.object({
|
||||||
id: z.coerce.number()
|
id: z.coerce.number()
|
||||||
@@ -25,6 +26,8 @@ export const Component = () => {
|
|||||||
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
|
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
|
||||||
useExplorerTopBarOptions();
|
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
|
// we destructure this since `mutate` is a stable reference but the object it's in is not
|
||||||
const { mutate: quickRescan } = useLibraryMutation('locations.quickRescan');
|
const { mutate: quickRescan } = useLibraryMutation('locations.quickRescan');
|
||||||
|
|
||||||
@@ -55,8 +58,20 @@ export const Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBarChildren
|
<TopBarPortal
|
||||||
toolOptions={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
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">
|
<div className="relative flex w-full flex-col">
|
||||||
<Explorer
|
<Explorer
|
||||||
@@ -109,3 +124,13 @@ const useItems = () => {
|
|||||||
|
|
||||||
return { query, items };
|
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}
|
onClick={onClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex shrink-0 items-center rounded-md px-1.5 py-1 text-sm',
|
'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" />
|
<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 Explorer from '../Explorer';
|
||||||
import { SEARCH_PARAMS, useExplorerOrder } from '../Explorer/util';
|
import { SEARCH_PARAMS, useExplorerOrder } from '../Explorer/util';
|
||||||
import { usePageLayout } from '../PageLayout';
|
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 CategoryButton from '../overview/CategoryButton';
|
||||||
import Statistics from '../overview/Statistics';
|
import Statistics from '../overview/Statistics';
|
||||||
|
|
||||||
@@ -92,12 +93,12 @@ export const Component = () => {
|
|||||||
favorite: isFavoritesCategory ? true : undefined,
|
favorite: isFavoritesCategory ? true : undefined,
|
||||||
...(explorerStore.layoutMode === 'media'
|
...(explorerStore.layoutMode === 'media'
|
||||||
? {
|
? {
|
||||||
kind: [5, 7].includes(kind)
|
kind: [5, 7].includes(kind)
|
||||||
? [kind]
|
? [kind]
|
||||||
: isFavoritesCategory
|
: isFavoritesCategory
|
||||||
? [5, 7]
|
? [5, 7]
|
||||||
: [5, 7, kind]
|
: [5, 7, kind]
|
||||||
}
|
}
|
||||||
: { kind: isFavoritesCategory ? [] : [kind] })
|
: { kind: isFavoritesCategory ? [] : [kind] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,19 +129,25 @@ export const Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBarChildren
|
<TopBarPortal
|
||||||
toolOptions={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
right={
|
||||||
|
<TopBarOptions
|
||||||
|
options={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Explorer
|
<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"
|
viewClassName="!pl-0 !pt-0 !h-auto"
|
||||||
items={items}
|
items={items}
|
||||||
onLoadMore={query.fetchNextPage}
|
onLoadMore={query.fetchNextPage}
|
||||||
hasNextPage={query.hasNextPage}
|
hasNextPage={query.hasNextPage}
|
||||||
isFetchingNextPage={query.isFetchingNextPage}
|
isFetchingNextPage={query.isFetchingNextPage}
|
||||||
|
scrollRef={page?.ref}
|
||||||
>
|
>
|
||||||
<Statistics />
|
<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) => {
|
{categories.data?.map((category) => {
|
||||||
const iconString = CategoryToIcon[category.name] || 'Document';
|
const iconString = CategoryToIcon[category.name] || 'Document';
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
} from '~/hooks';
|
} from '~/hooks';
|
||||||
import Explorer from './Explorer';
|
import Explorer from './Explorer';
|
||||||
import { getExplorerItemData } from './Explorer/util';
|
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({
|
const SEARCH_PARAMS = z.object({
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
@@ -49,12 +50,16 @@ const ExplorerStuff = memo((props: { args: SearchArgs }) => {
|
|||||||
<>
|
<>
|
||||||
{items && items.length > 0 ? (
|
{items && items.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<TopBarChildren
|
<TopBarPortal
|
||||||
toolOptions={[
|
right={
|
||||||
explorerViewOptions,
|
<TopBarOptions
|
||||||
explorerToolOptions,
|
options={[
|
||||||
explorerControlOptions
|
explorerViewOptions,
|
||||||
]}
|
explorerToolOptions,
|
||||||
|
explorerControlOptions
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Explorer items={items} />
|
<Explorer items={items} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { tw } from '@sd/ui';
|
|||||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||||
import Icon from '../Layout/Sidebar/Icon';
|
import Icon from '../Layout/Sidebar/Icon';
|
||||||
import SidebarLink from '../Layout/Sidebar/Link';
|
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 Heading = tw.div`mb-1 ml-1 text-xs font-semibold text-gray-400`;
|
||||||
const Section = tw.div`space-y-0.5`;
|
const Section = tw.div`space-y-0.5`;
|
||||||
@@ -26,10 +27,13 @@ export default () => {
|
|||||||
return (
|
return (
|
||||||
<div className="custom-scroll no-scrollbar h-full w-60 max-w-[180px] shrink-0 border-r border-app-line/50 pb-5">
|
<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' ? (
|
{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="h-3" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-6 px-4 py-3">
|
<div className="space-y-6 px-4 py-3">
|
||||||
<Section>
|
<Section>
|
||||||
<Heading>Client</Heading>
|
<Heading>Client</Heading>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export * from './ColorPicker';
|
|||||||
export * from './DismissibleNotice';
|
export * from './DismissibleNotice';
|
||||||
export * from './DragRegion';
|
export * from './DragRegion';
|
||||||
export * from './ExternalObject';
|
export * from './ExternalObject';
|
||||||
export * from './NavigationButtons';
|
|
||||||
export * from './PasswordMeter';
|
export * from './PasswordMeter';
|
||||||
export * from './SubtleButton';
|
export * from './SubtleButton';
|
||||||
export * from './TrafficLights';
|
export * from './TrafficLights';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { proxy, useSnapshot } from 'valtio';
|
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';
|
import { resetStore } from '@sd/client/src/stores/util';
|
||||||
|
|
||||||
type UnionKeys<T> = T extends any ? keyof T : never;
|
type UnionKeys<T> = T extends any ? keyof T : never;
|
||||||
@@ -14,7 +14,7 @@ export enum ExplorerKind {
|
|||||||
|
|
||||||
export type CutCopyType = 'Cut' | 'Copy';
|
export type CutCopyType = 'Cut' | 'Copy';
|
||||||
|
|
||||||
export type ExplorerOrderByKeys = UnionKeys<Ordering> | 'none';
|
export type ExplorerOrderByKeys = UnionKeys<FilePathSearchOrdering> | 'none';
|
||||||
|
|
||||||
export type ExplorerDirection = 'asc' | 'desc';
|
export type ExplorerDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
@@ -28,8 +28,6 @@ const state = {
|
|||||||
tagAssignMode: false,
|
tagAssignMode: false,
|
||||||
showInspector: false,
|
showInspector: false,
|
||||||
multiSelectIndexes: [] as number[],
|
multiSelectIndexes: [] as number[],
|
||||||
contextMenuObjectId: null as number | null,
|
|
||||||
contextMenuActiveObject: null as object | null,
|
|
||||||
newThumbnails: {} as Record<string, boolean | undefined>,
|
newThumbnails: {} as Record<string, boolean | undefined>,
|
||||||
cutCopyState: {
|
cutCopyState: {
|
||||||
sourcePath: '', // this is used solely for preventing copy/cutting to the same path (as that will truncate the file)
|
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,
|
mediaAspectSquare: true,
|
||||||
orderBy: 'dateCreated' as ExplorerOrderByKeys,
|
orderBy: 'dateCreated' as ExplorerOrderByKeys,
|
||||||
orderByDirection: 'desc' as ExplorerDirection,
|
orderByDirection: 'desc' as ExplorerDirection,
|
||||||
groupBy: 'none'
|
groupBy: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.
|
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ import {
|
|||||||
Tag
|
Tag
|
||||||
} from 'phosphor-react';
|
} from 'phosphor-react';
|
||||||
import OptionsPanel from '~/app/$libraryId/Explorer/OptionsPanel';
|
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 { KeyManager } from '../app/$libraryId/KeyManager';
|
||||||
import { getExplorerStore, useExplorerStore } from './useExplorerStore';
|
import { getExplorerStore, useExplorerStore } from './useExplorerStore';
|
||||||
|
import { useLibraryMutation } from '@sd/client';
|
||||||
|
|
||||||
export const useExplorerTopBarOptions = () => {
|
export const useExplorerTopBarOptions = () => {
|
||||||
const explorerStore = useExplorerStore();
|
const explorerStore = useExplorerStore();
|
||||||
|
|
||||||
|
const reload = useLibraryMutation('locations.quickRescan');
|
||||||
|
|
||||||
const explorerViewOptions: ToolOption[] = [
|
const explorerViewOptions: ToolOption[] = [
|
||||||
{
|
{
|
||||||
toolTipLabel: 'Grid view',
|
toolTipLabel: 'Grid view',
|
||||||
@@ -94,7 +97,12 @@ export const useExplorerTopBarOptions = () => {
|
|||||||
showAtResolution: 'xl:flex'
|
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} />,
|
icon: <ArrowClockwise className={TOP_BAR_ICON_STYLE} />,
|
||||||
individual: true,
|
individual: true,
|
||||||
showAtResolution: 'xl:flex'
|
showAtResolution: 'xl:flex'
|
||||||
|
|||||||
@@ -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 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.
|
* This denotes the `StoredKey` version.
|
||||||
*/
|
*/
|
||||||
@@ -117,12 +119,12 @@ export type EncryptedKey = number[]
|
|||||||
|
|
||||||
export type PeerId = string
|
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 GenerateThumbsForLocationArgs = { id: number; path: string }
|
||||||
|
|
||||||
export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig }
|
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.
|
* 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 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 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 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 }
|
export type BuildInfo = { version: string; commit: string }
|
||||||
|
|
||||||
@@ -224,12 +220,14 @@ export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
|
|||||||
|
|
||||||
export type ObjectSearchOrdering = { dateAccessed: boolean }
|
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 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 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
|
* 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 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 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[] }
|
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 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 SearchData<T> = { cursor: number[] | null; items: T[] }
|
||||||
|
|
||||||
export type OptionalRange<T> = { from: T | null; to: T | null }
|
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 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.
|
* This defines all available password hashing algorithms.
|
||||||
*/
|
*/
|
||||||
export type HashingAlgorithm = { name: "Argon2id"; params: Params } | { name: "BalloonBlake3"; params: Params }
|
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 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 })[] }
|
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 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 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 }
|
export type RelationOperation = { relation_item: string; relation_group: string; relation: string; data: RelationOperationData }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
--color-app-button: var(--dark-hue), 15%, 23%;
|
--color-app-button: var(--dark-hue), 15%, 23%;
|
||||||
--color-app-hover: var(--dark-hue), 15%, 25%;
|
--color-app-hover: var(--dark-hue), 15%, 25%;
|
||||||
--color-app-selected: var(--dark-hue), 15%, 26%;
|
--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-active: var(--dark-hue), 15%, 30%;
|
||||||
--color-app-shade: var(--dark-hue), 15%, 0%;
|
--color-app-shade: var(--dark-hue), 15%, 0%;
|
||||||
--color-app-frame: var(--dark-hue), 15%, 25%;
|
--color-app-frame: var(--dark-hue), 15%, 25%;
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
// main
|
// main
|
||||||
--color-app: var(--light-hue), 5%, 100%;
|
--color-app: var(--light-hue), 5%, 100%;
|
||||||
--color-app-box: var(--light-hue), 5%, 98%;
|
--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-light-box: var(--light-hue), 5%, 100%;
|
||||||
--color-app-overlay: var(--light-hue), 5%, 100%;
|
--color-app-overlay: var(--light-hue), 5%, 100%;
|
||||||
--color-app-input: 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-button: var(--light-hue), 5%, 100%;
|
||||||
--color-app-divider: var(--light-hue), 5%, 80%;
|
--color-app-divider: var(--light-hue), 5%, 80%;
|
||||||
--color-app-selected: var(--light-hue), 5%, 93%;
|
--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-hover: var(--light-hue), 5%, 97%;
|
||||||
--color-app-active: var(--light-hue), 5%, 87%;
|
--color-app-active: var(--light-hue), 5%, 87%;
|
||||||
--color-app-shade: var(--light-hue), 15%, 50%;
|
--color-app-shade: var(--light-hue), 15%, 50%;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ module.exports = function (app, options) {
|
|||||||
divider: alpha('--color-app-divider'),
|
divider: alpha('--color-app-divider'),
|
||||||
button: alpha('--color-app-button'),
|
button: alpha('--color-app-button'),
|
||||||
selected: alpha('--color-app-selected'),
|
selected: alpha('--color-app-selected'),
|
||||||
|
selectedItem: alpha('--color-app-selected-item'),
|
||||||
hover: alpha('--color-app-hover'),
|
hover: alpha('--color-app-hover'),
|
||||||
active: alpha('--color-app-active'),
|
active: alpha('--color-app-active'),
|
||||||
shade: alpha('--color-app-shade'),
|
shade: alpha('--color-app-shade'),
|
||||||
|
|||||||
Reference in New Issue
Block a user