diff --git a/package.json b/package.json index eef929d3b..ed83e1017 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "mpris-service": "^2.1.2", "nanoid": "^3.3.11", "node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f", - "nuqs": "^2.8.9", "overlayscrollbars": "^2.14.0", "overlayscrollbars-react": "^0.5.6", "qs": "^6.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51ca38fff..8b3eebabe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,9 +156,6 @@ importers: node-mpv: specifier: github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f version: https://codeload.github.com/jeffvli/Node-MPV/tar.gz/32b4d64395289ad710c41d481d2707a7acfc228f - nuqs: - specifier: ^2.8.9 - version: 2.8.9(react-router-dom@7.9.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) overlayscrollbars: specifier: ^2.14.0 version: 2.14.0 @@ -2018,9 +2015,6 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} @@ -4283,27 +4277,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nuqs@2.8.9: - resolution: {integrity: sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==} - peerDependencies: - '@remix-run/react': '>=2' - '@tanstack/react-router': ^1 - next: '>=14.2.0' - react: '>=18.2.0 || ^19.0.0-0' - react-router: 7.14.0 - react-router-dom: ^5 || ^6 || ^7 - peerDependenciesMeta: - '@remix-run/react': - optional: true - '@tanstack/react-router': - optional: true - next: - optional: true - react-router: - optional: true - react-router-dom: - optional: true - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4710,13 +4683,6 @@ packages: '@types/react': optional: true - react-router-dom@7.9.4: - resolution: {integrity: sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - react-router@7.14.0: resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==} engines: {node: '>=20.0.0'} @@ -7645,8 +7611,6 @@ snapshots: '@sindresorhus/is@4.6.0': {} - '@standard-schema/spec@1.0.0': {} - '@surma/rollup-plugin-off-main-thread@2.2.3': dependencies: ejs: 3.1.10 @@ -10304,14 +10268,6 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(react-router-dom@7.9.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): - dependencies: - '@standard-schema/spec': 1.0.0 - react: 19.2.4 - optionalDependencies: - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-router-dom: 7.9.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -10695,13 +10651,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-router-dom@7.9.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - optional: true - react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: cookie: 1.1.1 diff --git a/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts b/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts index d55c2219a..9bbfc1ce3 100644 --- a/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts @@ -349,9 +349,12 @@ export const useItemListInfiniteLoader = ({ mutationKey: getListRefreshMutationKey(eventKey), }); + const refreshMutationRef = useRef(refreshMutation); + refreshMutationRef.current = refreshMutation; + const refresh = useCallback( - async (force?: boolean) => refreshMutation.mutateAsync(force), - [refreshMutation], + async (force?: boolean) => refreshMutationRef.current.mutateAsync(force), + [], ); const updateItems = useCallback( @@ -383,7 +386,7 @@ export const useItemListInfiniteLoader = ({ return; } - refreshMutation.mutate(true); + refreshMutationRef.current.mutate(true); }; eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh); @@ -391,7 +394,7 @@ export const useItemListInfiniteLoader = ({ return () => { eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh); }; - }, [eventKey, refreshMutation]); + }, [eventKey]); useEffect(() => { const handleFavorite = (payload: UserFavoriteEventPayload) => { diff --git a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts index 73e1d2aa2..f3e0f2100 100644 --- a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts @@ -5,7 +5,7 @@ import { useSuspenseQuery, UseSuspenseQueryOptions, } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { queryKeys } from '/@/renderer/api/query-keys'; import { useListContext } from '/@/renderer/context/list-context'; @@ -115,6 +115,9 @@ export const useItemListPaginatedLoader = ({ mutationKey: getListRefreshMutationKey(eventKey ?? 'paginated'), }); + const refreshMutationRef = useRef(refreshMutation); + refreshMutationRef.current = refreshMutation; + const updateItems = useCallback( (indexes: number[], value: object) => { return queryClient.setQueryData( @@ -153,7 +156,7 @@ export const useItemListPaginatedLoader = ({ return; } - refreshMutation.mutate(true); + refreshMutationRef.current.mutate(true); }; const handleFavorite = (payload: UserFavoriteEventPayload) => { @@ -220,7 +223,7 @@ export const useItemListPaginatedLoader = ({ eventEmitter.off('USER_FAVORITE', handleFavorite); eventEmitter.off('USER_RATING', handleRating); }; - }, [data, eventKey, itemType, refreshMutation, serverId, updateItems]); + }, [data, eventKey, itemType, serverId, updateItems]); return { data: data?.items || [], pageCount, totalItemCount }; }; diff --git a/src/renderer/features/albums/hooks/use-album-list-filters.ts b/src/renderer/features/albums/hooks/use-album-list-filters.ts index da07fd861..223537419 100644 --- a/src/renderer/features/albums/hooks/use-album-list-filters.ts +++ b/src/renderer/features/albums/hooks/use-album-list-filters.ts @@ -13,6 +13,7 @@ import { setMultipleSearchParams, setSearchParam, } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -74,8 +75,10 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setGenreId = useCallback( (value: null | string[]) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.GENRE_ID, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.GENRE_ID, value), { + replace: true, + }); }); }, [setSearchParams], @@ -83,8 +86,13 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setAlbumArtist = useCallback( (value: null | string[]) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.ARTIST_IDS, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.ARTIST_IDS, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -92,8 +100,10 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setMinYear = useCallback( (value: null | number) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MIN_YEAR, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MIN_YEAR, value), { + replace: true, + }); }); }, [setSearchParams], @@ -101,8 +111,10 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setMaxYear = useCallback( (value: null | number) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MAX_YEAR, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MAX_YEAR, value), { + replace: true, + }); }); }, [setSearchParams], @@ -110,8 +122,10 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setFavorite = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.FAVORITE, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.FAVORITE, value), { + replace: true, + }); }); }, [setSearchParams], @@ -119,8 +133,13 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setCompilation = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.COMPILATION, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.COMPILATION, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -128,8 +147,13 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setHasRating = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.HAS_RATING, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.HAS_RATING, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -137,65 +161,71 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => { const setRecentlyPlayed = useCallback( (value: boolean | null) => { - setSearchParams( - (prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.RECENTLY_PLAYED, value), - { - replace: true, - }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.RECENTLY_PLAYED, value), + { + replace: true, + }, + ); + }); }, [setSearchParams], ); const setCustom = useCallback( (value: null | Record) => { - setSearchParams( - (prev) => { - const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM); - const newCustom = { - ...(previousValue ? JSON.parse(previousValue) : {}), - ...value, - }; + const newCustom = { + ...(previousValue ? JSON.parse(previousValue) : {}), + ...value, + }; - const filteredNewCustom = Object.fromEntries( - Object.entries(newCustom).filter( - ([, value]) => value !== null && value !== undefined, - ), - ); + const filteredNewCustom = Object.fromEntries( + Object.entries(newCustom).filter( + ([, value]) => value !== null && value !== undefined, + ), + ); - prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom)); - return prev; - }, - { - replace: true, - }, - ); + prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom)); + return prev; + }, + { + replace: true, + }, + ); + }); }, [setSearchParams], ); const clear = useCallback(() => { - setSearchParams( - (prev) => - setMultipleSearchParams( - prev, - { - [FILTER_KEYS.ALBUM._CUSTOM]: null, - [FILTER_KEYS.ALBUM.ARTIST_IDS]: null, - [FILTER_KEYS.ALBUM.COMPILATION]: null, - [FILTER_KEYS.ALBUM.FAVORITE]: null, - [FILTER_KEYS.ALBUM.GENRE_ID]: null, - [FILTER_KEYS.ALBUM.HAS_RATING]: null, - [FILTER_KEYS.ALBUM.MAX_YEAR]: null, - [FILTER_KEYS.ALBUM.MIN_YEAR]: null, - [FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: null, - [FILTER_KEYS.SHARED.SEARCH_TERM]: null, - }, - new Set([FILTER_KEYS.ALBUM._CUSTOM]), - ), - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => + setMultipleSearchParams( + prev, + { + [FILTER_KEYS.ALBUM._CUSTOM]: null, + [FILTER_KEYS.ALBUM.ARTIST_IDS]: null, + [FILTER_KEYS.ALBUM.COMPILATION]: null, + [FILTER_KEYS.ALBUM.FAVORITE]: null, + [FILTER_KEYS.ALBUM.GENRE_ID]: null, + [FILTER_KEYS.ALBUM.HAS_RATING]: null, + [FILTER_KEYS.ALBUM.MAX_YEAR]: null, + [FILTER_KEYS.ALBUM.MIN_YEAR]: null, + [FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: null, + [FILTER_KEYS.SHARED.SEARCH_TERM]: null, + }, + new Set([FILTER_KEYS.ALBUM._CUSTOM]), + ), + { replace: true }, + ); + }); }, [setSearchParams]); const query = useMemo( diff --git a/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts b/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts index d0378c2c2..ba904f104 100644 --- a/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts +++ b/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts @@ -6,6 +6,7 @@ import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-f import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { setMultipleSearchParams } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { AlbumArtistListSort } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -19,13 +20,15 @@ export const useAlbumArtistListFilters = () => { const [, setSearchParams] = useSearchParams(); const clear = useCallback(() => { - setSearchParams( - (prev) => - setMultipleSearchParams(prev, { - [FILTER_KEYS.SHARED.SEARCH_TERM]: null, - }), - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => + setMultipleSearchParams(prev, { + [FILTER_KEYS.SHARED.SEARCH_TERM]: null, + }), + { replace: true }, + ); + }); }, [setSearchParams]); const query = { diff --git a/src/renderer/features/folders/hooks/use-folder-list-filters.ts b/src/renderer/features/folders/hooks/use-folder-list-filters.ts index 1a75797bf..e9430b783 100644 --- a/src/renderer/features/folders/hooks/use-folder-list-filters.ts +++ b/src/renderer/features/folders/hooks/use-folder-list-filters.ts @@ -6,6 +6,7 @@ import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-f import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { parseJsonParam, setJsonSearchParam } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -29,13 +30,19 @@ export const useFolderListFilters = () => { }, [searchParams]); const setFolderPath = (path: FolderPathItem[]) => { - setSearchParams( - (prev) => { - const newParams = setJsonSearchParam(prev, FILTER_KEYS.FOLDER.FOLDER_PATH, path); - return newParams; - }, - { replace: false }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const newParams = setJsonSearchParam( + prev, + FILTER_KEYS.FOLDER.FOLDER_PATH, + path, + ); + return newParams; + }, + { replace: false }, + ); + }); }; // Navigate to a folder (adds to path) diff --git a/src/renderer/features/playlists/hooks/use-playlist-list-filters.ts b/src/renderer/features/playlists/hooks/use-playlist-list-filters.ts index f07302dbe..7b624bc3c 100644 --- a/src/renderer/features/playlists/hooks/use-playlist-list-filters.ts +++ b/src/renderer/features/playlists/hooks/use-playlist-list-filters.ts @@ -6,6 +6,7 @@ import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-f import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { parseCustomFiltersParam } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { PlaylistListSort } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -24,28 +25,30 @@ export const usePlaylistListFilters = () => { const setCustom = useCallback( (value: null | Record) => { - setSearchParams( - (prev) => { - const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM); - const newCustom = { - ...(previousValue ? JSON.parse(previousValue) : {}), - ...value, - }; + const newCustom = { + ...(previousValue ? JSON.parse(previousValue) : {}), + ...value, + }; - const filteredNewCustom = Object.fromEntries( - Object.entries(newCustom).filter( - ([, value]) => value !== null && value !== undefined, - ), - ); + const filteredNewCustom = Object.fromEntries( + Object.entries(newCustom).filter( + ([, value]) => value !== null && value !== undefined, + ), + ); - prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom)); - return prev; - }, - { - replace: true, - }, - ); + prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom)); + return prev; + }, + { + replace: true, + }, + ); + }); }, [setSearchParams], ); diff --git a/src/renderer/features/playlists/hooks/use-playlist-song-list-filters.ts b/src/renderer/features/playlists/hooks/use-playlist-song-list-filters.ts index c5b5bfa1e..a68d8ebf2 100644 --- a/src/renderer/features/playlists/hooks/use-playlist-song-list-filters.ts +++ b/src/renderer/features/playlists/hooks/use-playlist-song-list-filters.ts @@ -14,6 +14,7 @@ import { setMultipleSearchParams, setSearchParam, } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -74,18 +75,22 @@ export const usePlaylistSongListFilters = () => { const setAlbumArtistIds = useCallback( (value: null | string[]) => { - setSearchParams( - (prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_ARTIST_IDS, value), - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_ARTIST_IDS, value), + { replace: true }, + ); + }); }, [setSearchParams], ); const setGenreId = useCallback( (value: null | string[]) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), { + replace: true, + }); }); }, [setSearchParams], @@ -93,8 +98,13 @@ export const usePlaylistSongListFilters = () => { const setArtistIds = useCallback( (value: null | string[]) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.ARTIST_IDS, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.SONG.ARTIST_IDS, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -102,8 +112,10 @@ export const usePlaylistSongListFilters = () => { const setMinYear = useCallback( (value: null | number) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MIN_YEAR, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MIN_YEAR, value), { + replace: true, + }); }); }, [setSearchParams], @@ -111,8 +123,10 @@ export const usePlaylistSongListFilters = () => { const setMaxYear = useCallback( (value: null | number) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MAX_YEAR, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MAX_YEAR, value), { + replace: true, + }); }); }, [setSearchParams], @@ -120,8 +134,10 @@ export const usePlaylistSongListFilters = () => { const setFavorite = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.FAVORITE, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.FAVORITE, value), { + replace: true, + }); }); }, [setSearchParams], @@ -129,8 +145,13 @@ export const usePlaylistSongListFilters = () => { const setHasRating = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -153,51 +174,55 @@ export const usePlaylistSongListFilters = () => { const setCustom = useCallback( (value: null | Record) => { - setSearchParams( - (prev) => { - const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM); - const newCustom = { - ...(previousValue ? JSON.parse(previousValue) : {}), - ...value, - }; + const newCustom = { + ...(previousValue ? JSON.parse(previousValue) : {}), + ...value, + }; - const filteredNewCustom = Object.fromEntries( - Object.entries(newCustom).filter( - ([, value]) => value !== null && value !== undefined, - ), - ); + const filteredNewCustom = Object.fromEntries( + Object.entries(newCustom).filter( + ([, value]) => value !== null && value !== undefined, + ), + ); - prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom)); - return prev; - }, - { - replace: true, - }, - ); + prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom)); + return prev; + }, + { + replace: true, + }, + ); + }); }, [setSearchParams], ); const clear = useCallback(() => { - setSearchParams( - (prev) => - setMultipleSearchParams( - prev, - { - [FILTER_KEYS.SONG._CUSTOM]: null, - [FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: null, - [FILTER_KEYS.SONG.ARTIST_IDS]: null, - [FILTER_KEYS.SONG.FAVORITE]: null, - [FILTER_KEYS.SONG.GENRE_ID]: null, - [FILTER_KEYS.SONG.HAS_RATING]: null, - [FILTER_KEYS.SONG.MAX_YEAR]: null, - [FILTER_KEYS.SONG.MIN_YEAR]: null, - }, - new Set([FILTER_KEYS.SONG._CUSTOM]), - ), - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => + setMultipleSearchParams( + prev, + { + [FILTER_KEYS.SONG._CUSTOM]: null, + [FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: null, + [FILTER_KEYS.SONG.ARTIST_IDS]: null, + [FILTER_KEYS.SONG.FAVORITE]: null, + [FILTER_KEYS.SONG.GENRE_ID]: null, + [FILTER_KEYS.SONG.HAS_RATING]: null, + [FILTER_KEYS.SONG.MAX_YEAR]: null, + [FILTER_KEYS.SONG.MIN_YEAR]: null, + }, + new Set([FILTER_KEYS.SONG._CUSTOM]), + ), + { replace: true }, + ); + }); }, [setSearchParams]); const query = useMemo( diff --git a/src/renderer/features/search/components/command-palette.tsx b/src/renderer/features/search/components/command-palette.tsx index 826918c6b..6bd3cc936 100644 --- a/src/renderer/features/search/components/command-palette.tsx +++ b/src/renderer/features/search/components/command-palette.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useDeferredValue, useRef, useState } from 'react'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands'; @@ -49,6 +49,7 @@ function CommandPaletteSearch({ setQuery, }: CommandPaletteSearchProps) { const [debouncedQuery] = useDebouncedValue(query, 400); + const deferredSearchQuery = useDeferredValue(debouncedQuery ?? ''); const searchSectionsExpanded = useAppStore( (state) => state.commandPaletteSearchSectionsExpanded, ); @@ -83,7 +84,7 @@ function CommandPaletteSearch({ ; + sidebarElement: HTMLDivElement | null; } const ListWithSidebarContainerContext = createContext( @@ -36,12 +36,12 @@ function Sidebar({ children }: SidebarProps) { throw new Error('Sidebar must be used within ListWithSidebarContainer'); } - if (!context.sidebarRef?.current) { + if (!context.sidebarElement) { return null; } return ( - + {children} @@ -56,25 +56,25 @@ function SidebarPortal({ children }: SidebarPortalProps) { throw new Error('SidebarPortal must be used within ListWithSidebarContainer'); } - if (!context.sidebarRef?.current) { + if (!context.sidebarElement) { return null; } - return {children}; + return {children}; } export const ListWithSidebarContainer = ({ children, useBreakpoint = false, }: ListWithSidebarContainerProps) => { - const sidebarRef = useRef(null); + const [sidebarElement, setSidebarElement] = useState(null); const { isSidebarOpen = false } = useListContext(); const contextValue = useMemo( () => ({ - sidebarRef, + sidebarElement, }), - [], + [sidebarElement], ); return ( @@ -84,7 +84,7 @@ export const ListWithSidebarContainer = ({ data-sidebar-open={useBreakpoint ? undefined : isSidebarOpen} data-use-breakpoint={useBreakpoint} > -
+
{children}
diff --git a/src/renderer/features/shared/hooks/use-search-term-filter.ts b/src/renderer/features/shared/hooks/use-search-term-filter.ts index 53053e062..d3790b009 100644 --- a/src/renderer/features/shared/hooks/use-search-term-filter.ts +++ b/src/renderer/features/shared/hooks/use-search-term-filter.ts @@ -3,6 +3,7 @@ import { useSearchParams } from 'react-router'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback'; export const useSearchTermFilter = (defaultValue?: string) => { @@ -14,17 +15,19 @@ export const useSearchTermFilter = (defaultValue?: string) => { }, [searchParams, defaultValue]); const handleSetSearchTerm = (value: null | string) => { - setSearchParams( - (prev) => { - const newParams = setSearchParam( - prev, - FILTER_KEYS.SHARED.SEARCH_TERM, - value === '' ? null : value, - ); - return newParams; - }, - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const newParams = setSearchParam( + prev, + FILTER_KEYS.SHARED.SEARCH_TERM, + value === '' ? null : value, + ); + return newParams; + }, + { replace: true }, + ); + }); }; const debouncedSetSearchTerm = useDebouncedCallback(handleSetSearchTerm, 300); diff --git a/src/renderer/features/shared/hooks/use-set-favorite.ts b/src/renderer/features/shared/hooks/use-set-favorite.ts index 1d5281e35..4d8d6e758 100644 --- a/src/renderer/features/shared/hooks/use-set-favorite.ts +++ b/src/renderer/features/shared/hooks/use-set-favorite.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; @@ -8,21 +8,26 @@ export const useSetFavorite = () => { const createFavoriteMutation = useCreateFavorite({}); const deleteFavoriteMutation = useDeleteFavorite({}); + const createFavoriteMutationRef = useRef(createFavoriteMutation); + const deleteFavoriteMutationRef = useRef(deleteFavoriteMutation); + createFavoriteMutationRef.current = createFavoriteMutation; + deleteFavoriteMutationRef.current = deleteFavoriteMutation; + const setFavorite = useCallback( (serverId: string, id: string[], itemType: LibraryItem, isFavorite: boolean) => { if (isFavorite) { - createFavoriteMutation.mutate({ + createFavoriteMutationRef.current.mutate({ apiClientProps: { serverId }, query: { id, type: itemType }, }); } else { - deleteFavoriteMutation.mutate({ + deleteFavoriteMutationRef.current.mutate({ apiClientProps: { serverId }, query: { id, type: itemType }, }); } }, - [createFavoriteMutation, deleteFavoriteMutation], + [], ); return setFavorite; diff --git a/src/renderer/features/shared/hooks/use-set-rating.ts b/src/renderer/features/shared/hooks/use-set-rating.ts index 38fa31b19..59ab3cbba 100644 --- a/src/renderer/features/shared/hooks/use-set-rating.ts +++ b/src/renderer/features/shared/hooks/use-set-rating.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { useSetRatingMutation } from '/@/renderer/features/shared/mutations/set-rating-mutation'; import { LibraryItem } from '/@/shared/types/domain-types'; @@ -6,14 +6,17 @@ import { LibraryItem } from '/@/shared/types/domain-types'; export const useSetRating = () => { const setRatingMutation = useSetRatingMutation({}); + const setRatingMutationRef = useRef(setRatingMutation); + setRatingMutationRef.current = setRatingMutation; + const setRating = useCallback( (serverId: string, id: string[], itemType: LibraryItem, rating: number) => { - setRatingMutation.mutate({ + setRatingMutationRef.current.mutate({ apiClientProps: { serverId }, query: { id, rating, type: itemType }, }); }, - [setRatingMutation], + [], ); return setRating; diff --git a/src/renderer/features/shared/hooks/use-sort-by-filter.ts b/src/renderer/features/shared/hooks/use-sort-by-filter.ts index 87ae11266..8e3c5806c 100644 --- a/src/renderer/features/shared/hooks/use-sort-by-filter.ts +++ b/src/renderer/features/shared/hooks/use-sort-by-filter.ts @@ -5,6 +5,7 @@ import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use- import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { useCurrentServer } from '/@/renderer/store'; import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { ItemListKey } from '/@/shared/types/types'; export const useSortByFilter = (defaultValue: null | string, listKey: ItemListKey) => { @@ -20,14 +21,16 @@ export const useSortByFilter = (defaultValue: null | string, listKey: I }, [searchParams, persisted, defaultValue]); const handleSetSortBy = (sortBy: string) => { - setSearchParams( - (prev) => { - const newParams = setSearchParam(prev, FILTER_KEYS.SHARED.SORT_BY, sortBy); - return newParams; - }, - { replace: true }, - ); - setFilter(FILTER_KEYS.SHARED.SORT_BY, sortBy); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const newParams = setSearchParam(prev, FILTER_KEYS.SHARED.SORT_BY, sortBy); + return newParams; + }, + { replace: true }, + ); + setFilter(FILTER_KEYS.SHARED.SORT_BY, sortBy); + }); }; return { diff --git a/src/renderer/features/shared/hooks/use-sort-order-filter.ts b/src/renderer/features/shared/hooks/use-sort-order-filter.ts index 84f1cfa76..e9f47c14e 100644 --- a/src/renderer/features/shared/hooks/use-sort-order-filter.ts +++ b/src/renderer/features/shared/hooks/use-sort-order-filter.ts @@ -5,6 +5,7 @@ import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use- import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { useCurrentServer } from '/@/renderer/store'; import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -21,14 +22,20 @@ export const useSortOrderFilter = (defaultValue: null | string, listKey: ItemLis }, [searchParams, persisted, defaultValue]); const handleSetSortOrder = (sortOrder: SortOrder) => { - setSearchParams( - (prev) => { - const newParams = setSearchParam(prev, FILTER_KEYS.SHARED.SORT_ORDER, sortOrder); - return newParams; - }, - { replace: true }, - ); - setFilter(FILTER_KEYS.SHARED.SORT_ORDER, sortOrder); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const newParams = setSearchParam( + prev, + FILTER_KEYS.SHARED.SORT_ORDER, + sortOrder, + ); + return newParams; + }, + { replace: true }, + ); + setFilter(FILTER_KEYS.SHARED.SORT_ORDER, sortOrder); + }); }; return { diff --git a/src/renderer/features/songs/hooks/use-song-list-filters.ts b/src/renderer/features/songs/hooks/use-song-list-filters.ts index 1f957f8d9..4a57c5565 100644 --- a/src/renderer/features/songs/hooks/use-song-list-filters.ts +++ b/src/renderer/features/songs/hooks/use-song-list-filters.ts @@ -14,6 +14,7 @@ import { setMultipleSearchParams, setSearchParam, } from '/@/renderer/utils/query-params'; +import { runInUrlTransition } from '/@/renderer/utils/url-transition'; import { SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -65,8 +66,10 @@ export const useSongListFilters = (listKey?: ItemListKey) => { const setGenreId = useCallback( (value: null | string[]) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), { + replace: true, + }); }); }, [setSearchParams], @@ -74,8 +77,13 @@ export const useSongListFilters = (listKey?: ItemListKey) => { const setArtistIds = useCallback( (value: null | string[]) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.ARTIST_IDS, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.SONG.ARTIST_IDS, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -83,8 +91,10 @@ export const useSongListFilters = (listKey?: ItemListKey) => { const setMinYear = useCallback( (value: null | number) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MIN_YEAR, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MIN_YEAR, value), { + replace: true, + }); }); }, [setSearchParams], @@ -92,8 +102,10 @@ export const useSongListFilters = (listKey?: ItemListKey) => { const setMaxYear = useCallback( (value: null | number) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MAX_YEAR, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MAX_YEAR, value), { + replace: true, + }); }); }, [setSearchParams], @@ -101,8 +113,10 @@ export const useSongListFilters = (listKey?: ItemListKey) => { const setFavorite = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.FAVORITE, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.FAVORITE, value), { + replace: true, + }); }); }, [setSearchParams], @@ -110,8 +124,13 @@ export const useSongListFilters = (listKey?: ItemListKey) => { const setHasRating = useCallback( (value: boolean | null) => { - setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), { - replace: true, + runInUrlTransition(() => { + setSearchParams( + (prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), + { + replace: true, + }, + ); }); }, [setSearchParams], @@ -124,46 +143,53 @@ export const useSongListFilters = (listKey?: ItemListKey) => { | null | Record, ) => { - setSearchParams( - (prev) => { - const currentCustom = parseCustomFiltersParam(prev, FILTER_KEYS.SONG._CUSTOM); - let newValue = - typeof value === 'function' ? value(currentCustom ?? null) : value; - // Convert empty objects to null to clear them from URL - if ( - newValue && - typeof newValue === 'object' && - Object.keys(newValue).length === 0 - ) { - newValue = null; - } - return setJsonSearchParam(prev, FILTER_KEYS.SONG._CUSTOM, newValue); - }, - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => { + const currentCustom = parseCustomFiltersParam( + prev, + FILTER_KEYS.SONG._CUSTOM, + ); + let newValue = + typeof value === 'function' ? value(currentCustom ?? null) : value; + // Convert empty objects to null to clear them from URL + if ( + newValue && + typeof newValue === 'object' && + Object.keys(newValue).length === 0 + ) { + newValue = null; + } + return setJsonSearchParam(prev, FILTER_KEYS.SONG._CUSTOM, newValue); + }, + { replace: true }, + ); + }); }, [setSearchParams], ); const clear = useCallback(() => { - setSearchParams( - (prev) => - setMultipleSearchParams( - prev, - { - [FILTER_KEYS.SHARED.SEARCH_TERM]: null, - [FILTER_KEYS.SONG._CUSTOM]: null, - [FILTER_KEYS.SONG.ARTIST_IDS]: null, - [FILTER_KEYS.SONG.FAVORITE]: null, - [FILTER_KEYS.SONG.GENRE_ID]: null, - [FILTER_KEYS.SONG.HAS_RATING]: null, - [FILTER_KEYS.SONG.MAX_YEAR]: null, - [FILTER_KEYS.SONG.MIN_YEAR]: null, - }, - new Set([FILTER_KEYS.SONG._CUSTOM]), - ), - { replace: true }, - ); + runInUrlTransition(() => { + setSearchParams( + (prev) => + setMultipleSearchParams( + prev, + { + [FILTER_KEYS.SHARED.SEARCH_TERM]: null, + [FILTER_KEYS.SONG._CUSTOM]: null, + [FILTER_KEYS.SONG.ARTIST_IDS]: null, + [FILTER_KEYS.SONG.FAVORITE]: null, + [FILTER_KEYS.SONG.GENRE_ID]: null, + [FILTER_KEYS.SONG.HAS_RATING]: null, + [FILTER_KEYS.SONG.MAX_YEAR]: null, + [FILTER_KEYS.SONG.MIN_YEAR]: null, + }, + new Set([FILTER_KEYS.SONG._CUSTOM]), + ), + { replace: true }, + ); + }); }, [setSearchParams]); const query = useMemo( diff --git a/src/renderer/router/app-router.tsx b/src/renderer/router/app-router.tsx index a44016cd2..91046f41b 100644 --- a/src/renderer/router/app-router.tsx +++ b/src/renderer/router/app-router.tsx @@ -200,7 +200,7 @@ const appRouterModals = { export const AppRouter = () => { const router = ( - + diff --git a/src/renderer/utils/url-transition.ts b/src/renderer/utils/url-transition.ts new file mode 100644 index 000000000..d303b19ac --- /dev/null +++ b/src/renderer/utils/url-transition.ts @@ -0,0 +1,5 @@ +import { startTransition } from 'react'; + +export function runInUrlTransition(update: () => void): void { + startTransition(update); +}