fix list select all hotkey (#2195)

This commit is contained in:
jeffvli
2026-07-05 21:01:24 -07:00
parent 2ad4c58dcd
commit 7b5df97fa0
17 changed files with 189 additions and 87 deletions
@@ -6,7 +6,7 @@ import {
UseSuspenseQueryOptions,
} from '@tanstack/react-query';
import throttle from 'lodash/throttle';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { queryKeys } from '/@/renderer/api/query-keys';
import { useListContext } from '/@/renderer/context/list-context';
@@ -86,7 +86,6 @@ export const useItemListInfiniteLoader = ({
const queryClient = useQueryClient();
const lastFetchedPageRef = useRef<number>(-1);
const currentVisibleRangeRef = useRef<null | { startIndex: number; stopIndex: number }>(null);
const [isRefetching, setIsRefetching] = useState(false);
const refetchPromiseRef = useRef<null | Promise<void>>(null);
const previousDataQueryKeyRef = useRef<string>('');
const isRefetchingRef = useRef<boolean>(false);
@@ -213,7 +212,6 @@ export const useItemListInfiniteLoader = ({
const countQueryKey = listCountQuery.queryKey;
// Set refetching state and create a promise to suspend
setIsRefetching(true);
const refetchPromise = (async () => {
try {
// Reset the loaded pages
@@ -239,7 +237,6 @@ export const useItemListInfiniteLoader = ({
// Fetch the first page after count is refetched
await fetchPage(pageToFetch);
} finally {
setIsRefetching(false);
isRefetchingRef.current = false;
refetchPromiseRef.current = null;
}
@@ -248,7 +245,6 @@ export const useItemListInfiniteLoader = ({
refetchPromiseRef.current = refetchPromise;
refetchPromise.catch(() => {
setIsRefetching(false);
isRefetchingRef.current = false;
refetchPromiseRef.current = null;
});
@@ -266,7 +262,7 @@ export const useItemListInfiniteLoader = ({
});
// Suspend if refetching
if (isRefetching && refetchPromiseRef.current) {
if (isRefetchingRef.current && refetchPromiseRef.current) {
throw refetchPromiseRef.current;
}
@@ -494,10 +490,20 @@ export const useItemListInfiniteLoader = ({
.map(([, v]) => v);
}, [data]);
const getLoadedItems = useCallback(() => {
const cacheData = queryClient.getQueryData<InfiniteLoaderCacheData>(dataQueryKey);
const map = cacheData?.dataMap;
if (!map || map.size === 0) return [];
return Array.from(map.entries())
.sort(([a], [b]) => a - b)
.map(([, v]) => v);
}, [dataQueryKey, queryClient]);
return {
dataVersion: (data as any).version ?? 0,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
@@ -1,3 +1,4 @@
import { type RefObject, useRef } from 'react';
import { useNavigate } from 'react-router';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
@@ -13,12 +14,14 @@ import { Play } from '/@/shared/types/types';
export const useListHotkeys = ({
controls,
focusContainerRef,
focused,
internalState,
itemType,
onShowPlayingSong,
}: {
controls: ItemControls;
focusContainerRef?: RefObject<HTMLElement | null>;
focused: boolean;
internalState: ItemListStateActions;
itemType: LibraryItem;
@@ -27,6 +30,19 @@ export const useListHotkeys = ({
const { bindings } = useHotkeySettings();
const playButtonBehavior = usePlayButtonBehavior();
const navigate = useNavigate();
const focusedRef = useRef(focused);
focusedRef.current = focused;
const internalStateRef = useRef(internalState);
internalStateRef.current = internalState;
const isListFocused = () => {
const container = focusContainerRef?.current;
if (container) {
return container.contains(document.activeElement);
}
return focusedRef.current;
};
// Helper to check if item has required properties
const hasRequiredStateItemProperties = (
@@ -48,19 +64,20 @@ export const useListHotkeys = ({
[
'mod+a',
() => {
if (focused) {
if (internalState.isAllSelected()) {
internalState.deselectAll();
} else {
internalState.selectAll();
}
if (!isListFocused()) return;
const state = internalStateRef.current;
if (state.isAllSelected()) {
state.deselectAll();
} else {
state.selectAll();
}
},
],
[
bindings.listPlayDefault.hotkey,
() => {
if (!focused) return;
if (!isListFocused()) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
@@ -73,7 +90,7 @@ export const useListHotkeys = ({
[
bindings.listPlayNow.hotkey,
() => {
if (!focused) return;
if (!isListFocused()) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
@@ -85,7 +102,7 @@ export const useListHotkeys = ({
[
bindings.listPlayNext.hotkey,
() => {
if (!focused) return;
if (!isListFocused()) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
@@ -97,7 +114,7 @@ export const useListHotkeys = ({
[
bindings.listPlayLast.hotkey,
() => {
if (!focused) return;
if (!isListFocused()) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
@@ -109,7 +126,7 @@ export const useListHotkeys = ({
[
bindings.listNavigateToPage.hotkey,
() => {
if (!focused) return;
if (!isListFocused()) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
@@ -1463,6 +1463,7 @@ export const ItemDetailList = ({
useListHotkeys({
controls,
focusContainerRef: containerRef,
focused,
internalState,
itemType: LibraryItem.SONG,
@@ -343,6 +343,7 @@ export interface ItemGridListProps {
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
getItem?: (index: number) => ItemCardProps['data'];
getItemIndex?: (rowId: string) => number | undefined;
getLoadedItems?: () => unknown[];
getRowId?: ((item: unknown) => string) | string;
initialTop?: {
to: number;
@@ -372,6 +373,7 @@ const BaseItemGridList = ({
gap = 'sm',
getItem,
getItemIndex,
getLoadedItems,
getRowId,
initialTop,
itemCount,
@@ -401,9 +403,18 @@ const BaseItemGridList = ({
[data, getItem],
);
const dataRef = useRef(data);
const getLoadedItemsRef = useRef(getLoadedItems);
dataRef.current = data;
getLoadedItemsRef.current = getLoadedItems;
const getDataFn = useCallback(() => {
return data;
}, [data]);
const loadedItems = getLoadedItemsRef.current?.();
if (loadedItems?.length) {
return loadedItems;
}
return dataRef.current ?? [];
}, []);
const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]);
@@ -782,6 +793,7 @@ const BaseItemGridList = ({
useListHotkeys({
controls,
focusContainerRef: rootRef,
focused,
internalState,
itemType,
@@ -827,6 +827,7 @@ interface ItemTableListProps {
enableVerticalBorders?: boolean;
getItem?: (index: number) => undefined | unknown;
getItemIndex?: (rowId: string) => number | undefined;
getLoadedItems?: () => unknown[];
getRowId?: ((item: unknown) => string) | string;
groups?: TableGroupHeader[];
headerHeight?: number;
@@ -1264,6 +1265,7 @@ const BaseItemTableList = ({
enableVerticalBorders = false,
getItem,
getItemIndex,
getLoadedItems,
getRowId,
groups,
headerHeight = 40,
@@ -1498,9 +1500,18 @@ const BaseItemTableList = ({
[enableHeader, headerHeight, rowHeight, pinnedRowCount, size],
);
const dataRef = useRef(data);
const getLoadedItemsRef = useRef(getLoadedItems);
dataRef.current = data;
getLoadedItemsRef.current = getLoadedItems;
const getDataFn = useCallback(() => {
return data;
}, [data]);
const loadedItems = getLoadedItemsRef.current?.();
if (loadedItems?.length) {
return loadedItems;
}
return dataRef.current ?? [];
}, []);
const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]);
@@ -1646,6 +1657,7 @@ const BaseItemTableList = ({
useListHotkeys({
controls,
focusContainerRef: containerRef,
focused,
internalState,
itemType,
@@ -40,16 +40,23 @@ export const AlbumListInfiniteGrid = ({
const { pageKey } = useListContext();
const { dataVersion, getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: pageKey || ItemListKey.ALBUM,
itemsPerPage,
itemType: LibraryItem.ALBUM,
listCountQuery,
listQueryFn,
query,
serverId,
});
const {
dataVersion,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
} = useItemListInfiniteLoader({
eventKey: pageKey || ItemListKey.ALBUM,
itemsPerPage,
itemType: LibraryItem.ALBUM,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
@@ -67,6 +74,7 @@ export const AlbumListInfiniteGrid = ({
gap={gap}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -46,7 +46,7 @@ export const AlbumListInfiniteTable = ({
const listQueryFn = api.controller.getAlbumList;
const { pageKey } = useListContext();
const { getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
const { getItem, getItemIndex, getLoadedItems, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: pageKey || ItemListKey.ALBUM,
itemsPerPage,
@@ -83,6 +83,7 @@ export const AlbumListInfiniteTable = ({
enableVerticalBorders={enableVerticalBorders}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -37,16 +37,23 @@ export const AlbumArtistListInfiniteGrid = ({
const listQueryFn = api.controller.getAlbumArtistList;
const { dataVersion, getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.ALBUM_ARTIST,
itemsPerPage,
itemType: LibraryItem.ALBUM_ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const {
dataVersion,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
} = useItemListInfiniteLoader({
eventKey: ItemListKey.ALBUM_ARTIST,
itemsPerPage,
itemType: LibraryItem.ALBUM_ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
@@ -63,6 +70,7 @@ export const AlbumArtistListInfiniteGrid = ({
gap={gap}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -44,7 +44,7 @@ export const AlbumArtistListInfiniteTable = ({
const listQueryFn = api.controller.getAlbumArtistList;
const { getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
const { getItem, getItemIndex, getLoadedItems, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.ALBUM_ARTIST,
itemsPerPage,
@@ -82,6 +82,7 @@ export const AlbumArtistListInfiniteTable = ({
enableVerticalBorders={enableVerticalBorders}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -37,16 +37,23 @@ export const ArtistListInfiniteGrid = ({
const listQueryFn = api.controller.getArtistList;
const { dataVersion, getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.ARTIST,
itemsPerPage,
itemType: LibraryItem.ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const {
dataVersion,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
} = useItemListInfiniteLoader({
eventKey: ItemListKey.ARTIST,
itemsPerPage,
itemType: LibraryItem.ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
@@ -63,6 +70,7 @@ export const ArtistListInfiniteGrid = ({
gap={gap}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -44,7 +44,7 @@ export const ArtistListInfiniteTable = ({
const listQueryFn = api.controller.getArtistList;
const { getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
const { getItem, getItemIndex, getLoadedItems, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.ARTIST,
itemsPerPage,
@@ -82,6 +82,7 @@ export const ArtistListInfiniteTable = ({
enableVerticalBorders={enableVerticalBorders}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -37,16 +37,23 @@ export const GenreListInfiniteGrid = ({
const listQueryFn = api.controller.getGenreList;
const { dataVersion, getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.GENRE,
itemsPerPage,
itemType: LibraryItem.GENRE,
listCountQuery,
listQueryFn,
query,
serverId,
});
const {
dataVersion,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
} = useItemListInfiniteLoader({
eventKey: ItemListKey.GENRE,
itemsPerPage,
itemType: LibraryItem.GENRE,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
@@ -63,6 +70,7 @@ export const GenreListInfiniteGrid = ({
gap={gap}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -44,7 +44,7 @@ export const GenreListInfiniteTable = ({
const listQueryFn = api.controller.getGenreList;
const { getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
const { getItem, getItemIndex, getLoadedItems, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.GENRE,
itemsPerPage,
@@ -82,6 +82,7 @@ export const GenreListInfiniteTable = ({
enableVerticalBorders={enableVerticalBorders}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -37,16 +37,23 @@ export const PlaylistListInfiniteGrid = ({
const listQueryFn = api.controller.getPlaylistList;
const { dataVersion, getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.PLAYLIST,
itemsPerPage,
itemType: LibraryItem.PLAYLIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const {
dataVersion,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
} = useItemListInfiniteLoader({
eventKey: ItemListKey.PLAYLIST,
itemsPerPage,
itemType: LibraryItem.PLAYLIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
@@ -63,6 +70,7 @@ export const PlaylistListInfiniteGrid = ({
gap={gap}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -44,7 +44,7 @@ export const PlaylistListInfiniteTable = ({
const listQueryFn = api.controller.getPlaylistList;
const { getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
const { getItem, getItemIndex, getLoadedItems, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: ItemListKey.PLAYLIST,
itemsPerPage,
@@ -82,6 +82,7 @@ export const PlaylistListInfiniteTable = ({
enableVerticalBorders={enableVerticalBorders}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -34,16 +34,23 @@ export const SongListInfiniteGrid = ({
const listQueryFn = api.controller.getSongList;
const { pageKey } = useListContext();
const { dataVersion, getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: pageKey || ItemListKey.SONG,
itemsPerPage,
itemType: LibraryItem.SONG,
listCountQuery,
listQueryFn,
query,
serverId,
});
const {
dataVersion,
getItem,
getItemIndex,
getLoadedItems,
itemCount,
loadedItems,
onRangeChanged,
} = useItemListInfiniteLoader({
eventKey: pageKey || ItemListKey.SONG,
itemsPerPage,
itemType: LibraryItem.SONG,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
@@ -60,6 +67,7 @@ export const SongListInfiniteGrid = ({
gap={gap}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
@@ -42,7 +42,7 @@ export const SongListInfiniteTable = ({
const listQueryFn = api.controller.getSongList;
const { pageKey } = useListContext();
const { getItem, getItemIndex, itemCount, loadedItems, onRangeChanged } =
const { getItem, getItemIndex, getLoadedItems, itemCount, loadedItems, onRangeChanged } =
useItemListInfiniteLoader({
eventKey: pageKey || ItemListKey.SONG,
itemsPerPage,
@@ -83,6 +83,7 @@ export const SongListInfiniteTable = ({
enableVerticalBorders={enableVerticalBorders}
getItem={getItem}
getItemIndex={getItemIndex}
getLoadedItems={getLoadedItems}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',