This commit is contained in:
jeffvli
2025-07-29 19:07:58 -07:00
parent 98e8bda45d
commit a7430dae31
60 changed files with 583 additions and 155 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
'postcss-preset-mantine': {}, 'postcss-preset-mantine': {
mixins: {},
},
}, },
}; };
@@ -9,9 +9,7 @@ import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils';
import { albumListSortMap } from '/@/shared/types/domain/album-domain-types'; import { albumListSortMap } from '/@/shared/types/domain/album-domain-types';
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types'; import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types';
import { albumArtistListSortMap } from '/@/shared/types/domain/artist-domain-types'; import { albumArtistListSortMap } from '/@/shared/types/domain/artist-domain-types';
import { genreListSortMap } from '/@/shared/types/domain/genre-domain-types';
import { Played } from '/@/shared/types/domain/player-domain-types'; import { Played } from '/@/shared/types/domain/player-domain-types';
import { playlistListSortMap } from '/@/shared/types/domain/playlist-domain-types';
import { ServerFeature } from '/@/shared/types/domain/server-domain-types'; import { ServerFeature } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, sortOrderMap } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, sortOrderMap } from '/@/shared/types/domain/shared-domain-types';
import { Song, songListSortMap } from '/@/shared/types/domain/song-domain-types'; import { Song, songListSortMap } from '/@/shared/types/domain/song-domain-types';
@@ -11,9 +11,7 @@ import { albumListSortMap } from '/@/shared/types/domain/album-domain-types';
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types'; import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types';
import { albumArtistListSortMap } from '/@/shared/types/domain/artist-domain-types'; import { albumArtistListSortMap } from '/@/shared/types/domain/artist-domain-types';
import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types'; import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types';
import { genreListSortMap } from '/@/shared/types/domain/genre-domain-types';
import { import {
playlistListSortMap,
PlaylistSongListRequest, PlaylistSongListRequest,
PlaylistSongListResponse, PlaylistSongListResponse,
} from '/@/shared/types/domain/playlist-domain-types'; } from '/@/shared/types/domain/playlist-domain-types';
@@ -24,7 +22,6 @@ import {
} from '/@/shared/types/domain/server-domain-types'; } from '/@/shared/types/domain/server-domain-types';
import { sortOrderMap } from '/@/shared/types/domain/shared-domain-types'; import { sortOrderMap } from '/@/shared/types/domain/shared-domain-types';
import { Song, songListSortMap } from '/@/shared/types/domain/song-domain-types'; import { Song, songListSortMap } from '/@/shared/types/domain/song-domain-types';
import { userListSortMap } from '/@/shared/types/domain/user-domain-types';
const VERSION_INFO: VersionInfo = [ const VERSION_INFO: VersionInfo = [
['0.55.0', { [ServerFeature.BFR]: [1] }], ['0.55.0', { [ServerFeature.BFR]: [1] }],
@@ -17,8 +17,6 @@ import {
import { AlbumListSort, sortAlbumList } from '/@/shared/types/domain/album-domain-types'; import { AlbumListSort, sortAlbumList } from '/@/shared/types/domain/album-domain-types';
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types'; import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types';
import { sortAlbumArtistList } from '/@/shared/types/domain/artist-domain-types'; import { sortAlbumArtistList } from '/@/shared/types/domain/artist-domain-types';
import { GenreListSort } from '/@/shared/types/domain/genre-domain-types';
import { PlaylistListSort } from '/@/shared/types/domain/playlist-domain-types';
import { ServerFeatures } from '/@/shared/types/domain/server-domain-types'; import { ServerFeatures } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Song, sortSongList } from '/@/shared/types/domain/song-domain-types'; import { Song, sortSongList } from '/@/shared/types/domain/song-domain-types';
@@ -24,8 +24,8 @@ import { AppRoute } from '/@/renderer/router/routes';
import { PersistedTableColumn, useListStoreActions } from '/@/renderer/store'; import { PersistedTableColumn, useListStoreActions } from '/@/renderer/store';
import { ListKey, useListStoreByKey } from '/@/renderer/store/list.store'; import { ListKey, useListStoreByKey } from '/@/renderer/store/list.store';
import { import {
BasePaginatedResponse,
BasePaginatedQuery, BasePaginatedQuery,
BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types'; } from '/@/shared/types/adapter/api-controller-types';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
@@ -0,0 +1,45 @@
import { queryOptions, UseQueryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api/api-controller';
import { AlbumListRequest } from '/@/shared/types/domain/album-domain-types';
export const getAlbumListQueryKey = (serverId: string, request?: AlbumListRequest) => {
if (!request) {
return [serverId, 'albums'];
}
return [serverId, 'albums', request];
};
export const getInfiniteAlbumListQueryKey = (serverId: string, request?: AlbumListRequest) => {
if (!request) {
return [serverId, 'albums', 'infinite'];
}
return [serverId, 'albums', 'infinite', request];
};
export const getAlbumList = async (serverId: string, request: AlbumListRequest) => {
const [error, response] = await api.controller[serverId]!.album.getList!({
query: request.query,
});
if (error) {
throw new Error(error.message);
}
return response;
};
export const getAlbumListQuery = (
serverId: string,
request: AlbumListRequest,
options?: UseQueryOptions,
) => {
return queryOptions({
enabled: !!serverId,
queryFn: () => getAlbumList(serverId, request),
queryKey: getAlbumListQueryKey(serverId, request),
...options,
});
};
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumDetailQuery } from '/@/shared/types/domain/album-domain-types'; import { AlbumDetailQuery } from '/@/shared/types/domain/album-domain-types';
export const useAlbumDetail = (args: QueryHookArgs<AlbumDetailQuery>) => { export const useAlbumDetail = (args: RQueryHookArgs<AlbumDetailQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types'; import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types';
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => { export const useAlbumListCount = (args: RQueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
@@ -8,7 +8,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListResponse } from '/@/shared/types/domain/album-domain-types'; import { AlbumListQuery, AlbumListResponse } from '/@/shared/types/domain/album-domain-types';
export const useAlbumList = (args: QueryHookArgs<AlbumListQuery>) => { export const useAlbumList = (args: RQueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -33,7 +33,7 @@ export const useAlbumList = (args: QueryHookArgs<AlbumListQuery>) => {
}); });
}; };
export const useAlbumListInfinite = (args: QueryHookArgs<AlbumListQuery>) => { export const useAlbumListInfinite = (args: RQueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistDetail = (args: QueryHookArgs<AlbumArtistDetailQuery>) => { export const useAlbumArtistDetail = (args: RQueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => { export const useAlbumArtistListCount = (args: RQueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistList = (args: QueryHookArgs<AlbumArtistListQuery>) => { export const useAlbumArtistList = (args: RQueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistInfo = (args: QueryHookArgs<AlbumArtistDetailQuery>) => { export const useAlbumArtistInfo = (args: RQueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { ArtistListQuery } from '/@/shared/types/domain/artist-domain-types'; import { ArtistListQuery } from '/@/shared/types/domain/artist-domain-types';
export const useArtistListCount = (args: QueryHookArgs<ArtistListQuery>) => { export const useArtistListCount = (args: RQueryHookArgs<ArtistListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,10 +2,10 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
export const useRoles = (args: QueryHookArgs<object>) => { export const useRoles = (args: RQueryHookArgs<object>) => {
const { options, serverId } = args; const { options, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { TopSongListQuery } from '/@/shared/types/domain/song-domain-types'; import { TopSongListQuery } from '/@/shared/types/domain/song-domain-types';
export const useTopSongsList = (args: QueryHookArgs<TopSongListQuery>) => { export const useTopSongsList = (args: RQueryHookArgs<TopSongListQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { GenreListQuery } from '/@/shared/types/domain/genre-domain-types'; import { GenreListQuery } from '/@/shared/types/domain/genre-domain-types';
export const useGenreList = (args: QueryHookArgs<GenreListQuery>) => { export const useGenreList = (args: RQueryHookArgs<GenreListQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,12 +2,12 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListSort } from '/@/shared/types/domain/album-domain-types'; import { AlbumListQuery, AlbumListSort } from '/@/shared/types/domain/album-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
export const useRecentlyPlayed = (args: QueryHookArgs<Partial<AlbumListQuery>>) => { export const useRecentlyPlayed = (args: RQueryHookArgs<Partial<AlbumListQuery>>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -4,26 +4,21 @@ import { useTranslation } from 'react-i18next';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
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 { 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 { useAlbumList } from '/@/renderer/features/albums';
import { useRecentlyPlayed } from '/@/renderer/features/home/queries/recently-played-query'; import { useRecentlyPlayed } from '/@/renderer/features/home/queries/recently-played-query';
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared'; import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { AlbumInfiniteCarousel } from '/@/renderer/features/shared/components/infinite-album-carousel/infinite-album-carousel';
import { useSongList } from '/@/renderer/features/songs'; import { useSongList } from '/@/renderer/features/songs';
import { AppRoute } from '/@/renderer/router/routes';
import { import {
HomeItem, HomeItem,
useCurrentServer, useCurrentServer,
useGeneralSettings, useGeneralSettings,
useWindowSettings, useWindowSettings,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spinner } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title'; import { AlbumListSort, AlbumListSortOptions } from '/@/shared/types/domain/album-domain-types';
import { AlbumListSort } from '/@/shared/types/domain/album-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { SongListSort } from '/@/shared/types/domain/song-domain-types'; import { SongListSort } from '/@/shared/types/domain/song-domain-types';
@@ -40,15 +35,15 @@ const HomeRoute = () => {
const feature = useAlbumList({ const feature = useAlbumList({
options: { options: {
gcTime: 1000 * 60,
enabled: homeFeature, enabled: homeFeature,
gcTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, },
query: { query: {
limit: 20, limit: 20,
offset: 0,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
offset: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -63,9 +58,9 @@ const HomeRoute = () => {
}, },
query: { query: {
limit: itemsPerPage, limit: itemsPerPage,
offset: 0,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: ListSortOrder.ASC, sortOrder: ListSortOrder.ASC,
offset: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -76,9 +71,9 @@ const HomeRoute = () => {
}, },
query: { query: {
limit: itemsPerPage, limit: itemsPerPage,
offset: 0,
sortBy: AlbumListSort.RECENTLY_PLAYED, sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
offset: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -89,9 +84,9 @@ const HomeRoute = () => {
}, },
query: { query: {
limit: itemsPerPage, limit: itemsPerPage,
offset: 0,
sortBy: AlbumListSort.RECENTLY_ADDED, sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
offset: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -103,9 +98,9 @@ const HomeRoute = () => {
}, },
query: { query: {
limit: itemsPerPage, limit: itemsPerPage,
offset: 0,
sortBy: AlbumListSort.PLAY_COUNT, sortBy: AlbumListSort.PLAY_COUNT,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
offset: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -118,9 +113,9 @@ const HomeRoute = () => {
}, },
query: { query: {
limit: itemsPerPage, limit: itemsPerPage,
offset: 0,
sortBy: SongListSort.PLAY_COUNT, sortBy: SongListSort.PLAY_COUNT,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
offset: 0,
}, },
serverId: server?.id, serverId: server?.id,
}, },
@@ -253,7 +248,15 @@ const HomeRoute = () => {
px="2rem" px="2rem"
> >
{homeFeature && <FeatureCarousel data={featureItemsWithImage} />} {homeFeature && <FeatureCarousel data={featureItemsWithImage} />}
{sortedCarousel.map((carousel) => (
<AlbumInfiniteCarousel
serverId={server?.id ?? ''}
sortBy={AlbumListSortOptions.NAME}
sortOrder={ListSortOrder.ASC}
title={t('page.home.explore', { postProcess: 'sentenceCase' })}
/>
{/* {sortedCarousel.map((carousel) => (
<MemoizedSwiperGridCarousel <MemoizedSwiperGridCarousel
cardRows={[ cardRows={[
{ {
@@ -317,7 +320,7 @@ const HomeRoute = () => {
}} }}
uniqueId={carousel.uniqueId} uniqueId={carousel.uniqueId}
/> />
))} ))} */}
</Stack> </Stack>
</NativeScrollArea> </NativeScrollArea>
</AnimatedPage> </AnimatedPage>
@@ -3,7 +3,7 @@ import isElectron from 'is-electron';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById, useLyricsSettings } from '/@/renderer/store'; import { useServerById, useLyricsSettings } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils'; import { hasFeature } from '/@/shared/api/utils';
import { import {
@@ -61,7 +61,7 @@ const formatLyrics = (lyrics: string) => {
}; };
export const useServerLyrics = ( export const useServerLyrics = (
args: QueryHookArgs<LyricsQuery>, args: RQueryHookArgs<LyricsQuery>,
): UseQueryResult<null | string> => { ): UseQueryResult<null | string> => {
const { query, serverId } = args; const { query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -81,7 +81,7 @@ export const useServerLyrics = (
}; };
export const useSongLyricsBySong = ( export const useSongLyricsBySong = (
args: QueryHookArgs<LyricsQuery>, args: RQueryHookArgs<LyricsQuery>,
song: QueueSong | undefined, song: QueueSong | undefined,
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => { ): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
const { query } = args; const { query } = args;
@@ -170,7 +170,7 @@ export const useSongLyricsBySong = (
}; };
export const useSongLyricsByRemoteId = ( export const useSongLyricsByRemoteId = (
args: QueryHookArgs<Partial<LyricGetQuery>>, args: RQueryHookArgs<Partial<LyricGetQuery>>,
): UseQueryResult<null | string> => { ): UseQueryResult<null | string> => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { query, serverId } = args; const { query, serverId } = args;
@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { import {
InternetProviderLyricSearchResponse, InternetProviderLyricSearchResponse,
LyricSearchQuery, LyricSearchQuery,
@@ -11,7 +11,7 @@ import {
const lyricsIpc = isElectron() ? window.api.lyrics : null; const lyricsIpc = isElectron() ? window.api.lyrics : null;
export const useLyricSearch = (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => { export const useLyricSearch = (args: Omit<RQueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
const { options, query } = args; const { options, query } = args;
return useQuery<Record<LyricSource, InternetProviderLyricSearchResponse[]>>({ return useQuery<Record<LyricSource, InternetProviderLyricSearchResponse[]>>({
@@ -19,12 +19,12 @@ import { Icon } from '/@/shared/components/icon/icon';
import { NumberInput } from '/@/shared/components/number-input/number-input'; import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { GenreListResponse, GenreListSort } from '/@/shared/types/domain/genre-domain-types'; import { GenreListResponse } from '/@/shared/types/domain/genre-domain-types';
import { Played } from '/@/shared/types/domain/player-domain-types'; import { Played } from '/@/shared/types/domain/player-domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { RandomSongListQuery } from '/@/shared/types/domain/song-domain-types'; import { RandomSongListQuery } from '/@/shared/types/domain/song-domain-types';
import { Play, PlayQueueAddOptions } from '/@/shared/types/types'; import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
interface ShuffleAllSlice extends RandomSongListQuery { interface ShuffleAllSlice extends RandomSongListQuery {
actions: { actions: {
@@ -256,9 +256,9 @@ export const openShuffleAllModal = async (
signal, signal,
}, },
query: { query: {
offset: 0,
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: ListSortOrder.ASC,
offset: 0,
}, },
}), }),
queryKey: queryKeys.genres.list(server?.id), queryKey: queryKeys.genres.list(server?.id),
@@ -2,12 +2,12 @@ import { useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { MutationOptions } from '/@/renderer/lib/react-query'; import { RMutationOptions } from '/@/renderer/lib/react-query';
import { useServerById, useIncrementQueuePlayCount } from '/@/renderer/store'; import { useServerById, useIncrementQueuePlayCount } from '/@/renderer/store';
import { usePlayEvent } from '/@/renderer/store/event.store'; import { usePlayEvent } from '/@/renderer/store/event.store';
import { ScrobbleRequest, ScrobbleResponse } from '/@/shared/types/domain/user-domain-types'; import { ScrobbleRequest, ScrobbleResponse } from '/@/shared/types/domain/user-domain-types';
export const useSendScrobble = (options?: MutationOptions) => { export const useSendScrobble = (options?: RMutationOptions) => {
const incrementPlayCount = useIncrementQueuePlayCount(); const incrementPlayCount = useIncrementQueuePlayCount();
const sendPlayEvent = usePlayEvent(); const sendPlayEvent = usePlayEvent();
@@ -16,9 +16,9 @@ import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { PlaylistListSort } from '/@/shared/types/domain/playlist-domain-types'; import { PlaylistListSortOptions } from '/@/shared/types/domain/playlist-domain-types';
import { SongListQuery, SongListSort } from '/@/shared/types/domain/song-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { SongListQuery, SongListSort } from '/@/shared/types/domain/song-domain-types';
export const AddToPlaylistContextModal = ({ export const AddToPlaylistContextModal = ({
id, id,
@@ -44,9 +44,9 @@ export const AddToPlaylistContextModal = ({
smart: false, smart: false,
}, },
}, },
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
offset: 0, offset: 0,
sortBy: PlaylistListSortOptions.NAME,
sortOrder: ListSortOrder.ASC,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -70,9 +70,9 @@ export const AddToPlaylistContextModal = ({
const getSongsByAlbum = async (albumId: string) => { const getSongsByAlbum = async (albumId: string) => {
const query: SongListQuery = { const query: SongListQuery = {
albumIds: [albumId], albumIds: [albumId],
offset: 0,
sortBy: SongListSort.ALBUM, sortBy: SongListSort.ALBUM,
sortOrder: ListSortOrder.ASC, sortOrder: ListSortOrder.ASC,
offset: 0,
}; };
const queryKey = queryKeys.songs.list(server?.id || '', query); const queryKey = queryKeys.songs.list(server?.id || '', query);
@@ -88,9 +88,9 @@ export const AddToPlaylistContextModal = ({
const getSongsByArtist = async (artistId: string) => { const getSongsByArtist = async (artistId: string) => {
const query: SongListQuery = { const query: SongListQuery = {
artistIds: [artistId], artistIds: [artistId],
offset: 0,
sortBy: SongListSort.ARTIST, sortBy: SongListSort.ARTIST,
sortOrder: ListSortOrder.ASC, sortOrder: ListSortOrder.ASC,
offset: 0,
}; };
const queryKey = queryKeys.songs.list(server?.id || '', query); const queryKey = queryKeys.songs.list(server?.id || '', query);
@@ -32,10 +32,9 @@ import { Icon } from '/@/shared/components/icon/icon';
import { NumberInput } from '/@/shared/components/number-input/number-input'; import { NumberInput } from '/@/shared/components/number-input/number-input';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { PlaylistListSort } from '/@/shared/types/domain/playlist-domain-types'; import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { SongListSort } from '/@/shared/types/domain/song-domain-types'; import { SongListSort } from '/@/shared/types/domain/song-domain-types';
import { QueryBuilderGroup, QueryBuilderRule } from '/@/shared/types/types'; import { QueryBuilderGroup, QueryBuilderRule } from '/@/shared/types/types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
type AddArgs = { type AddArgs = {
groupIndex: number[]; groupIndex: number[];
@@ -111,7 +110,7 @@ export const PlaylistQueryBuilder = forwardRef(
); );
const { data: playlists } = usePlaylistList({ const { data: playlists } = usePlaylistList({
query: { sortBy: PlaylistListSort.NAME, sortOrder: ListSortOrder.ASC, offset: 0 }, query: { offset: 0, sortBy: PlaylistListSort.NAME, sortOrder: ListSortOrder.ASC },
serverId: server?.id, serverId: server?.id,
}); });
@@ -3,14 +3,14 @@ import { AxiosError } from 'axios';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { import {
AddToPlaylistArgs, AddToPlaylistArgs,
AddToPlaylistResponse, AddToPlaylistResponse,
} from '/@/shared/types/domain/playlist-domain-types'; } from '/@/shared/types/domain/playlist-domain-types';
export const useAddToPlaylist = (args: MutationHookArgs) => { export const useAddToPlaylist = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -3,11 +3,11 @@ import { AxiosError } from 'axios';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { CreatePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types'; import { CreatePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useCreatePlaylist = (args: MutationHookArgs) => { export const useCreatePlaylist = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -3,11 +3,11 @@ import { AxiosError } from 'axios';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById, useCurrentServer } from '/@/renderer/store'; import { useServerById, useCurrentServer } from '/@/renderer/store';
import { DeletePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types'; import { DeletePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useDeletePlaylist = (args: MutationHookArgs) => { export const useDeletePlaylist = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const server = useCurrentServer(); const server = useCurrentServer();
@@ -3,11 +3,11 @@ import { AxiosError, AxiosError } from 'axios';
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 { MutationOptions } from '/@/renderer/lib/react-query'; import { RMutationOptions } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { RemoveFromPlaylistResponse } from '/@/shared/types/domain/playlist-domain-types'; import { RemoveFromPlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useRemoveFromPlaylist = (options?: MutationOptions) => { export const useRemoveFromPlaylist = (options?: RMutationOptions) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation< return useMutation<
@@ -3,11 +3,11 @@ import { AxiosError } from 'axios';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { UpdatePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types'; import { UpdatePlaylistResponse } from '/@/shared/types/domain/playlist-domain-types';
export const useUpdatePlaylist = (args: MutationHookArgs) => { export const useUpdatePlaylist = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { PlaylistDetailQuery } from '/@/shared/types/domain/playlist-domain-types'; import { PlaylistDetailQuery } from '/@/shared/types/domain/playlist-domain-types';
export const usePlaylistDetail = (args: QueryHookArgs<PlaylistDetailQuery>) => { export const usePlaylistDetail = (args: RQueryHookArgs<PlaylistDetailQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryOptions } from '/@/renderer/lib/react-query'; import type { RQueryOptions } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -8,7 +8,7 @@ import { useServerById } from '/@/renderer/store';
import { PlaylistListQuery } from '/@/shared/types/domain/playlist-domain-types'; import { PlaylistListQuery } from '/@/shared/types/domain/playlist-domain-types';
export const usePlaylistList = (args: { export const usePlaylistList = (args: {
options?: QueryOptions; options?: RQueryOptions;
query: PlaylistListQuery; query: PlaylistListQuery;
serverId?: string; serverId?: string;
}) => { }) => {
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { PlaylistSongListQuery } from '/@/shared/types/domain/playlist-domain-types'; import { PlaylistSongListQuery } from '/@/shared/types/domain/playlist-domain-types';
export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>) => { export const usePlaylistSongList = (args: RQueryHookArgs<PlaylistSongListQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { SearchQuery } from '/@/shared/types/domain/search-domain-types'; import { SearchQuery } from '/@/shared/types/domain/search-domain-types';
export const useSearch = (args: QueryHookArgs<SearchQuery>) => { export const useSearch = (args: RQueryHookArgs<SearchQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -0,0 +1,100 @@
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
import { nanoid } from 'nanoid';
import { memo, useCallback, useMemo } from 'react';
import { PosterCard } from '/@/renderer/components/card/poster-card';
import {
getAlbumList,
getInfiniteAlbumListQueryKey,
} from '/@/renderer/features/albums/api/queries/get-album-list-query';
import { GridCarousel } from '/@/shared/components/grid-carousel/grid-carousel';
import { AlbumListResponse, AlbumListSortOptions } from '/@/shared/types/domain/album-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
interface AlbumCarouselProps {
rowCount?: number;
serverId: string;
sortBy: AlbumListSortOptions;
sortOrder: ListSortOrder;
title: string;
}
const MemoizedAlbumCard = memo(PosterCard);
export function AlbumInfiniteCarousel(props: AlbumCarouselProps) {
const { rowCount = 1, serverId, sortBy, sortOrder, title } = props;
const { data: albums, fetchNextPage } = useInfiniteAlbumList(serverId, sortBy, sortOrder, 20);
const cards = useMemo(
() =>
albums.pages.flatMap((page) => {
const loadedCards = page.items.map((album) => ({
content: <MemoizedAlbumCard controls={{}} data={album} uniqueId={album.id} />,
id: album.id,
}));
if (page.items.length === 20) {
return loadedCards;
}
return [
...loadedCards,
...Array.from({ length: 20 - page.items.length }).map(() => {
const id = nanoid();
return {
content: <MemoizedAlbumCard controls={{}} />,
id,
};
}),
];
}),
[albums.pages],
);
const handleNextPage = useCallback(() => {}, []);
const handlePrevPage = useCallback(() => {}, []);
if (albums.pages[0]?.items.length === 0) {
return null;
}
return (
<GridCarousel
cards={cards}
loadNextPage={fetchNextPage}
onNextPage={handleNextPage}
onPrevPage={handlePrevPage}
rowCount={rowCount}
title={title}
/>
);
}
function useInfiniteAlbumList(
serverId: string,
sortBy: AlbumListSortOptions,
sortOrder: ListSortOrder,
limit: number,
) {
const query = useSuspenseInfiniteQuery<AlbumListResponse>({
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
if (lastPage.items.length < limit) {
return undefined;
}
const nextPageParam = Number(lastPageParam) + limit;
return String(nextPageParam);
},
initialPageParam: 0,
queryFn: ({ pageParam }) => {
return getAlbumList(serverId, {
query: { limit: limit, offset: Number(pageParam), sortBy, sortOrder },
});
},
queryKey: getInfiniteAlbumListQueryKey(serverId),
});
return query;
}
@@ -4,7 +4,7 @@ import isElectron from 'is-electron';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store'; import { useServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store'; import { useFavoriteEvent } from '/@/renderer/store/event.store';
import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types'; import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types';
@@ -14,7 +14,7 @@ import { FavoriteResponse } from '/@/shared/types/domain/user-domain-types';
const remote = isElectron() ? window.api.remote : null; const remote = isElectron() ? window.api.remote : null;
export const useCreateFavorite = (args: MutationHookArgs) => { export const useCreateFavorite = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById(); const setAlbumListData = useSetAlbumListItemDataById();
@@ -4,7 +4,7 @@ import isElectron from 'is-electron';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store'; import { useServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store'; import { useFavoriteEvent } from '/@/renderer/store/event.store';
import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types'; import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types';
@@ -14,7 +14,7 @@ import { FavoriteResponse } from '/@/shared/types/domain/user-domain-types';
const remote = isElectron() ? window.api.remote : null; const remote = isElectron() ? window.api.remote : null;
export const useDeleteFavorite = (args: MutationHookArgs) => { export const useDeleteFavorite = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById(); const setAlbumListData = useSetAlbumListItemDataById();
@@ -4,7 +4,7 @@ import isElectron from 'is-electron';
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 { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById, useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store'; import { useServerById, useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store';
import { useRatingEvent } from '/@/renderer/store/event.store'; import { useRatingEvent } from '/@/renderer/store/event.store';
import { Album, AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types'; import { Album, AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types';
@@ -14,7 +14,7 @@ import { RatingResponse, SetRatingRequest } from '/@/shared/types/domain/user-do
const remote = isElectron() ? window.api.remote : null; const remote = isElectron() ? window.api.remote : null;
export const useSetRating = (args: MutationHookArgs) => { export const useSetRating = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById(); const setAlbumListData = useSetAlbumListItemDataById();
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { ServerMusicFolderListQuery } from '/@/shared/types/domain/server-domain-types'; import { ServerMusicFolderListQuery } from '/@/shared/types/domain/server-domain-types';
export const useMusicFolders = (args: QueryHookArgs<ServerMusicFolderListQuery>) => { export const useMusicFolders = (args: RQueryHookArgs<ServerMusicFolderListQuery>) => {
const { options, serverId } = args || {}; const { options, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,12 +2,12 @@ import { useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { RMutationHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { AnyLibraryItems } from '/@/shared/types/domain/shared-domain-types'; import { AnyLibraryItems } from '/@/shared/types/domain/shared-domain-types';
import { ShareItemRequest, ShareItemResponse } from '/@/shared/types/domain/user-domain-types'; import { ShareItemRequest, ShareItemResponse } from '/@/shared/types/domain/user-domain-types';
export const useShareItem = (args: MutationHookArgs) => { export const useShareItem = (args: RMutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
return useMutation< return useMutation<
@@ -17,7 +17,7 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { ButtonProps } from '/@/shared/components/button/button'; import { ButtonProps } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Playlist, PlaylistListSort } from '/@/shared/types/domain/playlist-domain-types'; import { Playlist, PlaylistListSortOptions } from '/@/shared/types/domain/playlist-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
@@ -140,9 +140,9 @@ export const SidebarPlaylistList = () => {
const playlistsQuery = usePlaylistList({ const playlistsQuery = usePlaylistList({
query: { query: {
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
offset: 0, offset: 0,
sortBy: PlaylistListSortOptions.NAME,
sortOrder: ListSortOrder.ASC,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -256,9 +256,9 @@ export const SidebarSharedPlaylistList = () => {
const playlistsQuery = usePlaylistList({ const playlistsQuery = usePlaylistList({
query: { query: {
sortBy: PlaylistListSort.NAME,
sortOrder: ListSortOrder.ASC,
offset: 0, offset: 0,
sortBy: PlaylistListSortOptions.NAME,
sortOrder: ListSortOrder.ASC,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { SimilarSongsQuery } from '/@/shared/types/domain/song-domain-types'; import { SimilarSongsQuery } from '/@/shared/types/domain/song-domain-types';
export const useSimilarSongs = (args: QueryHookArgs<SimilarSongsQuery>) => { export const useSimilarSongs = (args: RQueryHookArgs<SimilarSongsQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { SongListQuery } from '/@/shared/types/domain/song-domain-types'; import { SongListQuery } from '/@/shared/types/domain/song-domain-types';
export const useSongListCount = (args: QueryHookArgs<SongListQuery>) => { export const useSongListCount = (args: RQueryHookArgs<SongListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { SongListQuery } from '/@/shared/types/domain/song-domain-types'; import { SongListQuery } from '/@/shared/types/domain/song-domain-types';
export const useSongList = (args: QueryHookArgs<SongListQuery>, imageSize?: number) => { export const useSongList = (args: RQueryHookArgs<SongListQuery>, imageSize?: number) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -2,13 +2,13 @@ import { useQuery } from '@tanstack/react-query';
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 { QueryHookArgs } from '/@/renderer/lib/react-query'; import { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { hasFeature } from '/@/shared/api/utils'; import { hasFeature } from '/@/shared/api/utils';
import { ServerFeature } from '/@/shared/types/domain/server-domain-types'; import { ServerFeature } from '/@/shared/types/domain/server-domain-types';
import { TagQuery } from '/@/shared/types/domain/tag-domain-types'; import { TagQuery } from '/@/shared/types/domain/tag-domain-types';
export const useTagList = (args: QueryHookArgs<TagQuery>) => { export const useTagList = (args: RQueryHookArgs<TagQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
@@ -1,4 +1,4 @@
import type { QueryHookArgs } from '/@/renderer/lib/react-query'; import type { RQueryHookArgs } from '/@/renderer/lib/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { useServerById } from '/@/renderer/store';
import { UserListQuery } from '/@/shared/types/domain/user-domain-types'; import { UserListQuery } from '/@/shared/types/domain/user-domain-types';
export const useUserList = (args: QueryHookArgs<UserListQuery>) => { export const useUserList = (args: RQueryHookArgs<UserListQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = useServerById(serverId);
+6 -10
View File
@@ -41,8 +41,6 @@ export type InfiniteQueryOptions = {
gcTime?: UseInfiniteQueryOptions['gcTime']; gcTime?: UseInfiniteQueryOptions['gcTime'];
meta?: UseInfiniteQueryOptions['meta']; meta?: UseInfiniteQueryOptions['meta'];
onError?: (err: any) => void; onError?: (err: any) => void;
onSettled?: any;
onSuccess?: any;
queryKey?: UseInfiniteQueryOptions['queryKey']; queryKey?: UseInfiniteQueryOptions['queryKey'];
refetchInterval?: number; refetchInterval?: number;
refetchIntervalInBackground?: UseInfiniteQueryOptions['refetchIntervalInBackground']; refetchIntervalInBackground?: UseInfiniteQueryOptions['refetchIntervalInBackground'];
@@ -53,11 +51,11 @@ export type InfiniteQueryOptions = {
useErrorBoundary?: boolean; useErrorBoundary?: boolean;
}; };
export type MutationHookArgs = { export type RMutationHookArgs = {
options?: MutationOptions; options?: RMutationOptions;
}; };
export type MutationOptions = { export type RMutationOptions = {
mutationKey: UseMutationOptions['mutationKey']; mutationKey: UseMutationOptions['mutationKey'];
onError?: (err: any) => void; onError?: (err: any) => void;
onSettled?: any; onSettled?: any;
@@ -67,19 +65,17 @@ export type MutationOptions = {
useErrorBoundary?: boolean; useErrorBoundary?: boolean;
}; };
export type QueryHookArgs<T> = { export type RQueryHookArgs<T> = {
options?: QueryOptions; options?: RQueryOptions;
query: T; query: T;
serverId: string | undefined; serverId: string | undefined;
}; };
export type QueryOptions = { export type RQueryOptions = {
enabled?: UseQueryOptions['enabled']; enabled?: UseQueryOptions['enabled'];
gcTime?: UseQueryOptions['gcTime']; gcTime?: UseQueryOptions['gcTime'];
meta?: UseQueryOptions['meta']; meta?: UseQueryOptions['meta'];
onError?: (err: any) => void; onError?: (err: any) => void;
onSettled?: any;
onSuccess?: any;
queryKey?: UseQueryOptions['queryKey']; queryKey?: UseQueryOptions['queryKey'];
refetchInterval?: number; refetchInterval?: number;
refetchIntervalInBackground?: UseQueryOptions['refetchIntervalInBackground']; refetchIntervalInBackground?: UseQueryOptions['refetchIntervalInBackground'];
+15 -15
View File
@@ -6,28 +6,28 @@ import { createWithEqualityFn } from 'zustand/traditional';
import { DataTableProps, PersistedTableColumn } from '/@/renderer/store/settings.store'; import { DataTableProps, PersistedTableColumn } from '/@/renderer/store/settings.store';
import { mergeOverridingColumns } from '/@/renderer/store/utils'; import { mergeOverridingColumns } from '/@/renderer/store/utils';
import { AlbumListRequest, AlbumListSort } from '/@/shared/types/domain/album-domain-types'; import { AlbumListRequest, AlbumListSort } from '/@/shared/types/domain/album-domain-types';
import { import { AlbumArtistListSort, ArtistListRequest } from '/@/shared/types/domain/artist-domain-types';
AlbumArtistListRequest, import { GenreListRequest, GenreListSortOptions } from '/@/shared/types/domain/genre-domain-types';
AlbumArtistListSort,
ArtistListRequest,
} from '/@/shared/types/domain/artist-domain-types';
import { GenreListRequest, GenreListSort } from '/@/shared/types/domain/genre-domain-types';
import { import {
PlaylistListRequest, PlaylistListRequest,
PlaylistListSort, PlaylistListSortOptions,
} from '/@/shared/types/domain/playlist-domain-types'; } from '/@/shared/types/domain/playlist-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { SongListRequest, SongListSort } from '/@/shared/types/domain/song-domain-types'; import {
SongListRequest,
SongListSort,
SongListSortOptions,
} from '/@/shared/types/domain/song-domain-types';
import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types'; import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types';
export const generatePageKey = (page: string, id?: string) => { export const generatePageKey = (page: string, id?: string) => {
return id ? `${page}_${id}` : page; return id ? `${page}_${id}` : page;
}; };
export type AlbumArtistListFilter = Omit<AlbumArtistListRequest['query'], 'limit' | 'startIndex'>; export type AlbumArtistListFilter = Omit<ArtistListRequest['query'], 'limit' | 'offset'>;
export type AlbumListFilter = Omit<AlbumListRequest['query'], 'limit' | 'startIndex'>; export type AlbumListFilter = Omit<AlbumListRequest['query'], 'limit' | 'offset'>;
export type ArtistListFilter = Omit<ArtistListRequest['query'], 'limit' | 'startIndex'>; export type ArtistListFilter = Omit<ArtistListRequest['query'], 'limit' | 'offset'>;
export type GenreListFilter = Omit<GenreListRequest['query'], 'limit' | 'startIndex'>; export type GenreListFilter = Omit<GenreListRequest['query'], 'limit' | 'offset'>;
export type ListDeterministicArgs = { key: ListKey }; export type ListDeterministicArgs = { key: ListKey };
export type ListGridProps = { export type ListGridProps = {
itemGap?: number; itemGap?: number;
@@ -544,7 +544,7 @@ export const useListStore = createWithEqualityFn<ListSlice>()(
genre: { genre: {
display: ListDisplayType.TABLE, display: ListDisplayType.TABLE,
filter: { filter: {
sortBy: GenreListSort.NAME, sortBy: GenreListSortOptions.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: ListSortOrder.ASC,
}, },
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
@@ -573,7 +573,7 @@ export const useListStore = createWithEqualityFn<ListSlice>()(
playlist: { playlist: {
display: ListDisplayType.GRID, display: ListDisplayType.GRID,
filter: { filter: {
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSortOptions.NAME,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
}, },
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
@@ -606,7 +606,7 @@ export const useListStore = createWithEqualityFn<ListSlice>()(
song: { song: {
display: ListDisplayType.TABLE, display: ListDisplayType.TABLE,
filter: { filter: {
sortBy: SongListSort.RECENTLY_ADDED, sortBy: SongListSortOptions.RECENTLY_ADDED,
sortOrder: ListSortOrder.DESC, sortOrder: ListSortOrder.DESC,
}, },
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
+7 -4
View File
@@ -5,9 +5,12 @@ import { createWithEqualityFn } from 'zustand/traditional';
import { PlaylistListFilter, SongListFilter } from '/@/renderer/store/list.store'; import { PlaylistListFilter, SongListFilter } from '/@/renderer/store/list.store';
import { DataTableProps } from '/@/renderer/store/settings.store'; import { DataTableProps } from '/@/renderer/store/settings.store';
import { mergeOverridingColumns } from '/@/renderer/store/utils'; import { mergeOverridingColumns } from '/@/renderer/store/utils';
import { PlaylistListSort, PlaylistListSort } from '/@/shared/types/domain/playlist-domain-types'; import {
import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types'; PlaylistListSortOptions,
PlaylistListSortOptions,
} from '/@/shared/types/domain/playlist-domain-types';
import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types';
export interface PlaylistSlice extends PlaylistState { export interface PlaylistSlice extends PlaylistState {
actions: { actions: {
@@ -155,8 +158,8 @@ export const usePlaylistStore = createWithEqualityFn<PlaylistSlice>()(
list: { list: {
display: ListDisplayType.TABLE, display: ListDisplayType.TABLE,
filter: { filter: {
musicFolderId: undefined, offset: 0,
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSortOptions.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: ListSortOrder.ASC,
}, },
table: { table: {
+13 -1
View File
@@ -55,7 +55,7 @@ export const createApiClient = (
}; };
const authMiddleware: (server: ServerListItem) => Middleware = (server: ServerListItem) => ({ const authMiddleware: (server: ServerListItem) => Middleware = (server: ServerListItem) => ({
onRequest: async ({ params }) => { onRequest: async ({ params, request }) => {
const credential = deserializeCredential(server.credential); const credential = deserializeCredential(server.credential);
if (params.query) { if (params.query) {
@@ -67,6 +67,18 @@ const authMiddleware: (server: ServerListItem) => Middleware = (server: ServerLi
params.query[key] = value; params.query[key] = value;
} }
} }
const stringifiedParams = qs.stringify(params.query, { arrayFormat: 'repeat' });
const url = new URL(request.url);
url.search = stringifiedParams;
return new Request(url.toString(), {
body: request.body,
headers: request.headers,
method: request.method,
signal: request.signal,
});
}, },
}); });
+1 -2
View File
@@ -2,7 +2,6 @@ import { AxiosHeaders } from 'axios';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { orderBy, shuffle } from 'lodash'; import { orderBy, shuffle } from 'lodash';
import { stringify } from 'querystring';
import semverCoerce from 'semver/functions/coerce'; import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte'; import semverGte from 'semver/functions/gte';
import { z } from 'zod'; import { z } from 'zod';
@@ -388,7 +387,7 @@ function getListCountKey(options: {
serverId: string; serverId: string;
type: LibraryItem | string; type: LibraryItem | string;
}) { }) {
const hash = stringify(options.query as Record<string, boolean | null | number | string>); const hash = JSON.stringify(options.query as Record<string, boolean | null | number | string>);
return `${options.serverId}::${options.type}::${hash}`; return `${options.serverId}::${options.type}::${hash}`;
} }
@@ -0,0 +1,54 @@
.grid-carousel {
display: flex;
flex-direction: column;
gap: base.$gap-md;
width: 100%;
margin: 0 auto;
container-name: grid-carousel;
container-type: inline-size;
}
.navigation {
display: flex;
align-items: center;
justify-content: space-between;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(20%, 1fr));
gap: 1rem;
height: calc(var(--row-count) * (100cqw / 2 + 3rem));
margin-bottom: 1rem;
overflow: hidden;
/* @mixin larger-than-sm {
grid-template-columns: repeat(4, minmax(20%, 1fr));
height: calc(var(--row-count) * (100cqw / 4 + 3rem));
}
@mixin larger-than-md {
grid-template-columns: repeat(5, minmax(15%, 1fr));
height: calc(var(--row-count) * (100cqw / 5 + 3rem));
}
@mixin larger-than-lg {
grid-template-columns: repeat(6, minmax(15%, 1fr));
height: calc(var(--row-count) * (100cqw / 6 + 3rem));
}
@mixin larger-than-xl {
grid-template-columns: repeat(7, minmax(10%, 1fr));
height: calc(var(--row-count) * (100cqw / 7 + 3rem));
}
@mixin larger-than-2xl {
grid-template-columns: repeat(8, minmax(5%, 1fr));
height: calc(var(--row-count) * (100cqw / 8 + 3rem));
}
@mixin larger-than-3xl {
grid-template-columns: repeat(9, minmax(5%, 1fr));
height: calc(var(--row-count) * (100cqw / 9 + 3rem));
} */
}
@@ -0,0 +1,162 @@
import { AnimatePresence, motion, Variants } from 'motion/react';
import { memo, ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import styles from './grid-carousel.module.css';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import { useContainerBreakpoints } from '/@/shared/hooks/use-container-breakpoints';
interface Card {
content: ReactNode;
id: string;
}
interface GridCarouselProps {
cards: Card[];
loadNextPage?: () => void;
onNextPage: (page: number) => void;
onPrevPage: (page: number) => void;
rowCount?: number;
title?: string;
}
const MemoizedCard = memo(({ content }: { content: ReactNode }) => (
<div className={styles.card}>{content}</div>
));
MemoizedCard.displayName = 'MemoizedCard';
const pageVariants: Variants = {
animate: { opacity: 1, transition: { duration: 0.3, ease: 'easeOut' }, x: 0 },
exit: (custom: { isNext: boolean }) => ({
opacity: 0,
transition: { duration: 0.3, ease: 'easeIn' },
x: custom.isNext ? -100 : 100,
}),
initial: (custom: { isNext: boolean }) => ({ opacity: 0, x: custom.isNext ? 100 : -100 }),
};
export function GridCarousel(props: GridCarouselProps) {
const { cards, loadNextPage, onNextPage, onPrevPage, rowCount = 1, title } = props;
const { breakpoints, ref: containerRef } = useContainerBreakpoints();
const [currentPage, setCurrentPage] = useState({
isNext: false,
page: 0,
});
const handlePrevPage = useCallback(() => {
setCurrentPage((prev) => ({
isNext: false,
page: prev.page > 0 ? prev.page - 1 : 0,
}));
onPrevPage(currentPage.page);
}, [currentPage, onPrevPage]);
const handleNextPage = useCallback(() => {
setCurrentPage((prev) => ({
isNext: true,
page: prev.page + 1,
}));
onNextPage(currentPage.page);
}, [currentPage, onNextPage]);
const cardsToShow = getCardsToShow(breakpoints);
const visibleCards = useMemo(() => {
return cards.slice(
currentPage.page * cardsToShow * rowCount,
(currentPage.page + 1) * cardsToShow * rowCount,
);
}, [cards, currentPage, cardsToShow, rowCount]);
const shouldLoadNextPage = visibleCards.length < cardsToShow * rowCount;
useEffect(() => {
if (shouldLoadNextPage) {
loadNextPage?.();
}
}, [loadNextPage, shouldLoadNextPage]);
const isPrevDisabled = currentPage.page === 0;
const isNextDisabled = visibleCards.length < cardsToShow * rowCount;
return (
<motion.div className={styles.gridCarousel} ref={containerRef}>
<div className={styles.navigation}>
<TextTitle order={1} size="lg">
{title}
</TextTitle>
<Group gap="xs" justify="end">
<ActionIcon
disabled={isPrevDisabled}
icon="arrowLeftS"
onClick={handlePrevPage}
size="lg"
variant="default"
/>
<ActionIcon
disabled={isNextDisabled}
icon="arrowRightS"
onClick={handleNextPage}
size="lg"
variant="default"
/>
</Group>
</div>
<AnimatePresence custom={currentPage} initial={false} mode="wait">
<motion.div
animate="animate"
className={styles.grid}
custom={currentPage}
exit="exit"
initial="initial"
key={currentPage.page}
style={{ '--row-count': rowCount } as React.CSSProperties}
variants={pageVariants}
>
{visibleCards.map((card) => (
<MemoizedCard content={card.content} key={card.id} />
))}
</motion.div>
</AnimatePresence>
</motion.div>
);
}
function getCardsToShow(breakpoints: {
isLargerThan2xl: boolean;
isLargerThan3xl: boolean;
isLargerThanLg: boolean;
isLargerThanMd: boolean;
isLargerThanSm: boolean;
isLargerThanXl: boolean;
}) {
if (breakpoints.isLargerThan3xl) {
return 10;
}
if (breakpoints.isLargerThan2xl) {
return 8;
}
if (breakpoints.isLargerThanXl) {
return 7;
}
if (breakpoints.isLargerThanLg) {
return 6;
}
if (breakpoints.isLargerThanMd) {
return 5;
}
if (breakpoints.isLargerThanSm) {
return 4;
}
return 2;
}
@@ -0,0 +1,54 @@
import { useResizeObserver } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { Breakpoints } from '/@/shared/types/types';
export function useContainerBreakpoints() {
const [ref, rect] = useResizeObserver();
const [globalBreakpoints, setGlobalBreakpoints] = useState({
lg: 0,
md: 0,
sm: 0,
xl: 0,
xxl: 0,
xxxl: 0,
});
useEffect(() => {
const root = document.documentElement;
const computedStyle = getComputedStyle(root);
const getBreakpointValue = (breakpoint: string) => {
const rootFontSize = 16;
const value = computedStyle.getPropertyValue(`--theme-breakpoint-${breakpoint}`).trim();
return parseInt(value, 10) * rootFontSize || 0;
};
setGlobalBreakpoints({
lg: getBreakpointValue('lg'),
md: getBreakpointValue('md'),
sm: getBreakpointValue('sm'),
xl: getBreakpointValue('xl'),
xxl: getBreakpointValue('xxl'),
xxxl: getBreakpointValue('xxxl'),
});
}, []);
const isLargerThanSm = rect?.width >= globalBreakpoints.sm;
const isLargerThanMd = rect?.width >= globalBreakpoints.md;
const isLargerThanLg = rect?.width >= globalBreakpoints.lg;
const isLargerThanXl = rect?.width >= globalBreakpoints.xl;
const isLargerThan2xl = rect?.width >= globalBreakpoints.xxl;
const isLargerThan3xl = rect?.width >= globalBreakpoints.xxxl;
const breakpoints: Breakpoints = {
isLargerThan2xl,
isLargerThan3xl,
isLargerThanLg,
isLargerThanMd,
isLargerThanSm,
isLargerThanXl,
};
return { breakpoints, rect, ref };
}
@@ -1,11 +1,8 @@
import { orderBy, shuffle } from 'lodash'; import { orderBy, shuffle } from 'lodash';
import { z } from 'zod';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { JFAlbumListSort } from '/@/shared/api/jellyfin.types'; import { JFAlbumListSort } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDAlbumListSort } from '/@/shared/api/navidrome.types'; import { NDAlbumListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { import {
BasePaginatedQuery, BasePaginatedQuery,
BasePaginatedResponse, BasePaginatedResponse,
@@ -91,7 +88,7 @@ export interface AlbumListQuery extends BasePaginatedQuery<AlbumListSortOptions>
export type AlbumListRequest = { query: AlbumListQuery; totalRecordCount?: number }; export type AlbumListRequest = { query: AlbumListQuery; totalRecordCount?: number };
export type AlbumListResponse = BasePaginatedResponse<Album[]> | null | undefined; export type AlbumListResponse = BasePaginatedResponse<Album[]>;
type AlbumListSortMap = { type AlbumListSortMap = {
jellyfin: Record<AlbumListSort, JFAlbumListSort | undefined>; jellyfin: Record<AlbumListSort, JFAlbumListSort | undefined>;
@@ -199,7 +196,7 @@ export type AlbumDetailQuery = { id: string };
export type AlbumDetailRequest = { query: AlbumDetailQuery }; export type AlbumDetailRequest = { query: AlbumDetailQuery };
export type AlbumDetailResponse = Album | null | undefined; export type AlbumDetailResponse = Album;
export type AlbumInfo = { export type AlbumInfo = {
imageUrl: null | string; imageUrl: null | string;
@@ -133,7 +133,7 @@ export interface ArtistListQuery extends BasePaginatedQuery<ArtistListSortOption
export type ArtistListRequest = { query: ArtistListQuery; totalRecordCount?: number }; export type ArtistListRequest = { query: ArtistListQuery; totalRecordCount?: number };
export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined; export type ArtistListResponse = BasePaginatedResponse<Artist[]>;
type ArtistListSortMap = { type ArtistListSortMap = {
jellyfin: Record<ArtistListSort, JFArtistListSort | undefined>; jellyfin: Record<ArtistListSort, JFArtistListSort | undefined>;
navidrome: Record<ArtistListSort, undefined>; navidrome: Record<ArtistListSort, undefined>;
@@ -36,7 +36,7 @@ export interface GenreListQuery extends BasePaginatedQuery<GenreListSortOptions>
export type GenreListRequest = { query: GenreListQuery; totalRecordCount?: number }; export type GenreListRequest = { query: GenreListQuery; totalRecordCount?: number };
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined; export type GenreListResponse = BasePaginatedResponse<Genre[]>;
export type RelatedGenre = { export type RelatedGenre = {
id: string; id: string;
@@ -119,7 +119,7 @@ export interface PlaylistListQuery extends BasePaginatedQuery<PlaylistListSortOp
export type PlaylistListRequest = { query: PlaylistListQuery; totalRecordCount?: number }; export type PlaylistListRequest = { query: PlaylistListQuery; totalRecordCount?: number };
export type PlaylistListResponse = BasePaginatedResponse<Playlist[]> | null | undefined; export type PlaylistListResponse = BasePaginatedResponse<Playlist[]>;
export type PlaylistSong = Song & { export type PlaylistSong = Song & {
playlistItemId: string; playlistItemId: string;
+2 -2
View File
@@ -153,7 +153,7 @@ export interface SongListQuery extends BasePaginatedQuery<SongListSort> {
export type SongListRequest = { query: SongListQuery; totalRecordCount?: number }; export type SongListRequest = { query: SongListQuery; totalRecordCount?: number };
export type SongListResponse = BasePaginatedResponse<Song[]> | null | undefined; export type SongListResponse = BasePaginatedResponse<Song[]>;
type SongListSortMap = { type SongListSortMap = {
jellyfin: Record<SongListSort, JFSongListSort | undefined>; jellyfin: Record<SongListSort, JFSongListSort | undefined>;
navidrome: Record<SongListSort, NDSongListSort | undefined>; navidrome: Record<SongListSort, NDSongListSort | undefined>;
@@ -263,7 +263,7 @@ export type TopSongListQuery = {
export type TopSongListRequest = { query: TopSongListQuery; totalRecordCount?: number }; export type TopSongListRequest = { query: TopSongListQuery; totalRecordCount?: number };
export type TopSongListResponse = BasePaginatedResponse<Song[]> | null | undefined; export type TopSongListResponse = BasePaginatedResponse<Song[]>;
export const sortSongList = ( export const sortSongList = (
songs: QueueSong[], songs: QueueSong[],
+9
View File
@@ -24,6 +24,15 @@ export enum Platform {
WINDOWS = 'windows', WINDOWS = 'windows',
} }
export type Breakpoints = {
isLargerThan2xl: boolean;
isLargerThan3xl: boolean;
isLargerThanLg: boolean;
isLargerThanMd: boolean;
isLargerThanSm: boolean;
isLargerThanXl: boolean;
};
export type CardRoute = { export type CardRoute = {
route: AppRoute | string; route: AppRoute | string;
slugs?: RouteSlug[]; slugs?: RouteSlug[];