[ENG-1418] Explorer settings (#1764)

* preferences

* useShortcut and move location id to function

* void

* rescan
This commit is contained in:
nikec
2023-11-10 11:23:51 +01:00
committed by GitHub
parent 826401efae
commit b8d13a8cfc
6 changed files with 125 additions and 90 deletions

View File

@@ -1,4 +1,4 @@
import { createContext, PropsWithChildren, useContext } from 'react';
import { ContextType, createContext, PropsWithChildren, useContext } from 'react';
import { Ordering } from './store';
import { UseExplorer } from './useExplorer';
@@ -9,12 +9,16 @@ import { UseExplorer } from './useExplorer';
*/
const ExplorerContext = createContext<UseExplorer<Ordering> | null>(null);
export const useExplorerContext = () => {
type ExplorerContext = NonNullable<ContextType<typeof ExplorerContext>>;
export const useExplorerContext = <T extends boolean = true>(
{ suspense }: { suspense?: T } = { suspense: true as T }
) => {
const ctx = useContext(ExplorerContext);
if (ctx === null) throw new Error('ExplorerContext.Provider not found!');
if (suspense && ctx === null) throw new Error('ExplorerContext.Provider not found!');
return ctx;
return ctx as T extends true ? ExplorerContext : ExplorerContext | undefined;
};
export const ExplorerContextProvider = <TExplorer extends UseExplorer<any>>({

View File

@@ -1,5 +1,4 @@
import {
ArrowClockwise,
Icon,
Key,
MonitorPlay,
@@ -13,10 +12,9 @@ import clsx from 'clsx';
import { useMemo } from 'react';
import { useDocumentEventListener } from 'rooks';
import { ExplorerLayout } from '@sd/client';
import { ModifierKeys, toast } from '@sd/ui';
import { useKeybind, useKeyMatcher, useOperatingSystem } from '~/hooks';
import { toast } from '@sd/ui';
import { useKeyMatcher } from '~/hooks';
import { useQuickRescan } from '../../../hooks/useQuickRescan';
import { KeyManager } from '../KeyManager';
import TopBarOptions, { ToolOption, TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
import { useExplorerContext } from './Context';
@@ -34,7 +32,6 @@ export const useExplorerTopBarOptions = () => {
const explorer = useExplorerContext();
const controlIcon = useKeyMatcher('Meta').icon;
const settings = explorer.useSettingsSnapshot();
const rescan = useQuickRescan();
const viewOptions = useMemo(
() =>
@@ -49,7 +46,8 @@ export const useExplorerTopBarOptions = () => {
toolTipLabel: `${layout} view`,
icon: <Icon className={TOP_BAR_ICON_STYLE} />,
keybinds: [controlIcon, (i + 1).toString()],
topBarActive: settings.layoutMode === layout,
topBarActive:
!explorer.isLoadingPreferences && settings.layoutMode === layout,
onClick: () => (explorer.settingsStore.layoutMode = layout),
showAtResolution: 'sm:flex'
} satisfies ToolOption & { layout: ExplorerLayout };
@@ -58,7 +56,13 @@ export const useExplorerTopBarOptions = () => {
},
[] as (ToolOption & { layout: ExplorerLayout })[]
),
[controlIcon, explorer.layouts, explorer.settingsStore, settings.layoutMode]
[
controlIcon,
explorer.isLoadingPreferences,
explorer.layouts,
explorer.settingsStore,
settings.layoutMode
]
);
const controlOptions: ToolOption[] = [
@@ -87,12 +91,6 @@ export const useExplorerTopBarOptions = () => {
}
];
const { parent } = useExplorerContext();
const os = useOperatingSystem();
useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'r'], () => rescan());
useDocumentEventListener('keydown', (e: unknown) => {
if (!(e instanceof KeyboardEvent)) return;
@@ -128,13 +126,6 @@ export const useExplorerTopBarOptions = () => {
topBarActive: explorerStore.tagAssignMode,
individual: true,
showAtResolution: 'xl:flex'
},
parent?.type === 'Location' && {
toolTipLabel: 'Reload',
onClick: rescan,
icon: <ArrowClockwise className={TOP_BAR_ICON_STYLE} />,
individual: true,
showAtResolution: 'xl:flex'
}
].filter(Boolean) as ToolOption[];
@@ -145,12 +136,16 @@ export const useExplorerTopBarOptions = () => {
};
};
export const DefaultTopBarOptions = () => {
export const DefaultTopBarOptions = (props: { options?: ToolOption[] }) => {
const options = useExplorerTopBarOptions();
return (
<TopBarOptions
options={[options.viewOptions, options.toolOptions, options.controlOptions]}
options={[
options.viewOptions,
[...options.toolOptions, ...(props.options ?? [])],
options.controlOptions
]}
/>
);
};

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio';
import { z } from 'zod';
import type {
@@ -39,6 +40,7 @@ export interface UseExplorerProps<TOrder extends Ordering> {
parent?: ExplorerParent;
loadMore?: () => void;
isFetchingNextPage?: boolean;
isLoadingPreferences?: boolean;
scrollRef?: RefObject<HTMLDivElement>;
/**
* @defaultValue `true`
@@ -98,33 +100,36 @@ export function useExplorerSettings<TOrder extends Ordering>({
location
}: {
settings: ReturnType<typeof createDefaultExplorerSettings<TOrder>>;
onSettingsChanged?: (settings: ExplorerSettings<TOrder>) => any;
onSettingsChanged?: (settings: ExplorerSettings<TOrder>, location: Location) => void;
orderingKeys?: z.ZodUnion<
[z.ZodLiteral<OrderingKeys<TOrder>>, ...z.ZodLiteral<OrderingKeys<TOrder>>[]]
>;
location?: Location | null;
}) {
const [store, setStore] = useState(() => proxy(settings));
const [store] = useState(() => proxy(settings));
useEffect(() => {
Object.assign(store, {
...settings,
...store
});
}, [store, settings]);
useEffect(() => {
setStore(proxy(settings));
}, [location, settings]);
useEffect(
() =>
subscribe(store, () => {
onSettingsChanged?.(snapshot(store) as ExplorerSettings<TOrder>);
}),
[onSettingsChanged, store]
const updateSettings = useDebouncedCallback(
(settings: ExplorerSettings<TOrder>, location: Location) => {
onSettingsChanged?.(settings, location);
},
500
);
useEffect(() => updateSettings.flush(), [location, updateSettings]);
useEffect(() => {
if (updateSettings.isPending()) return;
Object.assign(store, settings);
}, [settings, store, updateSettings]);
useEffect(() => {
if (!onSettingsChanged || !location) return;
const unsubscribe = subscribe(store, () => {
updateSettings(snapshot(store) as ExplorerSettings<TOrder>, location);
});
return () => unsubscribe();
}, [store, updateSettings, location, onSettingsChanged]);
return {
useSettingsSnapshot: () => useSnapshot(store),
settingsStore: store,

View File

@@ -1,12 +1,12 @@
import { Info } from '@phosphor-icons/react';
import { ArrowClockwise, Info } from '@phosphor-icons/react';
import { useCallback, useEffect, useMemo } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { stringify } from 'uuid';
import {
arraysEqual,
ExplorerSettings,
FilePathFilterArgs,
FilePathOrder,
Location,
ObjectKindEnum,
useLibraryContext,
useLibraryMutation,
@@ -18,7 +18,14 @@ import {
import { Loader, Tooltip } from '@sd/ui';
import { LocationIdParamsSchema } from '~/app/route-schemas';
import { Folder, Icon } from '~/components';
import { useIsLocationIndexing, useKeyDeleteFile, useZodRouteParams } from '~/hooks';
import {
useIsLocationIndexing,
useKeyDeleteFile,
useOperatingSystem,
useShortcut,
useZodRouteParams
} from '~/hooks';
import { useQuickRescan } from '~/hooks/useQuickRescan';
import Explorer from '../Explorer';
import { ExplorerContextProvider } from '../Explorer/Context';
@@ -29,16 +36,21 @@ import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explor
import { useExplorerSearchParams } from '../Explorer/util';
import { EmptyNotice } from '../Explorer/View';
import { TopBarPortal } from '../TopBar/Portal';
import { TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
import LocationOptions from './LocationOptions';
export const Component = () => {
const [{ path }] = useExplorerSearchParams();
const { id: locationId } = useZodRouteParams(LocationIdParamsSchema);
const location = useLibraryQuery(['locations.get', locationId]);
const os = useOperatingSystem();
const rspc = useRspcLibraryContext();
const [{ path }] = useExplorerSearchParams();
const { id: locationId } = useZodRouteParams(LocationIdParamsSchema);
const location = useLibraryQuery(['locations.get', locationId]);
const onlineLocations = useOnlineLocations();
const rescan = useQuickRescan();
const locationOnline = useMemo(() => {
const pub_id = location.data?.pub_id;
if (!pub_id) return false;
@@ -70,21 +82,23 @@ export const Component = () => {
return defaults;
}, [location.data, preferences.data?.location]);
const onSettingsChanged = useDebouncedCallback(
async (settings: ExplorerSettings<FilePathOrder>) => {
if (!location.data) return;
const pubId = stringify(location.data.pub_id);
try {
await updatePreferences.mutateAsync({
location: { [pubId]: { explorer: settings } }
});
rspc.queryClient.invalidateQueries(['preferences.get']);
} catch (e) {
alert('An error has occurred while updating your preferences.');
}
},
500
);
const onSettingsChanged = async (
settings: ExplorerSettings<FilePathOrder>,
location: Location
) => {
if (location.id === locationId && preferences.isLoading) return;
const pubId = stringify(location.pub_id);
try {
await updatePreferences.mutateAsync({
location: { [pubId]: { explorer: settings } }
});
rspc.queryClient.invalidateQueries(['preferences.get']);
} catch (e) {
alert('An error has occurred while updating your preferences.');
}
};
const explorerSettings = useExplorerSettings({
settings,
@@ -100,6 +114,7 @@ export const Component = () => {
count,
loadMore,
isFetchingNextPage: query.isFetchingNextPage,
isLoadingPreferences: preferences.isLoading,
settings: explorerSettings,
...(location.data && {
parent: { type: 'Location', location: location.data }
@@ -117,9 +132,11 @@ export const Component = () => {
explorer.resetSelectedItems.call(undefined);
}, [explorer.resetSelectedItems, path]);
useEffect(() => explorer.scrollRef.current?.scrollTo({ top: 0 }), [explorer.scrollRef, path]);
useKeyDeleteFile(explorer.selectedItems, location.data?.id);
useEffect(() => explorer.scrollRef.current?.scrollTo({ top: 0 }), [explorer.scrollRef, path]);
useShortcut('rescan', () => rescan(locationId));
return (
<ExplorerContextProvider explorer={explorer}>
@@ -142,14 +159,26 @@ export const Component = () => {
)}
</div>
}
right={<DefaultTopBarOptions />}
right={
<DefaultTopBarOptions
options={[
{
toolTipLabel: 'Reload',
onClick: () => rescan(locationId),
icon: <ArrowClockwise className={TOP_BAR_ICON_STYLE} />,
individual: true,
showAtResolution: 'xl:flex'
}
]}
/>
}
/>
{isLocationIndexing ? (
<div className="flex h-full w-full items-center justify-center">
<Loader />
</div>
) : (
) : !preferences.isLoading ? (
<Explorer
emptyNotice={
<EmptyNotice
@@ -159,7 +188,7 @@ export const Component = () => {
/>
}
/>
)}
) : null}
</ExplorerContextProvider>
);
};

View File

@@ -25,8 +25,7 @@ export const Component = () => {
order: null
}),
[]
),
onSettingsChanged: () => {}
)
});
const explorer = useExplorer({

View File

@@ -12,27 +12,30 @@ export const useQuickRescan = () => {
// gotta clean up any rescan subscriptions if the exist
useEffect(() => () => quickRescanSubscription.current?.(), []);
const { client } = useRspcLibraryContext();
const { parent } = useExplorerContext();
const explorer = useExplorerContext({ suspense: false });
const [{ path }] = useExplorerSearchParams();
const rescan = () => {
if (parent?.type === 'Location') {
quickRescanSubscription.current?.();
quickRescanSubscription.current = client.addSubscription(
[
'locations.quickRescan',
{
location_id: parent.location.id,
sub_path: path ?? ''
}
],
{ onData() {} }
);
const rescan = (id?: number) => {
const locationId =
id ?? (explorer?.parent?.type === 'Location' ? explorer.parent.location.id : undefined);
toast.success({
title: `Quick rescan started`
});
}
if (locationId === undefined) return;
quickRescanSubscription.current?.();
quickRescanSubscription.current = client.addSubscription(
[
'locations.quickRescan',
{
location_id: locationId,
sub_path: path ?? ''
}
],
{ onData() {} }
);
toast.success({
title: `Quick rescan started`
});
};
return rescan;