{
};
}, [blurHandler, focusHandler]);
- const [value, setValue] = useState(search.search);
+ const [value, setValue] = useState('');
+
+ useEffect(() => {
+ setValue(search.rawSearch);
+ }, [search.rawSearch]);
+
+ const updateDebounce = useDebouncedCallback((value: string) => {
+ search.setSearch(value);
+ }, 300);
function updateValue(value: string) {
setValue(value);
- search.setSearch(value);
+ updateDebounce(value);
}
function clearValue() {
- setValue('');
search.setSearch('');
}
@@ -69,10 +77,10 @@ export default () => {
onBlur={() => {
if (search.rawSearch === '' && !searchStore.interactingWithSearchOptions) {
clearValue();
- search.setOpen(false);
+ search.setSearchBarFocused(false);
}
}}
- onFocus={() => search.setOpen(true)}
+ onFocus={() => search.setSearchBarFocused(true)}
right={
{
diff --git a/interface/app/$libraryId/Search/index.tsx b/interface/app/$libraryId/Search/index.tsx
index 5aa9c6d78..8153c43ef 100644
--- a/interface/app/$libraryId/Search/index.tsx
+++ b/interface/app/$libraryId/Search/index.tsx
@@ -292,14 +292,14 @@ function EscapeButton() {
useKeybind(['Escape'], () => {
search.setSearch('');
- search.setOpen(false);
+ search.setSearchBarFocused(false);
});
return (
{
search.setSearch('');
- search.setOpen(false);
+ search.setSearchBarFocused(false);
}}
className="ml-2 rounded-lg border border-app-line bg-app-box px-2 py-1 text-[10.5px] tracking-widest shadow"
>
diff --git a/interface/app/$libraryId/Search/useSearch.ts b/interface/app/$libraryId/Search/useSearch.ts
index 6e4a633b2..f81207ecb 100644
--- a/interface/app/$libraryId/Search/useSearch.ts
+++ b/interface/app/$libraryId/Search/useSearch.ts
@@ -1,7 +1,6 @@
import { produce } from 'immer';
import { useCallback, useMemo, useState } from 'react';
import { useDebouncedValue } from 'rooks';
-import { useDebouncedCallback } from 'use-debounce';
import { SearchFilterArgs } from '@sd/client';
import { filterRegistry } from './Filters';
@@ -22,8 +21,7 @@ export interface UseSearchProps {
}
export function useSearch(props?: UseSearchProps) {
- const [open, setOpen] = useState(false);
- if (props?.open !== undefined && open !== props.open) setOpen(props.open);
+ const [searchBarFocused, setSearchBarFocused] = useState(false);
const searchState = useSearchStore();
@@ -123,14 +121,16 @@ export function useSearch(props?: UseSearchProps) {
// Filters generated from the search query
// rawSearch should only ever be read by the search input
- const [search, setSearch] = useState(props?.search ?? '');
+ const [rawSearch, setRawSearch] = useState(props?.search ?? '');
const [searchFromProps, setSearchFromProps] = useState(props?.search);
if (searchFromProps !== props?.search) {
setSearchFromProps(props?.search);
- setSearch(props?.search ?? '');
+ setRawSearch(props?.search ?? '');
}
+ const [search] = useDebouncedValue(rawSearch, 300);
+
const searchFilters = useMemo(() => {
const [name, ext] = search.split('.') ?? [];
@@ -166,14 +166,14 @@ export function useSearch(props?: UseSearchProps) {
}, [allFiltersAsOptions]);
return {
- open,
- setOpen,
+ open: props?.open || searchBarFocused,
fixedFilters,
fixedFiltersKeys,
search,
- rawSearch: search,
- setRawSearch: setSearch,
- setSearch: useDebouncedCallback(setSearch, 300),
+ rawSearch,
+ setSearch: setRawSearch,
+ searchBarFocused,
+ setSearchBarFocused,
dynamicFilters,
setDynamicFilters,
updateDynamicFilters,
diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx
index f2a293795..1c9dca81d 100644
--- a/interface/app/$libraryId/location/$id.tsx
+++ b/interface/app/$libraryId/location/$id.tsx
@@ -1,5 +1,6 @@
import { ArrowClockwise, Info } from '@phosphor-icons/react';
-import { useEffect, useMemo } from 'react';
+import { memo, useEffect, useMemo } from 'react';
+import { useSearchParams as useRawSearchParams } from 'react-router-dom';
import { stringify } from 'uuid';
import {
arraysEqual,
@@ -32,7 +33,7 @@ import { ExplorerContextProvider } from '../Explorer/Context';
import { usePathsExplorerQuery } from '../Explorer/queries';
import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
-import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
+import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
import { useExplorerSearchParams } from '../Explorer/util';
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
import SearchOptions, { SearchContextProvider, useSearch } from '../Search';
@@ -43,6 +44,7 @@ import LocationOptions from './LocationOptions';
export const Component = () => {
const { id: locationId } = useZodRouteParams(LocationIdParamsSchema);
+ const [{ path }] = useExplorerSearchParams();
const result = useLibraryQuery(['locations.get', locationId], {
keepPreviousData: true,
suspense: true
@@ -50,12 +52,12 @@ export const Component = () => {
useNodes(result.data?.nodes);
const location = useCache(result.data?.item);
- return ;
+ // 'key' allows search state to be thrown out when entering a folder
+ return ;
};
const LocationExplorer = ({ location }: { location: Location; path?: string }) => {
const [{ path, take }] = useExplorerSearchParams();
- const rspc = useRspcLibraryContext();
const onlineLocations = useOnlineLocations();
@@ -67,71 +69,13 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
return onlineLocations.some((l) => arraysEqual(pub_id, l));
}, [location.pub_id, onlineLocations]);
- const preferences = useLibraryQuery(['preferences.get']);
- const updatePreferences = useLibraryMutation('preferences.update');
-
- const settings = useMemo(() => {
- const defaults = createDefaultExplorerSettings({
- order: { field: 'name', value: 'Asc' }
- });
-
- if (!location) return defaults;
-
- const pubId = stringify(location.pub_id);
-
- const settings = preferences.data?.location?.[pubId]?.explorer;
-
- if (!settings) return defaults;
-
- for (const [key, value] of Object.entries(settings)) {
- if (value !== null) Object.assign(defaults, { [key]: value });
- }
-
- return defaults;
- }, [location, preferences.data?.location]);
-
- const onSettingsChanged = async (
- settings: ExplorerSettings,
- changedLocation: Location
- ) => {
- if (changedLocation.id === location.id && preferences.isLoading) return;
-
- const pubId = stringify(changedLocation.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,
- onSettingsChanged,
- orderingKeys: filePathOrderingKeysSchema,
- location
- });
-
- const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
-
- const fixedFilters = useMemo(
- () => [
- { filePath: { locations: { in: [location.id] } } },
- ...(explorerSettingsSnapshot.layoutMode === 'media'
- ? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
- : [])
- ],
- [location.id, explorerSettingsSnapshot.layoutMode]
- );
-
- const search = useSearch({ fixedFilters });
+ const { explorerSettings, preferences } = useLocationExplorerSettings(location);
const { layoutMode, mediaViewWithDescendants, showHiddenFiles } =
explorerSettings.useSettingsSnapshot();
+ const search = useLocationSearch(explorerSettings, location);
+
const paths = usePathsExplorerQuery({
arg: {
filters: [
@@ -251,3 +195,117 @@ function getLastSectionOfPath(path: string): string | undefined {
const lastSection = sections[sections.length - 1];
return lastSection;
}
+
+function useLocationExplorerSettings(location: Location) {
+ const rspc = useRspcLibraryContext();
+
+ const preferences = useLibraryQuery(['preferences.get']);
+ const updatePreferences = useLibraryMutation('preferences.update');
+
+ const settings = useMemo(() => {
+ const defaults = createDefaultExplorerSettings({
+ order: { field: 'name', value: 'Asc' }
+ });
+
+ if (!location) return defaults;
+
+ const pubId = stringify(location.pub_id);
+
+ const settings = preferences.data?.location?.[pubId]?.explorer;
+
+ if (!settings) return defaults;
+
+ for (const [key, value] of Object.entries(settings)) {
+ if (value !== null) Object.assign(defaults, { [key]: value });
+ }
+
+ return defaults;
+ }, [location, preferences.data?.location]);
+
+ const onSettingsChanged = async (
+ settings: ExplorerSettings,
+ changedLocation: Location
+ ) => {
+ if (changedLocation.id === location.id && preferences.isLoading) return;
+
+ const pubId = stringify(changedLocation.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.');
+ }
+ };
+
+ return {
+ explorerSettings: useExplorerSettings({
+ settings,
+ onSettingsChanged,
+ orderingKeys: filePathOrderingKeysSchema,
+ location
+ }),
+ preferences
+ };
+}
+
+function useLocationSearch(
+ explorerSettings: UseExplorerSettings,
+ location: Location
+) {
+ const [searchParams, setSearchParams] = useRawSearchParams();
+ const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
+
+ const fixedFilters = useMemo(
+ () => [
+ { filePath: { locations: { in: [location.id] } } },
+ ...(explorerSettingsSnapshot.layoutMode === 'media'
+ ? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
+ : [])
+ ],
+ [location.id, explorerSettingsSnapshot.layoutMode]
+ );
+
+ const filtersParam = searchParams.get('filters');
+ const dynamicFilters = useMemo(() => JSON.parse(filtersParam ?? '[]'), [filtersParam]);
+
+ const searchQueryParam = searchParams.get('search');
+
+ const search = useSearch({
+ open: !!searchQueryParam || dynamicFilters.length > 0 || undefined,
+ search: searchParams.get('search') ?? undefined,
+ fixedFilters,
+ dynamicFilters
+ });
+
+ useEffect(() => {
+ setSearchParams(
+ (p) => {
+ if (search.dynamicFilters.length > 0)
+ p.set('filters', JSON.stringify(search.dynamicFilters));
+ else p.delete('filters');
+
+ return p;
+ },
+ { replace: true }
+ );
+ }, [search.dynamicFilters, setSearchParams]);
+
+ const searchQuery = search.search;
+
+ useEffect(() => {
+ setSearchParams(
+ (p) => {
+ if (searchQuery !== '') p.set('search', searchQuery);
+ else p.delete('search');
+
+ return p;
+ },
+ { replace: true }
+ );
+ }, [searchQuery, setSearchParams]);
+
+ return search;
+}
diff --git a/interface/app/$libraryId/tag/$id.tsx b/interface/app/$libraryId/tag/$id.tsx
index 7fc370e4b..0f8f6ce89 100644
--- a/interface/app/$libraryId/tag/$id.tsx
+++ b/interface/app/$libraryId/tag/$id.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { memo, useMemo } from 'react';
import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client';
import { LocationIdParamsSchema } from '~/app/route-schemas';
import { Icon } from '~/components';
@@ -82,6 +82,7 @@ export function Component() {
)}
+