upgrade and refactor for react-query v5

This commit is contained in:
jeffvli 2025-11-02 01:16:53 -07:00
parent dd70d30cd3
commit 8115963264
94 changed files with 1650 additions and 1750 deletions

View file

@ -77,9 +77,9 @@
"@mantine/hooks": "^8.2.8", "@mantine/hooks": "^8.2.8",
"@mantine/modals": "^8.2.8", "@mantine/modals": "^8.2.8",
"@mantine/notifications": "^8.2.8", "@mantine/notifications": "^8.2.8",
"@tanstack/react-query": "^4.32.1", "@tanstack/react-query": "^5.89.0",
"@tanstack/react-query-devtools": "^4.32.1", "@tanstack/react-query-devtools": "^5.89.0",
"@tanstack/react-query-persist-client": "^4.32.1", "@tanstack/react-query-persist-client": "^5.89.0",
"@ts-rest/core": "^3.23.0", "@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.3.0", "@xhayper/discord-rpc": "^1.3.0",
"audiomotion-analyzer": "^4.5.0", "audiomotion-analyzer": "^4.5.0",

128
pnpm-lock.yaml generated
View file

@ -60,14 +60,14 @@ importers:
specifier: ^8.2.8 specifier: ^8.2.8
version: 8.2.8(@mantine/core@8.2.8(@mantine/hooks@8.2.8(react@19.1.0))(@types/react@18.3.23)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.2.8(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 8.2.8(@mantine/core@8.2.8(@mantine/hooks@8.2.8(react@19.1.0))(@types/react@18.3.23)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.2.8(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^4.32.1 specifier: ^5.89.0
version: 4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 5.89.0(react@19.1.0)
'@tanstack/react-query-devtools': '@tanstack/react-query-devtools':
specifier: ^4.32.1 specifier: ^5.89.0
version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 5.89.0(@tanstack/react-query@5.89.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-query-persist-client': '@tanstack/react-query-persist-client':
specifier: ^4.32.1 specifier: ^5.89.0
version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) version: 5.89.0(@tanstack/react-query@5.89.0(react@19.1.0))(react@19.1.0)
'@ts-rest/core': '@ts-rest/core':
specifier: ^3.23.0 specifier: ^3.23.0
version: 3.52.1(@types/node@22.15.32)(zod@3.25.23) version: 3.52.1(@types/node@22.15.32)(zod@3.25.23)
@ -1848,39 +1848,31 @@ packages:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@tanstack/match-sorter-utils@8.19.4': '@tanstack/query-core@5.89.0':
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} resolution: {integrity: sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q==}
engines: {node: '>=12'}
'@tanstack/query-core@4.36.1': '@tanstack/query-devtools@5.87.3':
resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} resolution: {integrity: sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg==}
'@tanstack/query-persist-client-core@4.36.1': '@tanstack/query-persist-client-core@5.89.0':
resolution: {integrity: sha512-eocgCeI7D7TRv1IUUBMfVwOI0wdSmMkBIbkKhqEdTrnUHUQEeOaYac8oeZk2cumAWJdycu6P/wB+WqGynTnzXg==} resolution: {integrity: sha512-kxZgQGgD7VqSFTDA/JyajywixHGGhzjMTtkENeVcS6BoTW6CGOkvoZH3L4/ROsaCZ4ibDfrmPzfUCpghID5ENg==}
'@tanstack/react-query-devtools@4.36.1': '@tanstack/react-query-devtools@5.89.0':
resolution: {integrity: sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==} resolution: {integrity: sha512-Syc4UjZeIJCkXCRGyQcWwlnv89JNb98MMg/DAkFCV3rwOcknj98+nG3Nm6xLXM6ne9sK6RZeDJMPLKZUh6NUGA==}
peerDependencies: peerDependencies:
'@tanstack/react-query': ^4.36.1 '@tanstack/react-query': ^5.89.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^18 || ^19
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
'@tanstack/react-query-persist-client@4.36.1': '@tanstack/react-query-persist-client@5.89.0':
resolution: {integrity: sha512-32I5b9aAu4NCiXZ7Te/KEQLfHbYeTNriVPrKYcvEThnZ9tlW01vLcSoxpUIsMYRsembvJUUAkzYBAiZHLOd6pQ==} resolution: {integrity: sha512-c1RaSID8DPzr7HnO2kfah5ON/lEtN/g0gN4nRsxWPi8gjWQRMfOh9av/KJWxxqWnBMPZ+tMV5Lb1OS38GAIRrw==}
peerDependencies: peerDependencies:
'@tanstack/react-query': ^4.36.1 '@tanstack/react-query': ^5.89.0
react: ^18 || ^19
'@tanstack/react-query@4.36.1': '@tanstack/react-query@5.89.0':
resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} resolution: {integrity: sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^18 || ^19
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
'@tootallnate/once@2.0.0': '@tootallnate/once@2.0.0':
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
@ -2525,12 +2517,8 @@ packages:
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
copy-anything@3.0.5: core-js-compat@3.45.1:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==}
engines: {node: '>=12.13'}
core-js-compat@3.46.0:
resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==}
core-util-is@1.0.2: core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@ -3655,10 +3643,6 @@ packages:
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
isarray@1.0.0: isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@ -4629,9 +4613,6 @@ packages:
resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
hasBin: true hasBin: true
remove-accents@0.5.0:
resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==}
remove-trailing-separator@1.1.0: remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
@ -5156,10 +5137,6 @@ packages:
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
engines: {node: '>= 8.0'} engines: {node: '>= 8.0'}
superjson@1.13.3:
resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==}
engines: {node: '>=10'}
supports-color@7.2.0: supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -6474,7 +6451,7 @@ snapshots:
babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.27.1) babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.27.1)
babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.27.1) babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.27.1)
babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.27.1) babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.27.1)
core-js-compat: 3.46.0 core-js-compat: 3.45.1
semver: 6.3.1 semver: 6.3.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -7354,37 +7331,30 @@ snapshots:
dependencies: dependencies:
defer-to-connect: 2.0.1 defer-to-connect: 2.0.1
'@tanstack/match-sorter-utils@8.19.4': '@tanstack/query-core@5.89.0': {}
dependencies:
remove-accents: 0.5.0
'@tanstack/query-core@4.36.1': {} '@tanstack/query-devtools@5.87.3': {}
'@tanstack/query-persist-client-core@4.36.1': '@tanstack/query-persist-client-core@5.89.0':
dependencies: dependencies:
'@tanstack/query-core': 4.36.1 '@tanstack/query-core': 5.89.0
'@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@tanstack/react-query-devtools@5.89.0(@tanstack/react-query@5.89.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@tanstack/match-sorter-utils': 8.19.4 '@tanstack/query-devtools': 5.87.3
'@tanstack/react-query': 4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': 5.89.0(react@19.1.0)
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
superjson: 1.13.3
use-sync-external-store: 1.5.0(react@19.1.0)
'@tanstack/react-query-persist-client@4.36.1(@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': '@tanstack/react-query-persist-client@5.89.0(@tanstack/react-query@5.89.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@tanstack/query-persist-client-core': 4.36.1 '@tanstack/query-persist-client-core': 5.89.0
'@tanstack/react-query': 4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': 5.89.0(react@19.1.0)
react: 19.1.0
'@tanstack/react-query@4.36.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: '@tanstack/react-query@5.89.0(react@19.1.0)':
'@tanstack/query-core': 4.36.1 dependencies:
'@tanstack/query-core': 5.89.0
react: 19.1.0 react: 19.1.0
use-sync-external-store: 1.5.0(react@19.1.0)
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
'@tootallnate/once@2.0.0': {} '@tootallnate/once@2.0.0': {}
@ -7842,7 +7812,7 @@ snapshots:
dependencies: dependencies:
'@babel/core': 7.27.1 '@babel/core': 7.27.1
'@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.27.1) '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.27.1)
core-js-compat: 3.46.0 core-js-compat: 3.45.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -8193,11 +8163,7 @@ snapshots:
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
copy-anything@3.0.5: core-js-compat@3.45.1:
dependencies:
is-what: 4.1.16
core-js-compat@3.46.0:
dependencies: dependencies:
browserslist: 4.27.0 browserslist: 4.27.0
@ -9597,8 +9563,6 @@ snapshots:
call-bound: 1.0.4 call-bound: 1.0.4
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
is-what@4.1.16: {}
isarray@1.0.0: {} isarray@1.0.0: {}
isarray@2.0.5: {} isarray@2.0.5: {}
@ -10524,8 +10488,6 @@ snapshots:
dependencies: dependencies:
jsesc: 3.1.0 jsesc: 3.1.0
remove-accents@0.5.0: {}
remove-trailing-separator@1.1.0: {} remove-trailing-separator@1.1.0: {}
replace-ext@2.0.0: {} replace-ext@2.0.0: {}
@ -11121,10 +11083,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
superjson@1.13.3:
dependencies:
copy-anything: 3.0.5
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0

View file

@ -163,7 +163,8 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
}, },
); );
const results = (await queryClient.fetchQuery(queryKey, async ({ signal }) => { const results = (await queryClient.fetchQuery({
queryFn: async ({ signal }) => {
const res = await queryFn!({ const res = await queryFn!({
apiClientProps: { apiClientProps: {
server, server,
@ -177,6 +178,8 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
}); });
return res; return res;
},
queryKey,
})) as BasePaginatedResponse<any>; })) as BasePaginatedResponse<any>;
if (isClientSideSort && results?.items) { if (isClientSideSort && results?.items) {

View file

@ -0,0 +1,54 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { AlbumDetailQuery, AlbumListQuery } from '/@/shared/types/domain-types';
export const albumQueries = {
detail: (args: QueryHookArgs<AlbumDetailQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getAlbumDetail({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albums.detail(args.serverId, args.query),
...args.options,
});
},
list: (args: QueryHookArgs<AlbumListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getAlbumList({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albums.list(
args.serverId,
args.query,
args.query?.artistIds?.length === 1 ? args.query?.artistIds[0] : undefined,
),
...args.options,
});
},
listCount: (args: QueryHookArgs<AlbumListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getAlbumListCount({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albums.count(
args.serverId,
args.query,
args.query?.artistIds?.length === 1 ? args.query?.artistIds[0] : undefined,
),
...args.options,
});
},
};

View file

@ -2,6 +2,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core'; import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
import { useSetState } from '@mantine/hooks'; import { useSetState } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react'; import { MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useParams } from 'react-router'; import { generatePath, useParams } from 'react-router';
@ -18,8 +19,7 @@ import {
} from '/@/renderer/components/virtual-table'; } from '/@/renderer/components/virtual-table';
import { FullWidthDiscCell } from '/@/renderer/components/virtual-table/cells/full-width-disc-cell'; import { FullWidthDiscCell } from '/@/renderer/components/virtual-table/cells/full-width-disc-cell';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles'; import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
import { import {
useHandleGeneralContextMenu, useHandleGeneralContextMenu,
useHandleTableContextMenu, useHandleTableContextMenu,
@ -71,7 +71,14 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const { t } = useTranslation(); const { t } = useTranslation();
const { albumId } = useParams() as { albumId: string }; const { albumId } = useParams() as { albumId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id }); const detailQuery = useQuery(
albumQueries.detail({ query: { id: albumId }, serverId: server.id }),
);
const { data: detail } = useQuery(
albumQueries.detail({ query: { id: albumId }, serverId: server.id }),
);
const cq = useContainerQuery(); const cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const tableConfig = useTableSettings('albumDetail'); const tableConfig = useTableSettings('albumDetail');
@ -99,7 +106,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
); );
const songsRowData = useMemo(() => { const songsRowData = useMemo(() => {
if (!detailQuery.data?.songs) { if (!detail?.songs) {
return []; return [];
} }
@ -109,7 +116,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const rowData: (QueueSong | { id: string; name: string })[] = []; const rowData: (QueueSong | { id: string; name: string })[] = [];
const discTranslated = t('common.disc', { postProcess: 'upperCase' }); const discTranslated = t('common.disc', { postProcess: 'upperCase' });
for (const song of detailQuery.data.songs) { for (const song of detail.songs) {
if (song.discNumber !== discNumber || song.discSubtitle !== discSubtitle) { if (song.discNumber !== discNumber || song.discSubtitle !== discSubtitle) {
discNumber = song.discNumber; discNumber = song.discNumber;
discSubtitle = song.discSubtitle; discSubtitle = song.discSubtitle;
@ -128,7 +135,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
} }
return rowData; return rowData;
}, [detailQuery.data?.songs, t]); }, [detail?.songs, t]);
const [pagination, setPagination] = useSetState({ const [pagination, setPagination] = useSetState({
artist: 0, artist: 0,
@ -152,29 +159,46 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
[pagination, setPagination], [pagination, setPagination],
); );
const artistQuery = useAlbumList({ const artistQuery = useQuery(
options: { albumQueries.list({
cacheTime: 1000 * 60,
enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined,
keepPreviousData: true,
staleTime: 1000 * 60,
},
query: { query: {
_custom: { _custom: {
jellyfin: { jellyfin: {
ExcludeItemIds: detailQuery?.data?.id, ExcludeItemIds: detail?.id,
}, },
}, },
artistIds: detailQuery?.data?.albumArtists.length artistIds: detail?.albumArtists.length ? [detail?.albumArtists[0].id] : undefined,
? [detailQuery?.data?.albumArtists[0].id]
: undefined,
limit: 15, limit: 15,
sortBy: AlbumListSort.YEAR, sortBy: AlbumListSort.YEAR,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server.id,
}); }),
);
// const artistQuery = useAlbumList({
// options: {
// enabled: detail?.albumArtists[0]?.id !== undefined,
// gcTime: 1000 * 60,
// placeholderData: true,
// },
// query: {
// _custom: {
// jellyfin: {
// ExcludeItemIds: detailQuery?.data?.id,
// },
// },
// artistIds: detailQuery?.data?.albumArtists.length
// ? [detailQuery?.data?.albumArtists[0].id]
// : undefined,
// limit: 15,
// sortBy: AlbumListSort.YEAR,
// sortOrder: SortOrder.DESC,
// startIndex: 0,
// },
// serverId: server?.id,
// });
const relatedAlbumGenresRequest: AlbumListQuery = { const relatedAlbumGenresRequest: AlbumListQuery = {
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined, genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
@ -184,20 +208,21 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
startIndex: 0, startIndex: 0,
}; };
const relatedAlbumGenresQuery = useAlbumList({ const relatedAlbumGenresQuery = useQuery(
albumQueries.list({
options: { options: {
cacheTime: 1000 * 60,
enabled: !!detailQuery?.data?.genres?.[0], enabled: !!detailQuery?.data?.genres?.[0],
gcTime: 1000 * 60,
queryKey: queryKeys.albums.related( queryKey: queryKeys.albums.related(
server?.id || '', server?.id || '',
albumId, albumId,
relatedAlbumGenresRequest, relatedAlbumGenresRequest,
), ),
staleTime: 1000 * 60,
}, },
query: relatedAlbumGenresRequest, query: relatedAlbumGenresRequest,
serverId: server?.id, serverId: server?.id,
}); }),
);
const carousels = [ const carousels = [
{ {
@ -335,8 +360,8 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
: undefined, : undefined,
}} }}
loading={ loading={
createFavoriteMutation.isLoading || createFavoriteMutation.isPending ||
deleteFavoriteMutation.isLoading deleteFavoriteMutation.isPending
} }
onClick={handleFavorite} onClick={handleFavorite}
size="lg" size="lg"

View file

@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react'; import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useParams } from 'react-router'; import { generatePath, useParams } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared'; import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { useSongChange } from '/@/renderer/hooks/use-song-change'; import { useSongChange } from '/@/renderer/hooks/use-song-change';
@ -30,7 +31,9 @@ export const AlbumDetailHeader = forwardRef(
({ background }: AlbumDetailHeaderProps, ref: Ref<HTMLDivElement>) => { ({ background }: AlbumDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
const { albumId } = useParams() as { albumId: string }; const { albumId } = useParams() as { albumId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id }); const detailQuery = useQuery(
albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
);
const cq = useContainerQuery(); const cq = useContainerQuery();
const { t } = useTranslation(); const { t } = useTranslation();
@ -146,7 +149,7 @@ export const AlbumDetailHeader = forwardRef(
onChange={handleUpdateRating} onChange={handleUpdateRating}
readOnly={ readOnly={
detailQuery?.isFetching || detailQuery?.isFetching ||
updateRatingMutation.isLoading updateRatingMutation.isPending
} }
value={detailQuery?.data?.userRating || 0} value={detailQuery?.data?.userRating || 0}
/> />

View file

@ -174,7 +174,8 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
const queryKey = queryKeys.albums.list(server?.id || '', query, id); const queryKey = queryKeys.albums.list(server?.id || '', query, id);
const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => const albums = await queryClient.fetchQuery({
queryFn: async ({ signal }) =>
controller.getAlbumList({ controller.getAlbumList({
apiClientProps: { apiClientProps: {
server, server,
@ -182,7 +183,8 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
}, },
query, query,
}), }),
); queryKey,
});
return albums; return albums;
}, },

View file

@ -1,7 +1,7 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { openModal } from '@mantine/modals'; import { openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -14,7 +14,8 @@ import { useListContext } from '/@/renderer/context/list-context';
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters'; import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; import { OrderToggleButton } from '/@/renderer/features/shared';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { FilterButton } from '/@/renderer/features/shared/components/filter-button'; import { FilterButton } from '/@/renderer/features/shared/components/filter-button';
import { FolderButton } from '/@/renderer/features/shared/components/folder-button'; import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
@ -225,7 +226,9 @@ export const AlbumListHeaderFilters = ({
server, server,
}); });
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const musicFoldersQuery = useQuery(
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
);
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -288,7 +291,7 @@ export const AlbumListHeaderFilters = ({
}; };
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); queryClient.invalidateQueries({ queryKey: queryKeys.albums.list(server?.id || '') });
onFilterChange(filter); onFilterChange(filter);
}, [filter, onFilterChange, queryClient, server?.id]); }, [filter, onFilterChange, queryClient, server?.id]);

View file

@ -1,11 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { AlbumListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store'; import { AlbumListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@ -27,7 +28,7 @@ interface JellyfinAlbumFiltersProps {
disableArtistFilter?: boolean; disableArtistFilter?: boolean;
onFilterChange: (filters: AlbumListFilter) => void; onFilterChange: (filters: AlbumListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId: string;
} }
export const JellyfinAlbumFilters = ({ export const JellyfinAlbumFilters = ({
@ -42,9 +43,10 @@ export const JellyfinAlbumFilters = ({
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
const genreListQuery = useGenreList({ const genreListQuery = useQuery(
genresQueries.list({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -54,7 +56,8 @@ export const JellyfinAlbumFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
@ -64,9 +67,10 @@ export const JellyfinAlbumFilters = ({
})); }));
}, [genreListQuery.data]); }, [genreListQuery.data]);
const tagsQuery = useTagList({ const tagsQuery = useQuery(
sharedQueries.tags({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -74,7 +78,8 @@ export const JellyfinAlbumFilters = ({
type: LibraryItem.ALBUM, type: LibraryItem.ALBUM,
}, },
serverId, serverId,
}); }),
);
const selectedTags = useMemo(() => { const selectedTags = useMemo(() => {
return filter?._custom?.jellyfin?.Tags?.split('|'); return filter?._custom?.jellyfin?.Tags?.split('|');
@ -171,9 +176,10 @@ export const JellyfinAlbumFilters = ({
onFilterChange(updatedFilters); onFilterChange(updatedFilters);
}, 250); }, 250);
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useQuery(
artistsQueries.albumArtistList({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -182,7 +188,8 @@ export const JellyfinAlbumFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const selectableAlbumArtists = useMemo(() => { const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return []; if (!albumArtistListQuery?.data?.items) return [];

View file

@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -6,9 +7,9 @@ import {
MultiSelectWithInvalidData, MultiSelectWithInvalidData,
SelectWithInvalidData, SelectWithInvalidData,
} from '/@/renderer/components/select-with-invalid-data'; } from '/@/renderer/components/select-with-invalid-data';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { import {
AlbumListFilter, AlbumListFilter,
getServerById, getServerById,
@ -39,7 +40,7 @@ interface NavidromeAlbumFiltersProps {
disableArtistFilter?: boolean; disableArtistFilter?: boolean;
onFilterChange: (filters: AlbumListFilter) => void; onFilterChange: (filters: AlbumListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId: string;
} }
export const NavidromeAlbumFilters = ({ export const NavidromeAlbumFilters = ({
@ -54,9 +55,10 @@ export const NavidromeAlbumFilters = ({
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const server = getServerById(serverId); const server = getServerById(serverId);
const genreListQuery = useGenreList({ const genreListQuery = useQuery(
genresQueries.list({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -65,7 +67,8 @@ export const NavidromeAlbumFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
@ -90,16 +93,18 @@ export const NavidromeAlbumFilters = ({
onFilterChange(updatedFilters); onFilterChange(updatedFilters);
}, 250); }, 250);
const tagsQuery = useTagList({ const tagsQuery = useQuery(
sharedQueries.tags({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
type: LibraryItem.ALBUM, type: LibraryItem.ALBUM,
}, },
serverId, serverId,
}); }),
);
const yesNoUndefinedFilters = [ const yesNoUndefinedFilters = [
{ {
@ -199,9 +204,10 @@ export const NavidromeAlbumFilters = ({
onFilterChange(updatedFilters); onFilterChange(updatedFilters);
}, 500); }, 500);
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useQuery(
artistsQueries.albumArtistList({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -211,7 +217,8 @@ export const NavidromeAlbumFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const selectableAlbumArtists = useMemo(() => { const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return []; if (!albumArtistListQuery?.data?.items) return [];

View file

@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo, useState } from 'react'; import { ChangeEvent, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@ -26,7 +27,7 @@ interface SubsonicAlbumFiltersProps {
disableArtistFilter?: boolean; disableArtistFilter?: boolean;
onFilterChange: (filters: AlbumListFilter) => void; onFilterChange: (filters: AlbumListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId: string;
} }
export const SubsonicAlbumFilters = ({ export const SubsonicAlbumFilters = ({
@ -40,9 +41,10 @@ export const SubsonicAlbumFilters = ({
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>(''); const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useQuery(
artistsQueries.albumArtistList({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -51,7 +53,8 @@ export const SubsonicAlbumFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const selectableAlbumArtists = useMemo(() => { const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return []; if (!albumArtistListQuery?.data?.items) return [];
@ -73,9 +76,10 @@ export const SubsonicAlbumFilters = ({
onFilterChange(updatedFilters); onFilterChange(updatedFilters);
}; };
const genreListQuery = useGenreList({ const genreListQuery = useQuery(
genresQueries.list({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@ -84,7 +88,8 @@ export const SubsonicAlbumFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];

View file

@ -1,22 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { AlbumDetailQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useAlbumDetail = (args: QueryHookArgs<AlbumDetailQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return controller.getAlbumDetail({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.albums.detail(server?.id || '', query),
...options,
});
};

View file

@ -1,32 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { AlbumListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albums.count(
serverId || '',
Object.keys(query).length === 0 ? undefined : query,
),
...options,
});
};

View file

@ -1,67 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { AlbumListQuery, AlbumListResponse } from '/@/shared/types/domain-types';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useAlbumList = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return controller.getAlbumList({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albums.list(
serverId || '',
query,
query?.artistIds?.length === 1 ? query?.artistIds[0] : undefined,
),
...options,
});
};
export const useAlbumListInfinite = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useInfiniteQuery({
enabled: !!serverId,
getNextPageParam: (lastPage: AlbumListResponse | undefined, pages) => {
if (!lastPage?.items) return undefined;
if (lastPage?.items?.length >= (query?.limit || 50)) {
return pages?.length;
}
return undefined;
},
queryFn: ({ pageParam = 0, signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumList({
apiClientProps: {
server,
signal,
},
query: {
...query,
limit: query.limit || 50,
startIndex: pageParam * (query.limit || 50),
},
});
},
queryKey: queryKeys.albums.list(server?.id || '', query),
...options,
});
};

View file

@ -1,12 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useRef } from 'react'; import { useRef } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area'; import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content'; import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content';
import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-detail-header'; import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-detail-header';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared'; import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
@ -22,7 +23,9 @@ const AlbumDetailRoute = () => {
const { albumId } = useParams() as { albumId: string }; const { albumId } = useParams() as { albumId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id }); const detailQuery = useQuery(
albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
);
const { background: backgroundColor, colorId } = useFastAverageColor({ const { background: backgroundColor, colorId } = useFastAverageColor({
id: albumId, id: albumId,
src: detailQuery.data?.imageUrl, src: detailQuery.data?.imageUrl,

View file

@ -1,5 +1,6 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
@ -8,10 +9,10 @@ import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content'; import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header'; import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { useAlbumListCount } from '/@/renderer/features/albums/queries/album-list-count-query'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { useGenreList } from '/@/renderer/features/genres';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';
@ -53,10 +54,11 @@ const AlbumListRoute = () => {
key: pageKey, key: pageKey,
}); });
const genreList = useGenreList({ const genreList = useQuery(
genresQueries.list({
options: { options: {
cacheTime: 1000 * 60 * 60,
enabled: !!genreId, enabled: !!genreId,
gcTime: 1000 * 60 * 60,
}, },
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
@ -64,7 +66,8 @@ const AlbumListRoute = () => {
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const genreTitle = useMemo(() => { const genreTitle = useMemo(() => {
if (!genreList.data) return ''; if (!genreList.data) return '';
@ -75,16 +78,18 @@ const AlbumListRoute = () => {
return genre?.name; return genre?.name;
}, [genreId, genreList.data]); }, [genreId, genreList.data]);
const itemCountCheck = useAlbumListCount({ const itemCountCheck = useQuery(
albumQueries.listCount({
options: { options: {
cacheTime: 1000 * 60, gcTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, },
query: { query: {
...albumListFilter, ...albumListFilter,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data; const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;

View file

@ -183,8 +183,8 @@ const DummyAlbumDetailRoute = () => {
fill: detailQuery?.data?.userFavorite ? 'primary' : undefined, fill: detailQuery?.data?.userFavorite ? 'primary' : undefined,
}} }}
loading={ loading={
createFavoriteMutation.isLoading || createFavoriteMutation.isPending ||
deleteFavoriteMutation.isLoading deleteFavoriteMutation.isPending
} }
onClick={handleFavorite} onClick={handleFavorite}
variant="subtle" variant="subtle"

View file

@ -0,0 +1,81 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import {
AlbumArtistDetailQuery,
AlbumArtistListQuery,
ArtistListQuery,
TopSongListQuery,
} from '/@/shared/types/domain-types';
export const artistsQueries = {
albumArtistDetail: (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getAlbumArtistDetail({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albumArtists.detail(args.serverId || '', args.query),
...args.options,
});
},
albumArtistList: (args: QueryHookArgs<AlbumArtistListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getAlbumArtistList({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albumArtists.list(args.serverId || '', args.query),
...args.options,
});
},
albumArtistListCount: (args: QueryHookArgs<AlbumArtistListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getAlbumArtistListCount({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albumArtists.count(
args.serverId || '',
Object.keys(args.query).length === 0 ? undefined : args.query,
),
...args.options,
});
},
artistListCount: (args: QueryHookArgs<ArtistListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getArtistListCount({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albumArtists.count(
args.serverId || '',
Object.keys(args.query).length === 0 ? undefined : args.query,
),
...args.options,
});
},
topSongs: (args: QueryHookArgs<TopSongListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getTopSongs({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.albumArtists.topSongs(args.serverId || '', args.query),
...args.options,
});
},
};

View file

@ -1,4 +1,5 @@
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useParams } from 'react-router'; import { generatePath, useParams } from 'react-router';
@ -8,9 +9,8 @@ import styles from './album-artist-detail-content.module.css';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel'; import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
import { import {
useHandleGeneralContextMenu, useHandleGeneralContextMenu,
useHandleTableContextMenu, useHandleTableContextMenu,
@ -75,10 +75,12 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
return [enabled, order]; return [enabled, order];
}, [artistItems]); }, [artistItems]);
const detailQuery = useAlbumArtistDetail({ const detailQuery = useQuery(
artistsQueries.albumArtistDetail({
query: { id: routeId }, query: { id: routeId },
serverId: server?.id, serverId: server?.id,
}); }),
);
const artistDiscographyLink = `${generatePath( const artistDiscographyLink = `${generatePath(
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
@ -97,7 +99,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
artistName: detailQuery?.data?.name || '', artistName: detailQuery?.data?.name || '',
})}`; })}`;
const recentAlbumsQuery = useAlbumList({ const recentAlbumsQuery = useQuery(
albumQueries.list({
options: { options: {
enabled: enabledItem.recentAlbums, enabled: enabledItem.recentAlbums,
}, },
@ -110,9 +113,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const compilationAlbumsQuery = useAlbumList({ const compilationAlbumsQuery = useQuery(
albumQueries.list({
options: { options: {
enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC, enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC,
}, },
@ -125,9 +130,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const topSongsQuery = useTopSongsList({ const topSongsQuery = useQuery(
artistsQueries.topSongs({
options: { options: {
enabled: !!detailQuery?.data?.name && enabledItem.topSongs, enabled: !!detailQuery?.data?.name && enabledItem.topSongs,
}, },
@ -136,7 +143,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
artistId: routeId, artistId: routeId,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const topSongsColumnDefs: ColDef[] = useMemo( const topSongsColumnDefs: ColDef[] = useMemo(
() => () =>
@ -364,7 +372,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
fill: detailQuery?.data?.userFavorite ? 'primary' : undefined, fill: detailQuery?.data?.userFavorite ? 'primary' : undefined,
}} }}
loading={ loading={
createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading createFavoriteMutation.isPending || deleteFavoriteMutation.isPending
} }
onClick={handleFavorite} onClick={handleFavorite}
size="lg" size="lg"

View file

@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { forwardRef, Fragment, Ref } from 'react'; import { forwardRef, Fragment, Ref } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared'; import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
@ -30,10 +31,12 @@ export const AlbumArtistDetailHeader = forwardRef(
const routeId = (artistId || albumArtistId) as string; const routeId = (artistId || albumArtistId) as string;
const server = useCurrentServer(); const server = useCurrentServer();
const { t } = useTranslation(); const { t } = useTranslation();
const detailQuery = useAlbumArtistDetail({ const detailQuery = useQuery(
artistsQueries.albumArtistDetail({
query: { id: routeId }, query: { id: routeId },
serverId: server?.id, serverId: server?.id,
}); }),
);
const albumCount = detailQuery?.data?.albumCount; const albumCount = detailQuery?.data?.albumCount;
const songCount = detailQuery?.data?.songCount; const songCount = detailQuery?.data?.songCount;
@ -101,7 +104,7 @@ export const AlbumArtistDetailHeader = forwardRef(
<Rating <Rating
onChange={handleUpdateRating} onChange={handleUpdateRating}
readOnly={ readOnly={
detailQuery?.isFetching || updateRatingMutation.isLoading detailQuery?.isFetching || updateRatingMutation.isPending
} }
value={detailQuery?.data?.userRating || 0} value={detailQuery?.data?.userRating || 0}
/> />

View file

@ -83,9 +83,9 @@ export const AlbumArtistListGridView = ({ gridRef, itemCount }: AlbumArtistListG
const queryKey = queryKeys.albumArtists.list(server?.id || '', query); const queryKey = queryKeys.albumArtists.list(server?.id || '', query);
const albumArtistsRes = await queryClient.fetchQuery( const albumArtistsRes = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60 * 1,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: { apiClientProps: {
server, server,
@ -93,8 +93,8 @@ export const AlbumArtistListGridView = ({ gridRef, itemCount }: AlbumArtistListG
}, },
query, query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, queryKey,
); });
return albumArtistsRes; return albumArtistsRes;
}, },

View file

@ -1,7 +1,7 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { IDatasource } from '@ag-grid-community/core'; import { IDatasource } from '@ag-grid-community/core';
import { useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -12,7 +12,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { FolderButton } from '/@/renderer/features/shared/components/folder-button'; import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { MoreButton } from '/@/renderer/features/shared/components/more-button'; import { MoreButton } from '/@/renderer/features/shared/components/more-button';
@ -146,7 +146,9 @@ export const AlbumArtistListHeaderFilters = ({
const cq = useContainerQuery(); const cq = useContainerQuery();
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID; const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const musicFoldersQuery = useQuery(
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
);
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -176,9 +178,9 @@ export const AlbumArtistListHeaderFilters = ({
...filters, ...filters,
}); });
const albums = await queryClient.fetchQuery( const albums = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60 * 1,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: { apiClientProps: {
server, server,
@ -190,8 +192,8 @@ export const AlbumArtistListHeaderFilters = ({
...filters, ...filters,
}, },
}), }),
{ cacheTime: 1000 * 60 * 1 }, queryKey,
); });
return albums; return albums;
}, },
@ -212,9 +214,9 @@ export const AlbumArtistListHeaderFilters = ({
...filters, ...filters,
}); });
const albumArtistsRes = await queryClient.fetchQuery( const albumArtistsRes = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60 * 1,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: { apiClientProps: {
server, server,
@ -226,8 +228,8 @@ export const AlbumArtistListHeaderFilters = ({
...filters, ...filters,
}, },
}), }),
{ cacheTime: 1000 * 60 * 1 }, queryKey,
); });
params.successCallback( params.successCallback(
albumArtistsRes?.items || [], albumArtistsRes?.items || [],
@ -362,7 +364,7 @@ export const AlbumArtistListHeaderFilters = ({
}; };
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
queryClient.invalidateQueries(queryKeys.albumArtists.list(server?.id || '')); queryClient.invalidateQueries({ queryKey: queryKeys.albumArtists.list(server?.id || '') });
handleFilterChange(filter); handleFilterChange(filter);
}, [filter, handleFilterChange, queryClient, server?.id]); }, [filter, handleFilterChange, queryClient, server?.id]);

View file

@ -84,9 +84,9 @@ export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewPro
const queryKey = queryKeys.artists.list(server?.id || '', query); const queryKey = queryKeys.artists.list(server?.id || '', query);
const artistsRes = await queryClient.fetchQuery( const artistsRes = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60 * 1,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getArtistList({ api.controller.getArtistList({
apiClientProps: { apiClientProps: {
server, server,
@ -94,8 +94,8 @@ export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewPro
}, },
query, query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, queryKey,
); });
return artistsRes; return artistsRes;
}, },

View file

@ -1,7 +1,7 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { IDatasource } from '@ag-grid-community/core'; import { IDatasource } from '@ag-grid-community/core';
import { useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback } from 'react'; import { MouseEvent, MutableRefObject, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -12,8 +12,8 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { useRoles } from '/@/renderer/features/artists/queries/roles-query'; import { OrderToggleButton } from '/@/renderer/features/shared';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { MoreButton } from '/@/renderer/features/shared/components/more-button'; import { MoreButton } from '/@/renderer/features/shared/components/more-button';
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button'; import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
@ -142,17 +142,21 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
const { setDisplayType, setFilter, setGrid, setTable, setTablePagination } = const { setDisplayType, setFilter, setGrid, setTable, setTablePagination } =
useListStoreActions(); useListStoreActions();
const cq = useContainerQuery(); const cq = useContainerQuery();
const roles = useRoles({ const roles = useQuery(
sharedQueries.roles({
options: { options: {
cacheTime: 1000 * 60 * 60 * 2, gcTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2, staleTime: 1000 * 60 * 60 * 2,
}, },
query: {}, query: {},
serverId: server?.id, serverId: server?.id,
}); }),
);
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID; const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const musicFoldersQuery = useQuery(
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
);
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -182,9 +186,9 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
...filters, ...filters,
}); });
const albums = await queryClient.fetchQuery( const albums = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60 * 1,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getArtistList({ api.controller.getArtistList({
apiClientProps: { apiClientProps: {
server, server,
@ -196,8 +200,8 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
...filters, ...filters,
}, },
}), }),
{ cacheTime: 1000 * 60 * 1 }, queryKey,
); });
return albums; return albums;
}, },
@ -218,9 +222,9 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
...filters, ...filters,
}); });
const artistsRes = await queryClient.fetchQuery( const artistsRes = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60 * 1,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getArtistList({ api.controller.getArtistList({
apiClientProps: { apiClientProps: {
server, server,
@ -232,8 +236,8 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
...filters, ...filters,
}, },
}), }),
{ cacheTime: 1000 * 60 * 1 }, queryKey,
); });
params.successCallback( params.successCallback(
artistsRes?.items || [], artistsRes?.items || [],
@ -368,7 +372,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
}; };
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
queryClient.invalidateQueries(queryKeys.artists.list(server?.id || '')); queryClient.invalidateQueries({ queryKey: queryKeys.artists.list(server?.id || '') });
handleFilterChange(filter); handleFilterChange(filter);
}, [filter, handleFilterChange, queryClient, server?.id]); }, [filter, handleFilterChange, queryClient, server?.id]);

View file

@ -1,26 +0,0 @@
import type { AlbumArtistDetailQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const useAlbumArtistDetail = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server?.id && !!query.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumArtistDetail({
apiClientProps: { server, signal },
query,
});
},
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
...options,
});
};

View file

@ -1,31 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { AlbumArtistListQuery } from '/@/shared/types/domain-types';
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumArtistListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albumArtists.count(
serverId || '',
Object.keys(query).length === 0 ? undefined : query,
),
...options,
});
};

View file

@ -1,23 +0,0 @@
import type { AlbumArtistListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const useAlbumArtistList = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumArtistList({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.albumArtists.list(server?.id || '', query),
...options,
});
};

View file

@ -1,26 +0,0 @@
import type { AlbumArtistDetailQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const useAlbumArtistInfo = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server?.id && !!query.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumArtistDetail({
apiClientProps: { server, signal },
query,
});
},
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
...options,
});
};

View file

@ -1,31 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { ArtistListQuery } from '/@/shared/types/domain-types';
export const useArtistListCount = (args: QueryHookArgs<ArtistListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getArtistListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albumArtists.count(
serverId || '',
Object.keys(query).length === 0 ? undefined : query,
),
...options,
});
};

View file

@ -1,26 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const useRoles = (args: QueryHookArgs<object>) => {
const { options, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getRoles({
apiClientProps: {
server,
signal,
},
});
},
queryKey: queryKeys.roles.list(serverId || ''),
...options,
});
};

View file

@ -1,23 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { TopSongListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useTopSongsList = (args: QueryHookArgs<TopSongListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getTopSongs({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.albumArtists.topSongs(server?.id || '', query),
...options,
});
};

View file

@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { useRef } from 'react'; import { useRef } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area'; import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content'; import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content';
import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header'; import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared'; import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
@ -27,10 +28,12 @@ const AlbumArtistDetailRoute = () => {
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const detailQuery = useAlbumArtistDetail({ const detailQuery = useQuery(
artistsQueries.albumArtistDetail({
query: { id: routeId }, query: { id: routeId },
serverId: server?.id, serverId: server?.id,
}); }),
);
const { background: backgroundColor, colorId } = useFastAverageColor({ const { background: backgroundColor, colorId } = useFastAverageColor({
id: artistId, id: artistId,

View file

@ -1,13 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistDetailTopSongsListContent } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-content'; import { AlbumArtistDetailTopSongsListContent } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-content';
import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header'; import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store/auth.store'; import { useCurrentServer } from '/@/renderer/store/auth.store';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
@ -22,16 +22,20 @@ const AlbumArtistDetailTopSongsListRoute = () => {
const server = useCurrentServer(); const server = useCurrentServer();
const pageKey = LibraryItem.SONG; const pageKey = LibraryItem.SONG;
const detailQuery = useAlbumArtistDetail({ const detailQuery = useQuery(
artistsQueries.albumArtistDetail({
query: { id: routeId }, query: { id: routeId },
serverId: server?.id, serverId: server?.id,
}); }),
);
const topSongsQuery = useTopSongsList({ const topSongsQuery = useQuery(
artistsQueries.topSongs({
options: { enabled: !!detailQuery?.data?.name }, options: { enabled: !!detailQuery?.data?.name },
query: { artist: detailQuery?.data?.name || '', artistId: routeId }, query: { artist: detailQuery?.data?.name || '', artistId: routeId },
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = topSongsQuery?.data?.items?.length || 0; const itemCount = topSongsQuery?.data?.items?.length || 0;

View file

@ -1,12 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content'; import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header'; import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { useAlbumArtistListCount } from '/@/renderer/features/artists/queries/album-artist-list-count-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store/auth.store'; import { useCurrentServer } from '/@/renderer/store/auth.store';
import { useListFilterByKey } from '/@/renderer/store/list.store'; import { useListFilterByKey } from '/@/renderer/store/list.store';
@ -20,14 +21,15 @@ const AlbumArtistListRoute = () => {
const albumArtistListFilter = useListFilterByKey<AlbumArtistListQuery>({ key: pageKey }); const albumArtistListFilter = useListFilterByKey<AlbumArtistListQuery>({ key: pageKey });
const itemCountCheck = useAlbumArtistListCount({ const itemCountCheck = useQuery(
artistsQueries.albumArtistListCount({
options: { options: {
cacheTime: 1000 * 60, gcTime: 1000 * 60,
staleTime: 1000 * 60,
}, },
query: albumArtistListFilter, query: albumArtistListFilter,
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data; const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;

View file

@ -1,12 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content'; import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content';
import { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header'; import { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header';
import { useArtistListCount } from '/@/renderer/features/artists/queries/artist-list-count-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store/auth.store'; import { useCurrentServer } from '/@/renderer/store/auth.store';
import { useListFilterByKey } from '/@/renderer/store/list.store'; import { useListFilterByKey } from '/@/renderer/store/list.store';
@ -20,14 +21,16 @@ const ArtistListRoute = () => {
const artistListFilter = useListFilterByKey<ArtistListQuery>({ key: pageKey }); const artistListFilter = useListFilterByKey<ArtistListQuery>({ key: pageKey });
const itemCountCheck = useArtistListCount({ const itemCountCheck = useQuery(
artistsQueries.artistListCount({
options: { options: {
cacheTime: 1000 * 60, gcTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, },
query: artistListFilter, query: artistListFilter,
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data; const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;

View file

@ -549,7 +549,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
openModal({ openModal({
children: ( children: (
<ConfirmModal loading={removeFromPlaylistMutation.isLoading} onConfirm={confirm}> <ConfirmModal loading={removeFromPlaylistMutation.isPending} onConfirm={confirm}>
{t('common.areYouSure', { postProcess: 'sentenceCase' })} {t('common.areYouSure', { postProcess: 'sentenceCase' })}
</ConfirmModal> </ConfirmModal>
), ),

View file

@ -0,0 +1,25 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { GenreListQuery } from '/@/shared/types/domain-types';
export const genresQueries = {
list: (args: QueryHookArgs<GenreListQuery>) => {
return queryOptions({
gcTime: 1000 * 5,
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getGenreList({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.genres.list(args.serverId || '', args.query),
...args.options,
});
},
};

View file

@ -1,6 +1,6 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -10,7 +10,8 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; import { OrderToggleButton } from '/@/renderer/features/shared';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { FolderButton } from '/@/renderer/features/shared/components/folder-button'; import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { MoreButton } from '/@/renderer/features/shared/components/more-button'; import { MoreButton } from '/@/renderer/features/shared/components/more-button';
@ -93,7 +94,9 @@ export const GenreListHeaderFilters = ({
server, server,
}); });
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const musicFoldersQuery = useQuery(
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
);
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -121,7 +124,7 @@ export const GenreListHeaderFilters = ({
); );
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
queryClient.invalidateQueries(queryKeys.genres.list(server?.id || '')); queryClient.invalidateQueries({ queryKey: queryKeys.genres.list(server?.id || '') });
onFilterChange(filter); onFilterChange(filter);
}, [filter, onFilterChange, queryClient, server?.id]); }, [filter, onFilterChange, queryClient, server?.id]);

View file

@ -1,24 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { GenreListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useGenreList = (args: QueryHookArgs<GenreListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getGenreList({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.genres.list(server?.id || '', query),
staleTime: 1000 * 5,
...options,
});
};

View file

@ -1,12 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { GenreListContent } from '/@/renderer/features/genres/components/genre-list-content'; import { GenreListContent } from '/@/renderer/features/genres/components/genre-list-content';
import { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header'; import { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
import { useGenreList } from '/@/renderer/features/genres/queries/genre-list-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { useListStoreByKey } from '/@/renderer/store/list.store'; import { useListStoreByKey } from '/@/renderer/store/list.store';
@ -19,14 +20,16 @@ const GenreListRoute = () => {
const pageKey = 'genre'; const pageKey = 'genre';
const { filter } = useListStoreByKey<GenreListQuery>({ key: pageKey }); const { filter } = useListStoreByKey<GenreListQuery>({ key: pageKey });
const itemCountCheck = useGenreList({ const itemCountCheck = useQuery(
genresQueries.list({
query: { query: {
...filter, ...filter,
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -0,0 +1,32 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
export const homeQueries = {
recentlyPlayed: (args: QueryHookArgs<Partial<AlbumListQuery>>) => {
const requestQuery: AlbumListQuery = {
limit: 5,
sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: SortOrder.ASC,
startIndex: 0,
...args.query,
};
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getAlbumList({
apiClientProps: { server, signal },
query: requestQuery,
});
},
queryKey: queryKeys.albums.list(args.serverId || '', requestQuery),
...args.options,
});
},
};

View file

@ -1,37 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
export const useRecentlyPlayed = (args: QueryHookArgs<Partial<AlbumListQuery>>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
const requestQuery: AlbumListQuery = {
limit: 5,
sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: SortOrder.ASC,
startIndex: 0,
...query,
};
return useQuery({
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumList({
apiClientProps: {
server,
signal,
},
query: requestQuery,
});
},
queryKey: queryKeys.albums.list(server?.id || '', requestQuery),
...options,
});
};

View file

@ -1,13 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel'; import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel'; import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area'; import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
import { useAlbumList } from '/@/renderer/features/albums'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { useRecentlyPlayed } from '/@/renderer/features/home/queries/recently-played-query'; import { homeQueries } from '/@/renderer/features/home/api/home-api';
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared'; import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useSongList } from '/@/renderer/features/songs'; import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { import {
HomeItem, HomeItem,
@ -43,10 +44,11 @@ const HomeRoute = () => {
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const { homeFeature, homeItems } = useGeneralSettings(); const { homeFeature, homeItems } = useGeneralSettings();
const feature = useAlbumList({ const feature = useQuery(
albumQueries.list({
options: { options: {
cacheTime: 1000 * 60,
enabled: homeFeature, enabled: homeFeature,
gcTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, },
query: { query: {
@ -56,7 +58,8 @@ const HomeRoute = () => {
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const isJellyfin = server?.type === ServerType.JELLYFIN; const isJellyfin = server?.type === ServerType.JELLYFIN;
@ -74,70 +77,89 @@ const HomeRoute = () => {
); );
}, [homeItems]); }, [homeItems]);
const random = useAlbumList({ const random = useQuery(
albumQueries.list({
options: { options: {
enabled: queriesEnabled[HomeItem.RANDOM],
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
}, },
query: { query: {
...BASE_QUERY_ARGS, ...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC,
startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const recentlyPlayed = useRecentlyPlayed({ const recentlyPlayed = useQuery(
homeQueries.recentlyPlayed({
options: { options: {
enabled: queriesEnabled[HomeItem.RECENTLY_PLAYED] && !isJellyfin,
staleTime: 0, staleTime: 0,
}, },
query: { query: {
...BASE_QUERY_ARGS, ...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RECENTLY_PLAYED, sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: SortOrder.DESC,
startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const recentlyAdded = useAlbumList({ const recentlyAdded = useQuery(
albumQueries.list({
options: { options: {
enabled: queriesEnabled[HomeItem.RECENTLY_ADDED],
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
}, },
query: { query: {
...BASE_QUERY_ARGS, ...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RECENTLY_ADDED, sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC,
startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const mostPlayedAlbums = useAlbumList({ const mostPlayedAlbums = useQuery(
albumQueries.list({
options: { options: {
enabled: !isJellyfin && queriesEnabled[HomeItem.MOST_PLAYED], enabled:
server?.type === ServerType.SUBSONIC || server?.type === ServerType.NAVIDROME,
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
}, },
query: { query: {
...BASE_QUERY_ARGS, ...BASE_QUERY_ARGS,
sortBy: AlbumListSort.PLAY_COUNT, sortBy: AlbumListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC,
startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const mostPlayedSongs = useSongList( const mostPlayedSongs = useQuery(
songsQueries.list(
{ {
options: { options: {
enabled: isJellyfin && queriesEnabled[HomeItem.MOST_PLAYED], enabled: server?.type === ServerType.JELLYFIN,
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
}, },
query: { query: {
...BASE_QUERY_ARGS, ...BASE_QUERY_ARGS,
sortBy: SongListSort.PLAY_COUNT, sortBy: SongListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC,
startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}, },
300, 300,
),
); );
const recentlyReleased = useAlbumList({ const recentlyReleased = useQuery(
albumQueries.list({
options: { options: {
enabled: queriesEnabled[HomeItem.RECENTLY_RELEASED], enabled: queriesEnabled[HomeItem.RECENTLY_RELEASED],
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
@ -147,7 +169,8 @@ const HomeRoute = () => {
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const isLoading = const isLoading =
(random.isLoading && queriesEnabled[HomeItem.RANDOM]) || (random.isLoading && queriesEnabled[HomeItem.RANDOM]) ||

View file

@ -0,0 +1,200 @@
import { queryOptions } from '@tanstack/react-query';
import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useSettingsStore } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils';
import {
FullLyricsMetadata,
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricGetQuery,
LyricSearchQuery,
LyricsQuery,
QueueSong,
ServerType,
StructuredLyric,
SynchronizedLyricsArray,
} from '/@/shared/types/domain-types';
import { LyricSource } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
const lyricsIpc = isElectron() ? window.api.lyrics : null;
// Match LRC lyrics format by https://github.com/ustbhuangyi/lyric-parser
// [mm:ss.SSS] text
const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]([^\n]+)(\n|$)/g;
// Match karaoke lyrics format returned by NetEase
// [SSS,???] text
const alternateTimeExp = /\[(\d*),(\d*)]([^\n]+)(\n|$)/g;
const formatLyrics = (lyrics: string) => {
const synchronizedLines = lyrics.matchAll(timeExp);
const formattedLyrics: SynchronizedLyricsArray = [];
for (const line of synchronizedLines) {
const [, minute, sec, ms, text] = line;
const minutes = parseInt(minute, 10);
const seconds = parseInt(sec, 10);
const milis = ms?.length === 3 ? parseInt(ms, 10) : parseInt(ms, 10) * 10;
const timeInMilis = (minutes * 60 + seconds) * 1000 + milis;
formattedLyrics.push([timeInMilis, text]);
}
if (formattedLyrics.length > 0) return formattedLyrics;
const alternateSynchronizedLines = lyrics.matchAll(alternateTimeExp);
for (const line of alternateSynchronizedLines) {
const [, timeInMilis, , text] = line;
const cleanText = text
.replaceAll(/\(\d+,\d+\)/g, '')
.replaceAll(/\s,/g, ',')
.replaceAll(/\s\./g, '.');
formattedLyrics.push([Number(timeInMilis), cleanText]);
}
if (formattedLyrics.length > 0) return formattedLyrics;
// If no synchronized lyrics were found, return the original lyrics
return lyrics;
};
export const lyricsQueries = {
search: (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
return queryOptions({
gcTime: 1000 * 60 * 1,
queryFn: () => {
if (lyricsIpc) {
return lyricsIpc.searchRemoteLyrics(args.query);
}
return {} as Record<LyricSource, InternetProviderLyricSearchResponse[]>;
},
queryKey: queryKeys.songs.lyricsSearch(args.query),
staleTime: 1000 * 60 * 1,
...args.options,
});
},
serverLyrics: (args: QueryHookArgs<LyricsQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
// This should only be called for Jellyfin. Return null to ignore errors
if (server.type !== ServerType.JELLYFIN) return null;
return api.controller.getLyrics({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.songs.lyrics(args.serverId || '', args.query),
...args.options,
});
},
songLyrics: (args: QueryHookArgs<LyricsQuery>, song: QueueSong | undefined) => {
return queryOptions({
gcTime: Infinity,
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
const server = getServerById(song?.serverId);
if (!server) throw new Error('Server not found');
if (!song) return null;
const { preferLocalLyrics } = useSettingsStore.getState().lyrics;
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
let remoteLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
const subsonicLyrics = await api.controller
.getStructuredLyrics({
apiClientProps: { server, signal },
query: { songId: song.id },
})
.catch(console.error);
if (subsonicLyrics?.length) {
localLyrics = subsonicLyrics;
}
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
const jfLyrics = await api.controller
.getLyrics({
apiClientProps: { server, signal },
query: { songId: song.id },
})
.catch((err) => console.error(err));
if (jfLyrics) {
localLyrics = {
artist: song.artists?.[0]?.name,
lyrics: jfLyrics,
name: song.name,
remote: false,
source: server?.name ?? 'music server',
};
}
} else if (song.lyrics) {
localLyrics = {
artist: song.artists?.[0]?.name,
lyrics: formatLyrics(song.lyrics),
name: song.name,
remote: false,
source: server?.name ?? 'music server',
};
}
if (preferLocalLyrics && localLyrics) {
return localLyrics;
}
const { fetch } = useSettingsStore.getState().lyrics;
if (fetch) {
const remoteLyricsResult: InternetProviderLyricResponse | null =
await lyricsIpc?.getRemoteLyricsBySong(song);
if (remoteLyricsResult) {
remoteLyrics = {
...remoteLyricsResult,
lyrics: formatLyrics(remoteLyricsResult.lyrics),
remote: true,
};
}
}
if (remoteLyrics) {
return remoteLyrics;
}
if (localLyrics) {
return localLyrics;
}
return null;
},
queryKey: queryKeys.songs.lyrics(args.serverId || '', args.query),
staleTime: Infinity,
...args.options,
});
},
songLyricsByRemoteId: (args: QueryHookArgs<Partial<LyricGetQuery>>) => {
return queryOptions({
queryFn: async () => {
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
args.query as any,
);
if (remoteLyricsResult) {
return formatLyrics(remoteLyricsResult);
}
return null;
},
queryKey: queryKeys.songs.lyricsByRemoteId(args.query),
...args.options,
});
},
};

View file

@ -1,6 +1,7 @@
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { openModal } from '@mantine/modals'; import { openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -8,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import styles from './lyrics-search-form.module.css'; import styles from './lyrics-search-form.module.css';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { useLyricSearch } from '/@/renderer/features/lyrics/queries/lyric-search-query'; import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
@ -75,9 +76,11 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
const [debouncedArtist] = useDebouncedValue(form.values.artist, 500); const [debouncedArtist] = useDebouncedValue(form.values.artist, 500);
const [debouncedName] = useDebouncedValue(form.values.name, 500); const [debouncedName] = useDebouncedValue(form.values.name, 500);
const { data, isInitialLoading } = useLyricSearch({ const { data, isInitialLoading } = useQuery(
lyricsQueries.search({
query: { artist: debouncedArtist, name: debouncedName }, query: { artist: debouncedArtist, name: debouncedName },
}); }),
);
const searchResults = useMemo(() => { const searchResults = useMemo(() => {
if (!data) return []; if (!data) return [];

View file

@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { AnimatePresence, motion } from 'motion/react'; import { AnimatePresence, motion } from 'motion/react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
@ -7,12 +8,9 @@ import styles from './lyrics.module.css';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { ErrorFallback } from '/@/renderer/features/action-required'; import { ErrorFallback } from '/@/renderer/features/action-required';
import { translateLyrics } from '/@/renderer/features/lyrics/api/lyric-translate';
import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions'; import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
import {
useSongLyricsByRemoteId,
useSongLyricsBySong,
} from '/@/renderer/features/lyrics/queries/lyric-query';
import { translateLyrics } from '/@/renderer/features/lyrics/queries/lyric-translate';
import { import {
SynchronizedLyrics, SynchronizedLyrics,
SynchronizedLyricsProps, SynchronizedLyricsProps,
@ -43,12 +41,14 @@ export const Lyrics = () => {
const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null); const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null);
const [showTranslation, setShowTranslation] = useState(false); const [showTranslation, setShowTranslation] = useState(false);
const { data, isInitialLoading } = useSongLyricsBySong( const { data, isInitialLoading } = useQuery(
lyricsQueries.songLyrics(
{ {
query: { songId: currentSong?.id || '' }, query: { songId: currentSong?.id || '' },
serverId: currentSong?.serverId || '', serverId: currentSong?.serverId || '',
}, },
currentSong, currentSong,
),
); );
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined); const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
@ -116,7 +116,8 @@ export const Lyrics = () => {
await fetchTranslation(); await fetchTranslation();
}, [translatedLyrics, showTranslation, fetchTranslation]); }, [translatedLyrics, showTranslation, fetchTranslation]);
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({ const { isInitialLoading: isOverrideLoading } = useQuery(
lyricsQueries.songLyricsByRemoteId({
options: { options: {
enabled: !!override, enabled: !!override,
}, },
@ -125,8 +126,9 @@ export const Lyrics = () => {
remoteSource: override?.source as LyricSource | undefined, remoteSource: override?.source as LyricSource | undefined,
song: currentSong, song: currentSong,
}, },
serverId: currentSong?.serverId, serverId: currentSong?.serverId || '',
}); }),
);
useEffect(() => { useEffect(() => {
const unsubSongChange = usePlayerStore.subscribe( const unsubSongChange = usePlayerStore.subscribe(

View file

@ -1,211 +0,0 @@
import { useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query';
import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useLyricsSettings } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils';
import {
FullLyricsMetadata,
InternetProviderLyricResponse,
LyricGetQuery,
LyricsQuery,
QueueSong,
ServerType,
StructuredLyric,
SynchronizedLyricsArray,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
const lyricsIpc = isElectron() ? window.api.lyrics : null;
// Match LRC lyrics format by https://github.com/ustbhuangyi/lyric-parser
// [mm:ss.SSS] text
const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]([^\n]+)(\n|$)/g;
// Match karaoke lyrics format returned by NetEase
// [SSS,???] text
const alternateTimeExp = /\[(\d*),(\d*)]([^\n]+)(\n|$)/g;
const formatLyrics = (lyrics: string) => {
const synchronizedLines = lyrics.matchAll(timeExp);
const formattedLyrics: SynchronizedLyricsArray = [];
for (const line of synchronizedLines) {
const [, minute, sec, ms, text] = line;
const minutes = parseInt(minute, 10);
const seconds = parseInt(sec, 10);
const milis = ms?.length === 3 ? parseInt(ms, 10) : parseInt(ms, 10) * 10;
const timeInMilis = (minutes * 60 + seconds) * 1000 + milis;
formattedLyrics.push([timeInMilis, text]);
}
if (formattedLyrics.length > 0) return formattedLyrics;
const alternateSynchronizedLines = lyrics.matchAll(alternateTimeExp);
for (const line of alternateSynchronizedLines) {
const [, timeInMilis, , text] = line;
const cleanText = text
.replaceAll(/\(\d+,\d+\)/g, '')
.replaceAll(/\s,/g, ',')
.replaceAll(/\s\./g, '.');
formattedLyrics.push([Number(timeInMilis), cleanText]);
}
if (formattedLyrics.length > 0) return formattedLyrics;
// If no synchronized lyrics were found, return the original lyrics
return lyrics;
};
export const useServerLyrics = (
args: QueryHookArgs<LyricsQuery>,
): UseQueryResult<null | string> => {
const { query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
// Note: This currently fetches for every song, even if it shouldn't have
// lyrics, because for some reason HasLyrics is not exposed. Thus, ignore the error
onError: () => {},
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
// This should only be called for Jellyfin. Return null to ignore errors
if (server.type !== ServerType.JELLYFIN) return null;
return api.controller.getLyrics({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
});
};
export const useSongLyricsBySong = (
args: QueryHookArgs<LyricsQuery>,
song: QueueSong | undefined,
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
const { query } = args;
const { fetch, preferLocalLyrics } = useLyricsSettings();
const server = getServerById(song?.serverId);
return useQuery({
cacheTime: Infinity,
enabled: !!song && !!server,
onError: () => {},
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
if (!server) throw new Error('Server not found');
if (!song) return null;
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
let remoteLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
const subsonicLyrics = await api.controller
.getStructuredLyrics({
apiClientProps: { server, signal },
query: { songId: song.id },
})
.catch(console.error);
if (subsonicLyrics?.length) {
localLyrics = subsonicLyrics;
}
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
const jfLyrics = await api.controller
.getLyrics({
apiClientProps: { server, signal },
query: { songId: song.id },
})
.catch((err) => console.error(err));
if (jfLyrics) {
localLyrics = {
artist: song.artists?.[0]?.name,
lyrics: jfLyrics,
name: song.name,
remote: false,
source: server?.name ?? 'music server',
};
}
} else if (song.lyrics) {
localLyrics = {
artist: song.artists?.[0]?.name,
lyrics: formatLyrics(song.lyrics),
name: song.name,
remote: false,
source: server?.name ?? 'music server',
};
}
if (preferLocalLyrics && localLyrics) {
return localLyrics;
}
if (fetch) {
const remoteLyricsResult: InternetProviderLyricResponse | null =
await lyricsIpc?.getRemoteLyricsBySong(song);
if (remoteLyricsResult) {
remoteLyrics = {
...remoteLyricsResult,
lyrics: formatLyrics(remoteLyricsResult.lyrics),
remote: true,
};
}
}
if (remoteLyrics) {
return remoteLyrics;
}
if (localLyrics) {
return localLyrics;
}
return null;
},
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
staleTime: Infinity,
});
};
export const useSongLyricsByRemoteId = (
args: QueryHookArgs<Partial<LyricGetQuery>>,
): UseQueryResult<null | string> => {
const queryClient = useQueryClient();
const { query, serverId } = args;
return useQuery({
enabled: !!query.remoteSongId && !!query.remoteSource,
onError: () => {},
onSuccess: (data) => {
if (!data || !query.song) {
return;
}
const lyricsResult = {
artist: query.song.artists?.[0]?.name,
lyrics: data,
name: query.song.name,
remote: false,
source: query.remoteSource,
};
queryClient.setQueryData(
queryKeys.songs.lyrics(serverId, { songId: query.song.id }),
lyricsResult,
);
},
queryFn: async () => {
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(query as any);
if (remoteLyricsResult) {
return formatLyrics(remoteLyricsResult);
}
return null;
},
queryKey: queryKeys.songs.lyricsByRemoteId(query),
});
};

View file

@ -1,30 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import isElectron from 'is-electron';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import {
InternetProviderLyricSearchResponse,
LyricSearchQuery,
LyricSource,
} from '/@/shared/types/domain-types';
const lyricsIpc = isElectron() ? window.api.lyrics : null;
export const useLyricSearch = (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
const { options, query } = args;
return useQuery<Record<LyricSource, InternetProviderLyricSearchResponse[]>>({
cacheTime: 1000 * 60 * 1,
enabled: !!query.artist || !!query.name,
queryFn: () => {
if (lyricsIpc) {
return lyricsIpc.searchRemoteLyrics(query);
}
return {} as Record<LyricSource, InternetProviderLyricSearchResponse[]>;
},
queryKey: queryKeys.songs.lyricsSearch(query),
staleTime: 1000 * 60 * 1,
...options,
});
};

View file

@ -94,7 +94,7 @@ export const ShuffleAllModal = ({
const handlePlay = async (playType: Play) => { const handlePlay = async (playType: Play) => {
const res = await queryClient.fetchQuery({ const res = await queryClient.fetchQuery({
cacheTime: 0, gcTime: 0,
queryFn: ({ signal }) => queryFn: ({ signal }) =>
api.controller.getRandomSongList({ api.controller.getRandomSongList({
apiClientProps: { apiClientProps: {
@ -253,7 +253,7 @@ export const openShuffleAllModal = async (
const server = useAuthStore.getState().currentServer; const server = useAuthStore.getState().currentServer;
const genres = await props.queryClient.fetchQuery({ const genres = await props.queryClient.fetchQuery({
cacheTime: 1000 * 60 * 60 * 4, gcTime: 1000 * 60 * 5,
queryFn: ({ signal }) => queryFn: ({ signal }) =>
api.controller.getGenreList({ api.controller.getGenreList({
apiClientProps: { apiClientProps: {
@ -267,11 +267,11 @@ export const openShuffleAllModal = async (
}, },
}), }),
queryKey: queryKeys.genres.list(server?.id), queryKey: queryKeys.genres.list(server?.id),
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 60 * 4,
}); });
const musicFolders = await props.queryClient.fetchQuery({ const musicFolders = await props.queryClient.fetchQuery({
cacheTime: 1000 * 60 * 60 * 4, gcTime: 1000 * 60 * 5,
queryFn: ({ signal }) => queryFn: ({ signal }) =>
api.controller.getMusicFolderList({ api.controller.getMusicFolderList({
apiClientProps: { apiClientProps: {
@ -280,7 +280,7 @@ export const openShuffleAllModal = async (
}, },
}), }),
queryKey: queryKeys.musicFolders.list(server?.id), queryKey: queryKeys.musicFolders.list(server?.id),
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 60 * 4,
}); });
openModal({ openModal({

View file

@ -28,9 +28,9 @@ export const getPlaylistSongsById = async (args: {
const queryKey = queryKeys.playlists.songList(server?.id, id); const queryKey = queryKeys.playlists.songList(server?.id, id);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getPlaylistSongList({ api.controller.getPlaylistSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -38,11 +38,9 @@ export const getPlaylistSongsById = async (args: {
}, },
query: queryFilter, query: queryFilter,
}), }),
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
if (res) { if (res) {
res.items = sortSongList( res.items = sortSongList(
@ -74,9 +72,9 @@ export const getAlbumSongsById = async (args: {
const queryKey = queryKeys.songs.list(server?.id, queryFilter); const queryKey = queryKeys.songs.list(server?.id, queryFilter);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -84,11 +82,9 @@ export const getAlbumSongsById = async (args: {
}, },
query: queryFilter, query: queryFilter,
}), }),
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
return res; return res;
}; };
@ -118,9 +114,9 @@ export const getGenreSongsById = async (args: {
const queryKey = queryKeys.songs.list(server?.id, queryFilter); const queryKey = queryKeys.songs.list(server?.id, queryFilter);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -128,11 +124,9 @@ export const getGenreSongsById = async (args: {
}, },
query: queryFilter, query: queryFilter,
}), }),
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
data.items.push(...res!.items); data.items.push(...res!.items);
if (data.totalRecordCount) { if (data.totalRecordCount) {
@ -162,9 +156,9 @@ export const getAlbumArtistSongsById = async (args: {
const queryKey = queryKeys.songs.list(server?.id, queryFilter); const queryKey = queryKeys.songs.list(server?.id, queryFilter);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -172,11 +166,9 @@ export const getAlbumArtistSongsById = async (args: {
}, },
query: queryFilter, query: queryFilter,
}), }),
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
return res; return res;
}; };
@ -199,9 +191,9 @@ export const getArtistSongsById = async (args: {
const queryKey = queryKeys.songs.list(server?.id, queryFilter); const queryKey = queryKeys.songs.list(server?.id, queryFilter);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -209,11 +201,9 @@ export const getArtistSongsById = async (args: {
}, },
query: queryFilter, query: queryFilter,
}), }),
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
return res; return res;
}; };
@ -234,9 +224,9 @@ export const getSongsByQuery = async (args: {
const queryKey = queryKeys.songs.list(server?.id, queryFilter); const queryKey = queryKeys.songs.list(server?.id, queryFilter);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => { queryFn: async ({ signal }) => {
return api.controller.getSongList({ return api.controller.getSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -245,11 +235,9 @@ export const getSongsByQuery = async (args: {
query: queryFilter, query: queryFilter,
}); });
}, },
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
return res; return res;
}; };
@ -265,9 +253,9 @@ export const getSongById = async (args: {
const queryKey = queryKeys.songs.detail(server?.id, queryFilter); const queryKey = queryKeys.songs.detail(server?.id, queryFilter);
const res = await queryClient.fetchQuery( const res = await queryClient.fetchQuery({
queryKey, gcTime: 1000 * 60,
async ({ signal }) => queryFn: async ({ signal }) =>
api.controller.getSongDetail({ api.controller.getSongDetail({
apiClientProps: { apiClientProps: {
server, server,
@ -275,11 +263,9 @@ export const getSongById = async (args: {
}, },
query: queryFilter, query: queryFilter,
}), }),
{ queryKey,
cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, });
);
if (!res) throw new Error('Song not found'); if (!res) throw new Error('Song not found');

View file

@ -0,0 +1,57 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import {
PlaylistDetailQuery,
PlaylistListQuery,
PlaylistSongListQuery,
} from '/@/shared/types/domain-types';
export const playlistsQueries = {
detail: (args: QueryHookArgs<PlaylistDetailQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getPlaylistDetail({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.playlists.detail(args.serverId || '', args.query.id, args.query),
...args.options,
});
},
list: (args: QueryHookArgs<PlaylistListQuery>) => {
return queryOptions({
gcTime: 1000 * 60 * 60,
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getPlaylistList({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.playlists.list(args.serverId || '', args.query),
...args.options,
});
},
songList: (args: QueryHookArgs<PlaylistSongListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getPlaylistSongList({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.playlists.songList(args.serverId || '', args.query.id),
...args.options,
});
},
};

View file

@ -1,5 +1,6 @@
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { closeModal, ContextModalProps } from '@mantine/modals'; import { closeModal, ContextModalProps } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -8,8 +9,8 @@ import styles from './add-to-playlist-context-modal.module.css';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { getGenreSongsById } from '/@/renderer/features/player'; import { getGenreSongsById } from '/@/renderer/features/player';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-to-playlist-mutation'; import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-to-playlist-mutation';
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
@ -66,7 +67,8 @@ export const AddToPlaylistContextModal = ({
const addToPlaylistMutation = useAddToPlaylist({}); const addToPlaylistMutation = useAddToPlaylist({});
const playlistList = usePlaylistList({ const playlistList = useQuery(
playlistsQueries.list({
query: { query: {
_custom: { _custom: {
navidrome: { navidrome: {
@ -78,7 +80,8 @@ export const AddToPlaylistContextModal = ({
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const [playlistSelect, playlistMap] = useMemo(() => { const [playlistSelect, playlistMap] = useMemo(() => {
const existingPlaylists = new Array<Playlist & { label: string; value: string }>(); const existingPlaylists = new Array<Playlist & { label: string; value: string }>();
@ -113,9 +116,15 @@ export const AddToPlaylistContextModal = ({
const queryKey = queryKeys.songs.list(server?.id || '', query); const queryKey = queryKeys.songs.list(server?.id || '', query);
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => { const songsRes = await queryClient.fetchQuery({
queryFn: ({ signal }) => {
if (!server) throw new Error('No server'); if (!server) throw new Error('No server');
return api.controller.getSongList({ apiClientProps: { server, signal }, query }); return api.controller.getSongList({
apiClientProps: { server, signal },
query,
});
},
queryKey,
}); });
return songsRes; return songsRes;
@ -134,9 +143,15 @@ export const AddToPlaylistContextModal = ({
const queryKey = queryKeys.songs.list(server?.id || '', query); const queryKey = queryKeys.songs.list(server?.id || '', query);
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => { const songsRes = await queryClient.fetchQuery({
queryFn: ({ signal }) => {
if (!server) throw new Error('No server'); if (!server) throw new Error('No server');
return api.controller.getSongList({ apiClientProps: { server, signal }, query }); return api.controller.getSongList({
apiClientProps: { server, signal },
query,
});
},
queryKey,
}); });
return songsRes; return songsRes;
@ -213,9 +228,8 @@ export const AddToPlaylistContextModal = ({
if (values.skipDuplicates) { if (values.skipDuplicates) {
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId); const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId);
const playlistSongsRes = await queryClient.fetchQuery( const playlistSongsRes = await queryClient.fetchQuery({
queryKey, queryFn: ({ signal }) => {
({ signal }) => {
if (!server) if (!server)
throw new Error( throw new Error(
t('error.serverNotSelectedError', { t('error.serverNotSelectedError', {
@ -232,7 +246,8 @@ export const AddToPlaylistContextModal = ({
}, },
}); });
}, },
); queryKey,
});
const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id); const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id);
@ -508,7 +523,7 @@ export const AddToPlaylistContextModal = ({
/> />
<Group justify="flex-end"> <Group justify="flex-end">
<ModalButton <ModalButton
disabled={isLoading || addToPlaylistMutation.isLoading} disabled={isLoading || addToPlaylistMutation.isPending}
onClick={() => closeModal(id)} onClick={() => closeModal(id)}
uppercase uppercase
variant="subtle" variant="subtle"
@ -518,7 +533,7 @@ export const AddToPlaylistContextModal = ({
<ModalButton <ModalButton
disabled={ disabled={
isLoading || isLoading ||
addToPlaylistMutation.isLoading || addToPlaylistMutation.isPending ||
(form.values.selectedPlaylistIds.length === 0 && (form.values.selectedPlaylistIds.length === 0 &&
form.values.newPlaylists.length === 0) form.values.newPlaylists.length === 0)
} }

View file

@ -95,7 +95,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
}); });
const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST); const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isSubmitDisabled = !form.values.name || mutation.isLoading; const isSubmitDisabled = !form.values.name || mutation.isPending;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -160,7 +160,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
</ModalButton> </ModalButton>
<ModalButton <ModalButton
disabled={isSubmitDisabled} disabled={isSubmitDisabled}
loading={mutation.isLoading} loading={mutation.isPending}
type="submit" type="submit"
variant="filled" variant="filled"
> >

View file

@ -8,7 +8,7 @@ import type {
} from '@ag-grid-community/core'; } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { AnimatePresence } from 'motion/react'; import { AnimatePresence } from 'motion/react';
import { MutableRefObject, useCallback, useMemo } from 'react'; import { MutableRefObject, useCallback, useMemo } from 'react';
@ -25,7 +25,7 @@ import {
SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS, SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items'; } from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { useAppFocus } from '/@/renderer/hooks'; import { useAppFocus } from '/@/renderer/hooks';
import { import {
useCurrentServer, useCurrentServer,
@ -70,7 +70,9 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai
}; };
}, [page?.table.id, playlistId]); }, [page?.table.id, playlistId]);
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
);
const p = usePlaylistDetailTablePagination(playlistId); const p = usePlaylistDetailTablePagination(playlistId);
const pagination = { const pagination = {

View file

@ -1,7 +1,7 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react'; import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -10,9 +10,9 @@ import { useNavigate, useParams } from 'react-router';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form'; import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation'; import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { OrderToggleButton } from '/@/renderer/features/shared'; import { OrderToggleButton } from '/@/renderer/features/shared';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { MoreButton } from '/@/renderer/features/shared/components/more-button'; import { MoreButton } from '/@/renderer/features/shared/components/more-button';
@ -262,7 +262,9 @@ export const PlaylistDetailSongListHeaderFilters = ({
const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID; const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID;
const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC; const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC;
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
);
const isSmartPlaylist = detailQuery.data?.rules; const isSmartPlaylist = detailQuery.data?.rules;
const cq = useContainerQuery(); const cq = useContainerQuery();
@ -291,7 +293,9 @@ export const PlaylistDetailSongListHeaderFilters = ({
}, [tableRef, page.display, setPagination]); }, [tableRef, page.display, setPagination]);
const handleRefresh = () => { const handleRefresh = () => {
queryClient.invalidateQueries(queryKeys.playlists.songList(server?.id || '', playlistId)); queryClient.invalidateQueries({
queryKey: queryKeys.playlists.songList(server?.id || '', playlistId),
});
handleFilterChange(); handleFilterChange();
}; };

View file

@ -1,19 +1,27 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { MutableRefObject } from 'react'; import { MutableRefObject } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters'; import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner'; import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import {
LibraryItem,
PlaylistSongListQueryClientSide,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
interface PlaylistDetailHeaderProps { interface PlaylistDetailHeaderProps {
@ -33,7 +41,23 @@ export const PlaylistDetailSongListHeader = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { playlistId } = useParams() as { playlistId: string }; const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
);
const handlePlayQueueAdd = usePlayQueueAdd();
const page = usePlaylistDetailStore();
const filters: Partial<PlaylistSongListQueryClientSide> = {
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
};
const handlePlay = async (playType: Play) => {
handlePlayQueueAdd?.({
byItemType: { id: [playlistId], type: LibraryItem.PLAYLIST },
playType,
query: filters,
});
};
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();

View file

@ -121,7 +121,8 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
const queryKey = queryKeys.playlists.list(server?.id || '', query); const queryKey = queryKeys.playlists.list(server?.id || '', query);
const playlists = await queryClient.fetchQuery(queryKey, async ({ signal }) => const playlists = await queryClient.fetchQuery({
queryFn: async ({ signal }) =>
controller.getPlaylistList({ controller.getPlaylistList({
apiClientProps: { apiClientProps: {
server, server,
@ -129,7 +130,8 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
}, },
query, query,
}), }),
); queryKey,
});
return playlists; return playlists;
}, },

View file

@ -170,7 +170,8 @@ export const PlaylistListHeaderFilters = ({
const queryKey = queryKeys.playlists.list(server?.id || '', query); const queryKey = queryKeys.playlists.list(server?.id || '', query);
const playlists = await queryClient.fetchQuery(queryKey, async ({ signal }) => const playlists = await queryClient.fetchQuery({
queryFn: async ({ signal }) =>
api.controller.getPlaylistList({ api.controller.getPlaylistList({
apiClientProps: { apiClientProps: {
server, server,
@ -178,7 +179,8 @@ export const PlaylistListHeaderFilters = ({
}, },
query, query,
}), }),
); queryKey,
});
return playlists; return playlists;
}, },
@ -207,9 +209,8 @@ export const PlaylistListHeaderFilters = ({
...pageFilters, ...pageFilters,
}); });
const playlistsRes = await queryClient.fetchQuery( const playlistsRes = await queryClient.fetchQuery({
queryKey, queryFn: async ({ signal }) =>
async ({ signal }) =>
api.controller.getPlaylistList({ api.controller.getPlaylistList({
apiClientProps: { apiClientProps: {
server, server,
@ -221,7 +222,8 @@ export const PlaylistListHeaderFilters = ({
...pageFilters, ...pageFilters,
}, },
}), }),
); queryKey,
});
params.successCallback( params.successCallback(
playlistsRes?.items || [], playlistsRes?.items || [],
@ -338,7 +340,9 @@ export const PlaylistListHeaderFilters = ({
}; };
const handleRefresh = () => { const handleRefresh = () => {
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || '', filter)); queryClient.invalidateQueries({
queryKey: queryKeys.playlists.list(server?.id || '', filter),
});
handleFilterChange(filter); handleFilterChange(filter);
}; };

View file

@ -1,5 +1,6 @@
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { openModal } from '@mantine/modals'; import { openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import clone from 'lodash/clone'; import clone from 'lodash/clone';
import get from 'lodash/get'; import get from 'lodash/get';
import setWith from 'lodash/setWith'; import setWith from 'lodash/setWith';
@ -8,7 +9,7 @@ import { forwardRef, Ref, useImperativeHandle, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QueryBuilder } from '/@/renderer/components/query-builder'; import { QueryBuilder } from '/@/renderer/components/query-builder';
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { import {
convertNDQueryToQueryGroup, convertNDQueryToQueryGroup,
convertQueryGroupToNDQuery, convertQueryGroupToNDQuery,
@ -108,10 +109,12 @@ export const PlaylistQueryBuilder = forwardRef(
query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY, query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY,
); );
const { data: playlists } = usePlaylistList({ const { data: playlists } = useQuery(
playlistsQueries.list({
query: { sortBy: PlaylistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0 }, query: { sortBy: PlaylistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0 },
serverId: server?.id, serverId: server?.id,
}); }),
);
const playlistData = useMemo(() => { const playlistData = useMemo(() => {
if (!playlists) return []; if (!playlists) return [];

View file

@ -70,7 +70,7 @@ export const SaveAsPlaylistForm = ({
}); });
const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST); const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isSubmitDisabled = !form.values.name || mutation.isLoading; const isSubmitDisabled = !form.values.name || mutation.isPending;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -106,7 +106,7 @@ export const SaveAsPlaylistForm = ({
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton> <ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
<ModalButton <ModalButton
disabled={isSubmitDisabled} disabled={isSubmitDisabled}
loading={mutation.isLoading} loading={mutation.isPending}
type="submit" type="submit"
variant="filled" variant="filled"
> >

View file

@ -88,7 +88,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST); const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isOwnerDisplayed = server?.type === ServerType.NAVIDROME && userList; const isOwnerDisplayed = server?.type === ServerType.NAVIDROME && userList;
const isSubmitDisabled = !form.values.name || mutation.isLoading; const isSubmitDisabled = !form.values.name || mutation.isPending;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -143,7 +143,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
<ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton> <ModalButton onClick={onCancel}>{t('common.cancel')}</ModalButton>
<ModalButton <ModalButton
disabled={isSubmitDisabled} disabled={isSubmitDisabled}
loading={mutation.isLoading} loading={mutation.isPending}
type="submit" type="submit"
variant="filled" variant="filled"
> >

View file

@ -27,11 +27,16 @@ export const useAddToPlaylist = (args: MutationHookArgs) => {
if (!serverId) return; if (!serverId) return;
queryClient.invalidateQueries(queryKeys.playlists.list(serverId), { exact: false }); queryClient.invalidateQueries({
queryClient.invalidateQueries(queryKeys.playlists.detail(serverId, variables.query.id)); exact: false,
queryClient.invalidateQueries( queryKey: queryKeys.playlists.list(serverId),
queryKeys.playlists.songList(serverId, variables.query.id), });
); queryClient.invalidateQueries({
queryKey: queryKeys.playlists.detail(serverId, variables.query.id),
});
queryClient.invalidateQueries({
queryKey: queryKeys.playlists.songList(serverId, variables.query.id),
});
}, },
...options, ...options,
}); });

View file

@ -25,7 +25,10 @@ export const useCreatePlaylist = (args: MutationHookArgs) => {
onSuccess: (_args, variables) => { onSuccess: (_args, variables) => {
const server = getServerById(variables.serverId); const server = getServerById(variables.serverId);
if (server) { if (server) {
queryClient.invalidateQueries(queryKeys.playlists.list(server.id)); queryClient.invalidateQueries({
exact: false,
queryKey: queryKeys.playlists.list(server.id),
});
} }
}, },
...options, ...options,

View file

@ -24,11 +24,14 @@ export const useDeletePlaylist = (args: MutationHookArgs) => {
return api.controller.deletePlaylist({ ...args, apiClientProps: { server } }); return api.controller.deletePlaylist({ ...args, apiClientProps: { server } });
}, },
onMutate: () => { onMutate: () => {
queryClient.cancelQueries(queryKeys.playlists.list(server?.id || '')); queryClient.cancelQueries({ queryKey: queryKeys.playlists.list(server?.id || '') });
return null; return null;
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || '')); queryClient.invalidateQueries({
exact: false,
queryKey: queryKeys.playlists.list(server?.id || ''),
});
}, },
...options, ...options,
}); });

View file

@ -26,11 +26,15 @@ export const useRemoveFromPlaylist = (options?: MutationOptions) => {
if (!serverId) return; if (!serverId) return;
queryClient.invalidateQueries(queryKeys.playlists.list(serverId), { exact: false }); queryClient.invalidateQueries({
queryClient.invalidateQueries(queryKeys.playlists.detail(serverId, variables.query.id)); queryKey: queryKeys.playlists.list(serverId),
queryClient.invalidateQueries( });
queryKeys.playlists.songList(serverId, variables.query.id), queryClient.invalidateQueries({
); queryKey: queryKeys.playlists.detail(serverId, variables.query.id),
});
queryClient.invalidateQueries({
queryKey: queryKeys.playlists.songList(serverId, variables.query.id),
});
}, },
...options, ...options,
}); });

View file

@ -27,10 +27,14 @@ export const useUpdatePlaylist = (args: MutationHookArgs) => {
if (!serverId) return; if (!serverId) return;
queryClient.invalidateQueries(queryKeys.playlists.list(serverId)); queryClient.invalidateQueries({
queryKey: queryKeys.playlists.list(serverId),
});
if (query?.id) { if (query?.id) {
queryClient.invalidateQueries(queryKeys.playlists.detail(serverId, query.id)); queryClient.invalidateQueries({
queryKey: queryKeys.playlists.detail(serverId, query.id),
});
} }
}, },
...options, ...options,

View file

@ -1,23 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { PlaylistDetailQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const usePlaylistDetail = (args: QueryHookArgs<PlaylistDetailQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getPlaylistDetail({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.playlists.detail(server?.id || '', query.id, query),
...options,
});
};

View file

@ -1,28 +0,0 @@
import type { QueryOptions } from '/@/renderer/lib/react-query';
import type { PlaylistListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const usePlaylistList = (args: {
options?: QueryOptions;
query: PlaylistListQuery;
serverId?: string;
}) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
cacheTime: 1000 * 60 * 60,
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getPlaylistList({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.playlists.list(server?.id || '', query),
...options,
});
};

View file

@ -1,26 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { PlaylistSongListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getPlaylistSongList({
apiClientProps: { server, signal },
query,
});
},
queryKey: queryKeys.playlists.songList(server?.id || '', query.id),
...options,
});
};

View file

@ -1,20 +1,21 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import Fuse from 'fuse.js';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate, useParams } from 'react-router'; import { generatePath, useNavigate, useParams } from 'react-router';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content'; import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content';
import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header'; import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder'; import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form'; import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation'; import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation'; import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store'; import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store';
@ -35,7 +36,9 @@ const PlaylistDetailSongListRoute = () => {
const server = useCurrentServer(); const server = useCurrentServer();
const handlePlayQueueAdd = useHandlePlayQueueAdd(); const handlePlayQueueAdd = useHandlePlayQueueAdd();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
);
const createPlaylistMutation = useCreatePlaylist({}); const createPlaylistMutation = useCreatePlaylist({});
const deletePlaylistMutation = useDeletePlaylist({}); const deletePlaylistMutation = useDeletePlaylist({});
@ -148,12 +151,14 @@ const PlaylistDetailSongListRoute = () => {
const page = usePlaylistDetailStore(); const page = usePlaylistDetailStore();
const playlistSongs = usePlaylistSongList({ const playlistSongs = useQuery(
playlistsQueries.songList({
query: { query: {
id: playlistId, id: playlistId,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const filterSortedSongs = useMemo(() => { const filterSortedSongs = useMemo(() => {
let items = playlistSongs.data?.items; let items = playlistSongs.data?.items;

View file

@ -1,13 +1,14 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content'; import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content';
import { PlaylistListHeader } from '/@/renderer/features/playlists/components/playlist-list-header'; import { PlaylistListHeader } from '/@/renderer/features/playlists/components/playlist-list-header';
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store'; import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { PlaylistListSort, PlaylistSongListQuery, SortOrder } from '/@/shared/types/domain-types'; import { PlaylistListSort, PlaylistSongListQuery, SortOrder } from '/@/shared/types/domain-types';
@ -20,10 +21,10 @@ const PlaylistListRoute = () => {
const pageKey = 'playlist'; const pageKey = 'playlist';
const { filter } = useListStoreByKey<PlaylistSongListQuery>({ key: pageKey }); const { filter } = useListStoreByKey<PlaylistSongListQuery>({ key: pageKey });
const itemCountCheck = usePlaylistList({ const itemCountCheck = useQuery(
playlistsQueries.list({
options: { options: {
cacheTime: 1000 * 60 * 60 * 2, gcTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
}, },
query: { query: {
...filter, ...filter,
@ -33,7 +34,8 @@ const PlaylistListRoute = () => {
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -0,0 +1,22 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { SearchQuery } from '/@/shared/types/domain-types';
export const searchQueries = {
search: (args: QueryHookArgs<SearchQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.search({
apiClientProps: { server: getServerById(args.serverId), signal },
query: args.query,
});
},
queryKey: queryKeys.search.list(args.serverId || '', args.query),
...args.options,
});
},
};

View file

@ -1,16 +1,17 @@
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { Fragment, useCallback, useRef, useState } from 'react'; import { Fragment, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { searchQueries } from '/@/renderer/features/search/api/search-api';
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands'; import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
import { HomeCommands } from '/@/renderer/features/search/components/home-commands'; import { HomeCommands } from '/@/renderer/features/search/components/home-commands';
import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item';
import { ServerCommands } from '/@/renderer/features/search/components/server-commands'; import { ServerCommands } from '/@/renderer/features/search/components/server-commands';
import { useSearch } from '/@/renderer/features/search/queries/search-query';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
@ -48,7 +49,8 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
}); });
}, []); }, []);
const { data, isLoading } = useSearch({ const { data, isLoading } = useQuery(
searchQueries.search({
options: { enabled: isHome && debouncedQuery !== '' && query !== '' }, options: { enabled: isHome && debouncedQuery !== '' && query !== '' },
query: { query: {
albumArtistLimit: 4, albumArtistLimit: 4,
@ -60,7 +62,8 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
songStartIndex: 0, songStartIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0); const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0);
const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0); const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0);

View file

@ -1,28 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { SearchQuery } from '/@/shared/types/domain-types';
export const useSearch = (args: QueryHookArgs<SearchQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.search({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.search.list(serverId || '', query),
...options,
});
};

View file

@ -0,0 +1,63 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { MusicFolderListQuery, TagQuery, UserListQuery } from '/@/shared/types/domain-types';
export const sharedQueries = {
musicFolders: (args: QueryHookArgs<MusicFolderListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getMusicFolderList({ apiClientProps: { server, signal } });
},
queryKey: queryKeys.musicFolders.list(args.serverId || ''),
...args.options,
});
},
roles: (args: QueryHookArgs<object>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getRoles({
apiClientProps: { server, signal },
});
},
queryKey: queryKeys.roles.list(args.serverId || ''),
...args.options,
});
},
tags: (args: QueryHookArgs<TagQuery>) => {
return queryOptions({
gcTime: 1000 * 60,
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getTags({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.tags.list(args.serverId || '', args.query.type),
...args.options,
});
},
users: (args: QueryHookArgs<UserListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getUserList({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.users.list(args.serverId || '', args.query),
...args.options,
});
},
};

View file

@ -1,24 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { MusicFolderListQuery } from '/@/shared/types/domain-types';
export const useMusicFolders = (args: QueryHookArgs<MusicFolderListQuery>) => {
const { options, serverId } = args || {};
const server = getServerById(serverId);
const query = useQuery({
enabled: !!server,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getMusicFolderList({ apiClientProps: { server, signal } });
},
queryKey: queryKeys.musicFolders.list(server?.id || ''),
...options,
});
return query;
};

View file

@ -1,4 +1,5 @@
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx'; import clsx from 'clsx';
import { MouseEvent, useCallback, useMemo, useState } from 'react'; import { MouseEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -8,7 +9,8 @@ import { Link } from 'react-router-dom';
import styles from './sidebar-playlist-list.module.css'; import styles from './sidebar-playlist-list.module.css';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists'; import { CreatePlaylistForm } from '/@/renderer/features/playlists';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item'; import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
@ -142,14 +144,16 @@ export const SidebarPlaylistList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const playlistsQuery = usePlaylistList({ const playlistsQuery = useQuery(
playlistsQueries.list({
query: { query: {
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const handlePlayPlaylist = useCallback( const handlePlayPlaylist = useCallback(
(id: string, playType: Play) => { (id: string, playType: Play) => {
@ -258,14 +262,16 @@ export const SidebarSharedPlaylistList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const playlistsQuery = usePlaylistList({ const playlistsQuery = useQuery(
playlistsQueries.list({
query: { query: {
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const handlePlayPlaylist = useCallback( const handlePlayPlaylist = useCallback(
(id: string, playType: Play) => { (id: string, playType: Play) => {

View file

@ -1,5 +1,6 @@
import { RowDoubleClickedEvent } from '@ag-grid-community/core'; import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import { AgGridReact } from '@ag-grid-community/react'; import { AgGridReact } from '@ag-grid-community/react';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
@ -9,7 +10,7 @@ import { ErrorFallback } from '/@/renderer/features/action-required';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { useSimilarSongs } from '/@/renderer/features/similar-songs/queries/similar-song-queries'; import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { usePlayButtonBehavior, useTableSettings } from '/@/renderer/store'; import { usePlayButtonBehavior, useTableSettings } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spinner } from '/@/shared/components/spinner/spinner';
import { LibraryItem, Song } from '/@/shared/types/domain-types'; import { LibraryItem, Song } from '/@/shared/types/domain-types';
@ -26,14 +27,19 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
const handlePlayQueueAdd = useHandlePlayQueueAdd(); const handlePlayQueueAdd = useHandlePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const songQuery = useSimilarSongs({ const songQuery = useQuery(
songsQueries.similar({
options: { options: {
cacheTime: 1000 * 60 * 2, gcTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, },
query: {
albumArtistIds: song.albumArtists.map((art) => art.id),
count,
songId: song.id,
}, },
query: { albumArtistIds: song.albumArtists.map((art) => art.id), count, songId: song.id },
serverId: song?.serverId, serverId: song?.serverId,
}); }),
);
const columnDefs = useMemo( const columnDefs = useMemo(
() => getColumnDefs(tableConfig.columns, false, 'generic'), () => getColumnDefs(tableConfig.columns, false, 'generic'),

View file

@ -1,30 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { SimilarSongsQuery } from '/@/shared/types/domain-types';
export const useSimilarSongs = (args: QueryHookArgs<SimilarSongsQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getSimilarSongs({
apiClientProps: { server, signal },
query: {
albumArtistIds: query.albumArtistIds,
count: query.count ?? 50,
songId: query.songId,
},
});
},
queryKey: queryKeys.songs.similar(server?.id || '', query),
...options,
});
};

View file

@ -0,0 +1,60 @@
import { queryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { SimilarSongsQuery, SongListQuery } from '/@/shared/types/domain-types';
export const songsQueries = {
list: (args: QueryHookArgs<SongListQuery>, imageSize?: number) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return controller.getSongList({
apiClientProps: { server, signal },
query: { ...args.query, imageSize },
});
},
queryKey: queryKeys.songs.list(args.serverId || '', { ...args.query, imageSize }),
...args.options,
});
},
listCount: (args: QueryHookArgs<SongListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getSongListCount({
apiClientProps: { server, signal },
query: args.query,
});
},
queryKey: queryKeys.songs.count(
args.serverId || '',
Object.keys(args.query).length === 0 ? undefined : args.query,
),
...args.options,
});
},
similar: (args: QueryHookArgs<SimilarSongsQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
const server = getServerById(args.serverId);
if (!server) throw new Error('Server not found');
return api.controller.getSimilarSongs({
apiClientProps: { server, signal },
query: {
albumArtistIds: args.query.albumArtistIds,
count: args.query.count ?? 50,
songId: args.query.songId,
},
});
},
queryKey: queryKeys.songs.similar(args.serverId || '', args.query),
...args.options,
});
},
};

View file

@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store'; import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@ -18,7 +19,7 @@ interface JellyfinSongFiltersProps {
customFilters?: Partial<SongListFilter>; customFilters?: Partial<SongListFilter>;
onFilterChange: (filters: SongListFilter) => void; onFilterChange: (filters: SongListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId: string;
} }
export const JellyfinSongFilters = ({ export const JellyfinSongFilters = ({
@ -35,7 +36,8 @@ export const JellyfinSongFilters = ({
// Despite the fact that getTags returns genres, it only returns genre names. // Despite the fact that getTags returns genres, it only returns genre names.
// We prefer using IDs, hence the double query // We prefer using IDs, hence the double query
const genreListQuery = useGenreList({ const genreListQuery = useQuery(
genresQueries.list({
query: { query: {
musicFolderId: filter?.musicFolderId, musicFolderId: filter?.musicFolderId,
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
@ -43,7 +45,8 @@ export const JellyfinSongFilters = ({
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
@ -53,13 +56,15 @@ export const JellyfinSongFilters = ({
})); }));
}, [genreListQuery.data]); }, [genreListQuery.data]);
const tagsQuery = useTagList({ const tagsQuery = useQuery(
sharedQueries.tags({
query: { query: {
folder: filter?.musicFolderId, folder: filter?.musicFolderId,
type: LibraryItem.SONG, type: LibraryItem.SONG,
}, },
serverId, serverId,
}); }),
);
const selectedGenres = useMemo(() => { const selectedGenres = useMemo(() => {
return filter?._custom?.jellyfin?.GenreIds?.split(','); return filter?._custom?.jellyfin?.GenreIds?.split(',');

View file

@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -6,8 +7,8 @@ import {
MultiSelectWithInvalidData, MultiSelectWithInvalidData,
SelectWithInvalidData, SelectWithInvalidData,
} from '/@/renderer/components/select-with-invalid-data'; } from '/@/renderer/components/select-with-invalid-data';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { import {
getServerById, getServerById,
SongListFilter, SongListFilter,
@ -29,7 +30,7 @@ interface NavidromeSongFiltersProps {
customFilters?: Partial<SongListFilter>; customFilters?: Partial<SongListFilter>;
onFilterChange: (filters: SongListFilter) => void; onFilterChange: (filters: SongListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId: string;
} }
export const NavidromeSongFilters = ({ export const NavidromeSongFilters = ({
@ -45,21 +46,25 @@ export const NavidromeSongFilters = ({
const isGenrePage = customFilters?.genreIds !== undefined; const isGenrePage = customFilters?.genreIds !== undefined;
const genreListQuery = useGenreList({ const genreListQuery = useQuery(
genresQueries.list({
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const tagsQuery = useTagList({ const tagsQuery = useQuery(
sharedQueries.tags({
query: { query: {
type: LibraryItem.SONG, type: LibraryItem.SONG,
}, },
serverId, serverId,
}); }),
);
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];

View file

@ -13,7 +13,7 @@ import {
VirtualInfiniteGridRef, VirtualInfiniteGridRef,
} from '/@/renderer/components/virtual-grid'; } from '/@/renderer/components/virtual-grid';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite'; import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
@ -26,7 +26,6 @@ import {
SongListSort, SongListSort,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types'; import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface SongListGridViewProps { interface SongListGridViewProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>; gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number; itemCount?: number;
@ -185,7 +184,8 @@ export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps)
const queryKey = queryKeys.songs.list(server?.id || '', query, id); const queryKey = queryKeys.songs.list(server?.id || '', query, id);
const songs = await queryClient.fetchQuery(queryKey, async ({ signal }) => const songs = await queryClient.fetchQuery({
queryFn: async ({ signal }) =>
controller.getSongList({ controller.getSongList({
apiClientProps: { apiClientProps: {
server, server,
@ -193,7 +193,8 @@ export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps)
}, },
query, query,
}), }),
); queryKey,
});
return songs; return songs;
}, },

View file

@ -1,6 +1,7 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { openModal } from '@mantine/modals'; import { openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -10,7 +11,8 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; import { OrderToggleButton } from '/@/renderer/features/shared';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { FilterButton } from '/@/renderer/features/shared/components/filter-button'; import { FilterButton } from '/@/renderer/features/shared/components/filter-button';
import { FolderButton } from '/@/renderer/features/shared/components/folder-button'; import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
@ -223,7 +225,9 @@ export const SongListHeaderFilters = ({
const cq = useContainerQuery(); const cq = useContainerQuery();
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const musicFoldersQuery = useQuery(
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
);
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -407,7 +411,7 @@ export const SongListHeaderFilters = ({
}; };
const handleRefresh = () => { const handleRefresh = () => {
queryClient.invalidateQueries(queryKeys.songs.list(server?.id || '')); queryClient.invalidateQueries({ queryKey: queryKeys.songs.list(server?.id || '') });
if (isGrid) { if (isGrid) {
handleRefreshGrid(gridRef, filter); handleRefreshGrid(gridRef, filter);
} else { } else {

View file

@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store'; import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@ -16,7 +17,7 @@ interface SubsonicSongFiltersProps {
customFilters?: Partial<SongListFilter>; customFilters?: Partial<SongListFilter>;
onFilterChange: (filters: SongListFilter) => void; onFilterChange: (filters: SongListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId: string;
} }
export const SubsonicSongFilters = ({ export const SubsonicSongFilters = ({
@ -31,14 +32,16 @@ export const SubsonicSongFilters = ({
const isGenrePage = customFilters?.genreIds !== undefined; const isGenrePage = customFilters?.genreIds !== undefined;
const genreListQuery = useGenreList({ const genreListQuery = useQuery(
genresQueries.list({
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
}); }),
);
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];

View file

@ -1,32 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { SongListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useSongListCount = (args: QueryHookArgs<SongListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getSongListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.songs.count(
serverId || '',
Object.keys(query).length === 0 ? undefined : query,
),
...options,
});
};

View file

@ -1,26 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { SongListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useSongList = (args: QueryHookArgs<SongListQuery>, imageSize?: number) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return controller.getSongList({
apiClientProps: { server, signal },
query: { ...query, imageSize },
});
},
queryKey: queryKeys.songs.list(server?.id || '', { ...query, imageSize }),
...options,
});
};

View file

@ -1,17 +1,18 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { useGenreList } from '/@/renderer/features/genres'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content'; import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header'; import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
import { useSongListCount } from '/@/renderer/features/songs/queries/song-list-count-query';
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store'; import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types'; import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
@ -46,10 +47,11 @@ const TrackListRoute = () => {
key: pageKey, key: pageKey,
}); });
const genreList = useGenreList({ const genreList = useQuery(
genresQueries.list({
options: { options: {
cacheTime: 1000 * 60 * 60,
enabled: !!genreId, enabled: !!genreId,
gcTime: 1000 * 60 * 60,
}, },
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
@ -57,7 +59,8 @@ const TrackListRoute = () => {
startIndex: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); }),
);
const genreTitle = useMemo(() => { const genreTitle = useMemo(() => {
if (!genreList.data) return ''; if (!genreList.data) return '';
@ -68,14 +71,15 @@ const TrackListRoute = () => {
return genre?.name; return genre?.name;
}, [genreId, genreList.data]); }, [genreId, genreList.data]);
const itemCountCheck = useSongListCount({ const itemCountCheck = useQuery(
songsQueries.listCount({
options: { options: {
cacheTime: 1000 * 60, gcTime: 1000 * 60,
staleTime: 1000 * 60,
}, },
query: songListFilter, query: songListFilter,
serverId: server?.id, serverId: server?.id,
}); }),
);
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data; const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;

View file

@ -1,25 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils';
import { TagQuery } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
export const useTagList = (args: QueryHookArgs<TagQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server && hasFeature(server, ServerFeature.TAGS),
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getTags({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.tags.list(server?.id || '', query.type),
staleTime: 1000 * 60,
...options,
});
};

View file

@ -1,23 +0,0 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { UserListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store';
export const useUserList = (args: QueryHookArgs<UserListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
return useQuery({
enabled: !!server,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
api.controller.getUserList({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.users.list(server?.id || '', query),
...options,
});
};

View file

@ -1,5 +1,6 @@
import type { import type {
DefaultOptions, DefaultOptions,
QueryOptions,
UseInfiniteQueryOptions, UseInfiniteQueryOptions,
UseMutationOptions, UseMutationOptions,
UseQueryOptions, UseQueryOptions,
@ -22,14 +23,11 @@ const queryConfig: DefaultOptions = {
retry: process.env.NODE_ENV === 'production', retry: process.env.NODE_ENV === 'production',
}, },
queries: { queries: {
cacheTime: 1000 * 60 * 3, gcTime: 1000 * 60 * 3,
onError: (err) => {
console.error('react query error:', err);
},
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: process.env.NODE_ENV === 'production', retry: process.env.NODE_ENV === 'production',
staleTime: 1000 * 5, staleTime: 1000 * 5,
useErrorBoundary: (error: any) => { throwOnError: (error: any) => {
return error?.response?.status >= 500; return error?.response?.status >= 500;
}, },
}, },
@ -40,23 +38,10 @@ export const queryClient = new QueryClient({
queryCache, queryCache,
}); });
export type InfiniteQueryOptions = { export type InfiniteQueryHookArgs<T> = {
cacheTime?: UseInfiniteQueryOptions['cacheTime']; options?: UseInfiniteQueryOptions;
enabled?: UseInfiniteQueryOptions['enabled']; query: T;
keepPreviousData?: UseInfiniteQueryOptions['keepPreviousData']; serverId: string | undefined;
meta?: UseInfiniteQueryOptions['meta'];
onError?: (err: any) => void;
onSettled?: any;
onSuccess?: any;
queryKey?: UseInfiniteQueryOptions['queryKey'];
refetchInterval?: number;
refetchIntervalInBackground?: UseInfiniteQueryOptions['refetchIntervalInBackground'];
refetchOnWindowFocus?: boolean;
retry?: UseInfiniteQueryOptions['retry'];
retryDelay?: UseInfiniteQueryOptions['retryDelay'];
staleTime?: UseInfiniteQueryOptions['staleTime'];
suspense?: UseInfiniteQueryOptions['suspense'];
useErrorBoundary?: boolean;
}; };
export type MutationHookArgs = { export type MutationHookArgs = {
@ -74,26 +59,34 @@ export type MutationOptions = {
}; };
export type QueryHookArgs<T> = { export type QueryHookArgs<T> = {
options?: QueryOptions; options?: UseQueryHookOptions;
query: T; query: T;
serverId: string | undefined; serverId: string;
}; };
export type QueryOptions = { type UseQueryHookOptions = {
cacheTime?: UseQueryOptions['cacheTime']; enabled?: boolean;
enabled?: UseQueryOptions['enabled']; gcTime?: QueryOptions['gcTime'];
keepPreviousData?: UseQueryOptions['keepPreviousData']; // initialData?: UseQueryOptions['initialData'];
// initialDataUpdatedAt?: UseQueryOptions['initialDataUpdatedAt'];
meta?: UseQueryOptions['meta']; meta?: UseQueryOptions['meta'];
onError?: (err: any) => void; networkMode?: UseQueryOptions['networkMode'];
onSettled?: any; notifyOnChangeProps?: UseQueryOptions['notifyOnChangeProps'];
onSuccess?: any; placeholderData?: (prev: any) => any;
// queryFn?: UseQueryOptions['queryFn'];
queryKey?: UseQueryOptions['queryKey']; queryKey?: UseQueryOptions['queryKey'];
queryKeyHashFn?: UseQueryOptions['queryKeyHashFn'];
refetchInterval?: number; refetchInterval?: number;
refetchIntervalInBackground?: UseQueryOptions['refetchIntervalInBackground']; refetchIntervalInBackground?: UseQueryOptions['refetchIntervalInBackground'];
refetchOnMount?: boolean;
refetchOnReconnect?: boolean;
refetchOnWindowFocus?: boolean; refetchOnWindowFocus?: boolean;
retry?: UseQueryOptions['retry']; retry?: UseQueryOptions['retry'];
retryDelay?: UseQueryOptions['retryDelay']; retryDelay?: UseQueryOptions['retryDelay'];
staleTime?: UseQueryOptions['staleTime']; retryOnMount?: UseQueryOptions['retryOnMount'];
suspense?: UseQueryOptions['suspense']; // select?: UseQueryOptions['select'];
useErrorBoundary?: boolean; staleTime?: number;
structuralSharing?: UseQueryOptions['structuralSharing'];
subscribed?: UseQueryOptions['subscribed'];
throwOnError?: boolean;
}; };

View file

@ -31,7 +31,6 @@ createRoot(document.getElementById('root')!).render(
persistOptions={{ persistOptions={{
buster: 'feishin', buster: 'feishin',
dehydrateOptions: { dehydrateOptions: {
dehydrateQueries: true,
shouldDehydrateQuery: (query) => { shouldDehydrateQuery: (query) => {
const isSuccess = query.state.status === 'success'; const isSuccess = query.state.status === 'success';
const isLyricsQueryKey = const isLyricsQueryKey =
@ -45,7 +44,7 @@ createRoot(document.getElementById('root')!).render(
hydrateOptions: { hydrateOptions: {
defaultOptions: { defaultOptions: {
queries: { queries: {
cacheTime: Infinity, gcTime: Infinity,
}, },
}, },
}, },

View file

@ -91,7 +91,8 @@ export const useAuthStore = createWithEqualityFn<AuthSlice>()(
export const useCurrentServerId = () => useAuthStore((state) => state.currentServer)?.id || ''; export const useCurrentServerId = () => useAuthStore((state) => state.currentServer)?.id || '';
export const useCurrentServer = () => useAuthStore((state) => state.currentServer); export const useCurrentServer = () =>
useAuthStore((state) => state.currentServer) as ServerListItem;
export const useServerList = () => useAuthStore((state) => state.serverList); export const useServerList = () => useAuthStore((state) => state.serverList);