diff --git a/interface/app/$libraryId/Explorer/Context.tsx b/interface/app/$libraryId/Explorer/Context.tsx index 571820144..ab9b87c3f 100644 --- a/interface/app/$libraryId/Explorer/Context.tsx +++ b/interface/app/$libraryId/Explorer/Context.tsx @@ -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 | null>(null); -export const useExplorerContext = () => { +type ExplorerContext = NonNullable>; + +export const useExplorerContext = ( + { 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 = >({ diff --git a/interface/app/$libraryId/Explorer/TopBarOptions.tsx b/interface/app/$libraryId/Explorer/TopBarOptions.tsx index e89bef84b..1ab4dfafc 100644 --- a/interface/app/$libraryId/Explorer/TopBarOptions.tsx +++ b/interface/app/$libraryId/Explorer/TopBarOptions.tsx @@ -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: , 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: , - 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 ( ); }; diff --git a/interface/app/$libraryId/Explorer/useExplorer.ts b/interface/app/$libraryId/Explorer/useExplorer.ts index c440f8826..66b28f2e1 100644 --- a/interface/app/$libraryId/Explorer/useExplorer.ts +++ b/interface/app/$libraryId/Explorer/useExplorer.ts @@ -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 { parent?: ExplorerParent; loadMore?: () => void; isFetchingNextPage?: boolean; + isLoadingPreferences?: boolean; scrollRef?: RefObject; /** * @defaultValue `true` @@ -98,33 +100,36 @@ export function useExplorerSettings({ location }: { settings: ReturnType>; - onSettingsChanged?: (settings: ExplorerSettings) => any; + onSettingsChanged?: (settings: ExplorerSettings, location: Location) => void; orderingKeys?: z.ZodUnion< [z.ZodLiteral>, ...z.ZodLiteral>[]] >; 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); - }), - [onSettingsChanged, store] + const updateSettings = useDebouncedCallback( + (settings: ExplorerSettings, 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, location); + }); + return () => unsubscribe(); + }, [store, updateSettings, location, onSettingsChanged]); + return { useSettingsSnapshot: () => useSnapshot(store), settingsStore: store, diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index 178a77a40..6af0ef5d0 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -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) => { - 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, + 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 ( @@ -142,14 +159,26 @@ export const Component = () => { )} } - right={} + right={ + rescan(locationId), + icon: , + individual: true, + showAtResolution: 'xl:flex' + } + ]} + /> + } /> {isLocationIndexing ? (
- ) : ( + ) : !preferences.isLoading ? ( { /> } /> - )} + ) : null}
); }; diff --git a/interface/app/$libraryId/node/$id.tsx b/interface/app/$libraryId/node/$id.tsx index 3914c96bc..45ed880df 100644 --- a/interface/app/$libraryId/node/$id.tsx +++ b/interface/app/$libraryId/node/$id.tsx @@ -25,8 +25,7 @@ export const Component = () => { order: null }), [] - ), - onSettingsChanged: () => {} + ) }); const explorer = useExplorer({ diff --git a/interface/hooks/useQuickRescan.ts b/interface/hooks/useQuickRescan.ts index 05d0193da..1bbcbe6b8 100644 --- a/interface/hooks/useQuickRescan.ts +++ b/interface/hooks/useQuickRescan.ts @@ -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;