Browse Source

Add genre list page

-
- Add genre sort to getAllAlbums api
- Add genres to sortType picker
- Add router query param for sortType on album list
- Remove deprecated LibraryView page
master
jeffvli 3 years ago
parent
commit
518fb4c9f0
  1. 6
      src/App.tsx
  2. 33
      src/api/api.ts
  3. 76
      src/components/library/AlbumList.tsx
  4. 93
      src/components/library/GenreList.tsx
  5. 133
      src/components/library/LibraryView.tsx
  6. 29
      src/components/settings/ConfigPanels/LookAndFeelConfig.tsx
  7. 54
      src/components/settings/ListViewColumns.ts
  8. 45
      src/components/shared/setDefaultSettings.ts

6
src/App.tsx

@ -10,13 +10,13 @@ import NowPlayingView from './components/player/NowPlayingView';
import Login from './components/settings/Login'; import Login from './components/settings/Login';
import StarredView from './components/starred/StarredView'; import StarredView from './components/starred/StarredView';
import Dashboard from './components/dashboard/Dashboard'; import Dashboard from './components/dashboard/Dashboard';
import LibraryView from './components/library/LibraryView';
import PlayerBar from './components/player/PlayerBar'; import PlayerBar from './components/player/PlayerBar';
import AlbumView from './components/library/AlbumView'; import AlbumView from './components/library/AlbumView';
import ArtistView from './components/library/ArtistView'; import ArtistView from './components/library/ArtistView';
import setDefaultSettings from './components/shared/setDefaultSettings'; import setDefaultSettings from './components/shared/setDefaultSettings';
import AlbumList from './components/library/AlbumList'; import AlbumList from './components/library/AlbumList';
import ArtistList from './components/library/ArtistList'; import ArtistList from './components/library/ArtistList';
import GenreList from './components/library/GenreList';
import { MockFooter } from './components/settings/styled'; import { MockFooter } from './components/settings/styled';
import { defaultDark, defaultLight } from './styles/styledTheme'; import { defaultDark, defaultLight } from './styles/styledTheme';
import { useAppSelector } from './redux/hooks'; import { useAppSelector } from './redux/hooks';
@ -66,10 +66,10 @@ const App = () => {
<Switch> <Switch>
<Route exact path="/library/album" component={AlbumList} /> <Route exact path="/library/album" component={AlbumList} />
<Route exact path="/library/artist" component={ArtistList} /> <Route exact path="/library/artist" component={ArtistList} />
<Route exact path="/library/genre" component={LibraryView} /> <Route exact path="/library/genre" component={GenreList} />
<Route exact path="/library/artist/:id" component={ArtistView} /> <Route exact path="/library/artist/:id" component={ArtistView} />
<Route exact path="/library/album/:id" component={AlbumView} /> <Route exact path="/library/album/:id" component={AlbumView} />
<Route exact path="/folder" component={LibraryView} /> <Route exact path="/folder" />
<Route exact path="/nowplaying" component={NowPlayingView} /> <Route exact path="/nowplaying" component={NowPlayingView} />
<Route exact path="/playlist/:id" component={PlaylistView} /> <Route exact path="/playlist/:id" component={PlaylistView} />
<Route exact path="/playlist" component={PlaylistList} /> <Route exact path="/playlist" component={PlaylistList} />

33
src/api/api.ts

@ -281,9 +281,14 @@ export const getAllAlbums = (
const albums: any = api const albums: any = api
.get(`/getAlbumList2`, { .get(`/getAlbumList2`, {
params: { params: {
type: sortType, type: sortType.match('alphabeticalByName|alphabeticalByArtist|frequent|newest|recent')
? sortType
: 'byGenre',
size: 500, size: 500,
offset, offset,
genre: sortType.match('alphabeticalByName|alphabeticalByArtist|frequent|newest|recent')
? undefined
: sortType,
}, },
}) })
.then((res) => { .then((res) => {
@ -654,3 +659,29 @@ export const clearPlaylist = async (playlistId: string) => {
return data; return data;
}; };
export const getGenres = async () => {
const { data } = await api.get(`/getGenres`);
return (data.genres.genre || []).map((entry: any, index: any) => ({
...entry,
name: entry.value,
index,
uniqueId: nanoid(),
}));
};
// return {
// ...data.artist,
// image: getCoverArtUrl(data.artist, coverArtSize),
// type: 'artist',
// album: (data.artist.album || []).map((entry: any, index: any) => ({
// ...entry,
// albumId: entry.id,
// type: 'album',
// image: getCoverArtUrl(entry, coverArtSize),
// starred: entry.starred || undefined,
// index,
// uniqueId: nanoid(),
// })),
// };

76
src/components/library/AlbumList.tsx

@ -1,14 +1,15 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import _ from 'lodash';
import settings from 'electron-settings'; import settings from 'electron-settings';
import { ButtonToolbar } from 'rsuite'; import { ButtonToolbar } from 'rsuite';
import { useQuery, useQueryClient } from 'react-query'; import { useQuery, useQueryClient } from 'react-query';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router-dom';
import GridViewType from '../viewtypes/GridViewType'; import GridViewType from '../viewtypes/GridViewType';
import ListViewType from '../viewtypes/ListViewType'; import ListViewType from '../viewtypes/ListViewType';
import useSearchQuery from '../../hooks/useSearchQuery'; import useSearchQuery from '../../hooks/useSearchQuery';
import GenericPageHeader from '../layout/GenericPageHeader'; import GenericPageHeader from '../layout/GenericPageHeader';
import GenericPage from '../layout/GenericPage'; import GenericPage from '../layout/GenericPage';
import { getAlbumsDirect, getAllAlbums } from '../../api/api'; import { getAlbumsDirect, getAllAlbums, getGenres } from '../../api/api';
import PageLoader from '../loader/PageLoader'; import PageLoader from '../loader/PageLoader';
import { useAppDispatch } from '../../redux/hooks'; import { useAppDispatch } from '../../redux/hooks';
import { import {
@ -19,22 +20,25 @@ import {
} from '../../redux/multiSelectSlice'; } from '../../redux/multiSelectSlice';
import { StyledInputPicker } from '../shared/styled'; import { StyledInputPicker } from '../shared/styled';
import { RefreshButton } from '../shared/ToolbarButtons'; import { RefreshButton } from '../shared/ToolbarButtons';
import useRouterQuery from '../../hooks/useRouterQuery';
const ALBUM_SORT_TYPES = [ const ALBUM_SORT_TYPES = [
{ label: 'A-Z (Name)', value: 'alphabeticalByName' }, { label: 'A-Z (Name)', value: 'alphabeticalByName', role: 'Default' },
{ label: 'A-Z (Artist)', value: 'alphabeticalByArtist' }, { label: 'A-Z (Artist)', value: 'alphabeticalByArtist', role: 'Default' },
{ label: 'Most Played', value: 'frequent' }, { label: 'Most Played', value: 'frequent', role: 'Default' },
{ label: 'Newly Added', value: 'newest' }, { label: 'Newly Added', value: 'newest', role: 'Default' },
{ label: 'Random', value: 'random' }, { label: 'Random', value: 'random', role: 'Default' },
{ label: 'Recently Played', value: 'recent' }, { label: 'Recently Played', value: 'recent', role: 'Default' },
]; ];
const AlbumList = () => { const AlbumList = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const history = useHistory(); const history = useHistory();
const query = useRouterQuery();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [sortBy, setSortBy] = useState('random'); const [sortBy, setSortBy] = useState(query.get('sortType') || 'random');
const [sortTypes, setSortTypes] = useState<any[]>();
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const [viewType, setViewType] = useState(settings.getSync('albumViewType')); const [viewType, setViewType] = useState(settings.getSync('albumViewType'));
const { isLoading, isError, data: albums, error }: any = useQuery( const { isLoading, isError, data: albums, error }: any = useQuery(
@ -49,9 +53,30 @@ const AlbumList = () => {
staleTime: Infinity, // Only allow manual refresh staleTime: Infinity, // Only allow manual refresh
} }
); );
const { data: genres }: any = useQuery(
['genreList'],
async () => {
const res = await getGenres();
return res.map((genre: any) => {
if (genre.albumCount !== 0) {
return {
label: `${genre.value} (${genre.albumCount})`,
value: genre.value,
role: 'Genre',
};
}
return null;
});
},
{ refetchOnWindowFocus: false }
);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const filteredData = useSearchQuery(searchQuery, albums, ['name', 'artist', 'genre', 'year']); const filteredData = useSearchQuery(searchQuery, albums, ['name', 'artist', 'genre', 'year']);
useEffect(() => {
setSortTypes(_.compact(_.concat(ALBUM_SORT_TYPES, genres)));
}, [genres]);
let timeout: any = null; let timeout: any = null;
const handleRowClick = (e: any, rowData: any) => { const handleRowClick = (e: any, rowData: any) => {
if (timeout === null) { if (timeout === null) {
@ -84,6 +109,7 @@ const AlbumList = () => {
return ( return (
<GenericPage <GenericPage
hideDivider
header={ header={
<GenericPageHeader <GenericPageHeader
title="Albums" title="Albums"
@ -93,19 +119,22 @@ const AlbumList = () => {
</ButtonToolbar> </ButtonToolbar>
} }
subsidetitle={ subsidetitle={
<StyledInputPicker <>
width={140} <StyledInputPicker
defaultValue={sortBy} width={180}
data={ALBUM_SORT_TYPES} defaultValue={sortBy}
cleanable={false} groupBy="role"
placeholder="Sort Type" data={sortTypes}
onChange={async (value: string) => { cleanable={false}
await queryClient.cancelQueries(['albumList', offset, sortBy]); placeholder="Sort Type"
setSearchQuery(''); onChange={async (value: string) => {
setOffset(0); await queryClient.cancelQueries(['albumList', offset, sortBy]);
setSortBy(value); setSearchQuery('');
}} setOffset(0);
/> setSortBy(value);
}}
/>
</>
} }
searchQuery={searchQuery} searchQuery={searchQuery}
handleSearch={(e: any) => setSearchQuery(e)} handleSearch={(e: any) => setSearchQuery(e)}
@ -138,7 +167,6 @@ const AlbumList = () => {
disabledContextMenuOptions={['moveSelectedTo', 'removeFromCurrent', 'deletePlaylist']} disabledContextMenuOptions={['moveSelectedTo', 'removeFromCurrent', 'deletePlaylist']}
/> />
)} )}
{!isLoading && !isError && viewType === 'grid' && ( {!isLoading && !isError && viewType === 'grid' && (
<GridViewType <GridViewType
data={searchQuery !== '' ? filteredData : albums} data={searchQuery !== '' ? filteredData : albums}

93
src/components/library/GenreList.tsx

@ -0,0 +1,93 @@
import React, { useState } from 'react';
import settings from 'electron-settings';
import { useQuery } from 'react-query';
import { useHistory } from 'react-router';
import useSearchQuery from '../../hooks/useSearchQuery';
import GenericPage from '../layout/GenericPage';
import GenericPageHeader from '../layout/GenericPageHeader';
import ListViewType from '../viewtypes/ListViewType';
import PageLoader from '../loader/PageLoader';
import { useAppDispatch } from '../../redux/hooks';
import {
clearSelected,
setRangeSelected,
toggleRangeSelected,
toggleSelected,
} from '../../redux/multiSelectSlice';
import { getGenres } from '../../api/api';
const GenreList = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const { isLoading, isError, data: genres, error }: any = useQuery(['genreList'], () =>
getGenres()
);
const [searchQuery, setSearchQuery] = useState('');
const filteredData = useSearchQuery(searchQuery, genres, ['value']);
let timeout: any = null;
const handleRowClick = (e: any, rowData: any) => {
if (timeout === null) {
timeout = window.setTimeout(() => {
timeout = null;
if (e.ctrlKey) {
dispatch(toggleSelected(rowData));
} else if (e.shiftKey) {
dispatch(setRangeSelected(rowData));
dispatch(toggleRangeSelected(searchQuery !== '' ? filteredData : genres));
}
}, 100);
}
};
const handleRowDoubleClick = (rowData: any) => {
window.clearTimeout(timeout);
timeout = null;
dispatch(clearSelected());
history.push(`/library/album?sortType=${rowData.value}`);
};
return (
<GenericPage
hideDivider
header={
<GenericPageHeader
title="Genres"
searchQuery={searchQuery}
handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')}
viewTypeSetting="genre"
showSearchBar
/>
}
>
{isLoading && <PageLoader />}
{isError && <div>Error: {error}</div>}
{!isLoading && !isError && (
<ListViewType
data={searchQuery !== '' ? filteredData : genres}
tableColumns={settings.getSync('genreListColumns')}
rowHeight={Number(settings.getSync('genreListRowHeight'))}
fontSize={settings.getSync('genreListFontSize')}
handleRowClick={handleRowClick}
handleRowDoubleClick={handleRowDoubleClick}
listType="genre"
virtualized
disabledContextMenuOptions={[
'addToQueue',
'moveSelectedTo',
'removeFromCurrent',
'addToPlaylist',
'deletePlaylist',
'addToFavorites',
'removeFromFavorites',
]}
/>
)}
</GenericPage>
);
};
export default GenreList;

133
src/components/library/LibraryView.tsx

@ -1,133 +0,0 @@
import React, { useState } from 'react';
import { Nav, SelectPicker } from 'rsuite';
import { useQuery } from 'react-query';
import VisibilitySensor from 'react-visibility-sensor';
import settings from 'electron-settings';
import _ from 'lodash';
import useSearchQuery from '../../hooks/useSearchQuery';
import { getAlbumsDirect, getArtists } from '../../api/api';
import GenericPage from '../layout/GenericPage';
import GenericPageHeader from '../layout/GenericPageHeader';
import ArtistList from './ArtistList';
import PageLoader from '../loader/PageLoader';
const ALBUM_SORT_TYPES = [
{ label: 'A-Z (Name)', value: 'alphabeticalByName' },
{
label: 'A-Z (Artist)',
value: 'alphabeticalByArtist',
},
{ label: 'Most Played', value: 'frequent' },
{ label: 'Newly Added', value: 'newest' },
{ label: 'Recently Played', value: 'recent' },
];
const LibraryView = () => {
const [currentPage, setCurrentPage] = useState('albums');
const [sortBy, setSortBy] = useState(null);
const [data, setData] = useState<any[]>([]);
const [offset, setOffset] = useState(0);
const [viewType, setViewType] = useState(settings.getSync('albumViewType'));
const { isLoading: isLoadingArtists, data: artists }: any = useQuery('artists', getArtists, {
enabled: currentPage === 'artists',
});
const [searchQuery, setSearchQuery] = useState('');
const filteredData = useSearchQuery(
searchQuery,
currentPage === 'artists' ? artists : currentPage === 'albums' ? data : data,
['name', 'artist']
);
const onChange = (isVisible: boolean) => {
if (isVisible) {
setOffset(offset + 50);
setTimeout(async () => {
const res = await getAlbumsDirect({
type: sortBy || 'random',
size: 50,
offset,
});
const combinedData = data.concat(res);
// Ensure that no duplicates are added in the case of random fetching
const uniqueCombinedData = _.uniqBy(combinedData, (e: any) => e.id);
return setData(uniqueCombinedData);
}, 0);
}
};
const handleNavClick = (e: React.SetStateAction<string>) => {
setData([]);
setOffset(0);
setCurrentPage(e);
};
return (
<GenericPage
header={
<GenericPageHeader
title="Library"
subtitle={
<Nav activeKey={currentPage} onSelect={handleNavClick}>
<Nav.Item eventKey="albums">Albums</Nav.Item>
<Nav.Item eventKey="artists">Artists</Nav.Item>
<Nav.Item eventKey="genres">Genres</Nav.Item>
</Nav>
}
subsidetitle={
currentPage === 'albums' ? (
<SelectPicker
data={ALBUM_SORT_TYPES}
searchable={false}
placeholder="Sort Type"
menuAutoWidth
onChange={(value) => {
setData([]);
setOffset(0);
setSortBy(value);
}}
/>
) : undefined
}
searchQuery={searchQuery}
handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')}
showViewTypeButtons={currentPage === 'albums'}
viewTypeSetting="album"
showSearchBar
handleListClick={() => setViewType('list')}
handleGridClick={() => setViewType('grid')}
/>
}
>
{isLoadingArtists && <PageLoader />}
{artists && (
<>
{currentPage === 'artists' && (
<ArtistList viewType={viewType} data={searchQuery === '' ? artists : filteredData} />
)}
</>
)}
{data.length !== 1 && searchQuery === '' && currentPage === 'albums' && (
<VisibilitySensor onChange={onChange}>
<div
style={{
textAlign: 'center',
marginTop: '25px',
marginBottom: '25px',
}}
>
<PageLoader size="md" />
</div>
</VisibilitySensor>
)}
</GenericPage>
);
};
export default LibraryView;

29
src/components/settings/ConfigPanels/LookAndFeelConfig.tsx

@ -22,6 +22,8 @@ import {
playlistColumnList, playlistColumnList,
artistColumnPicker, artistColumnPicker,
artistColumnList, artistColumnList,
genreColumnPicker,
genreColumnList,
} from '../ListViewColumns'; } from '../ListViewColumns';
const LookAndFeelConfig = () => { const LookAndFeelConfig = () => {
@ -33,11 +35,13 @@ const LookAndFeelConfig = () => {
const playlistCols: any = settings.getSync('playlistListColumns'); const playlistCols: any = settings.getSync('playlistListColumns');
const artistCols: any = settings.getSync('artistListColumns'); const artistCols: any = settings.getSync('artistListColumns');
const miniCols: any = settings.getSync('miniListColumns'); const miniCols: any = settings.getSync('miniListColumns');
const genreCols: any = settings.getSync('genreListColumns');
const currentSongColumns = songCols?.map((column: any) => column.label) || []; const currentSongColumns = songCols?.map((column: any) => column.label) || [];
const currentAlbumColumns = albumCols?.map((column: any) => column.label) || []; const currentAlbumColumns = albumCols?.map((column: any) => column.label) || [];
const currentPlaylistColumns = playlistCols?.map((column: any) => column.label) || []; const currentPlaylistColumns = playlistCols?.map((column: any) => column.label) || [];
const currentArtistColumns = artistCols?.map((column: any) => column.label) || []; const currentArtistColumns = artistCols?.map((column: any) => column.label) || [];
const currentMiniColumns = miniCols?.map((column: any) => column.label) || []; const currentMiniColumns = miniCols?.map((column: any) => column.label) || [];
const currentGenreColumns = genreCols?.map((column: any) => column.label) || [];
return ( return (
<ConfigPanel header="Look & Feel" bordered> <ConfigPanel header="Look & Feel" bordered>
@ -88,11 +92,12 @@ const LookAndFeelConfig = () => {
activeKey={currentLAFTab} activeKey={currentLAFTab}
onSelect={(e) => setCurrentLAFTab(e)} onSelect={(e) => setCurrentLAFTab(e)}
> >
<StyledNavItem eventKey="songList">Song List</StyledNavItem> <StyledNavItem eventKey="songList">Songs</StyledNavItem>
<StyledNavItem eventKey="albumList">Album List</StyledNavItem> <StyledNavItem eventKey="albumList">Albums</StyledNavItem>
<StyledNavItem eventKey="playlistList">Playlist List</StyledNavItem> <StyledNavItem eventKey="playlistList">Playlists</StyledNavItem>
<StyledNavItem eventKey="artistList">Artist List</StyledNavItem> <StyledNavItem eventKey="artistList">Artists</StyledNavItem>
<StyledNavItem eventKey="miniList">Miniplayer List</StyledNavItem> <StyledNavItem eventKey="genreList">Genres</StyledNavItem>
<StyledNavItem eventKey="miniList">Miniplayer</StyledNavItem>
</Nav> </Nav>
{currentLAFTab === 'songList' && ( {currentLAFTab === 'songList' && (
<ListViewConfig <ListViewConfig
@ -150,6 +155,20 @@ const LookAndFeelConfig = () => {
/> />
)} )}
{currentLAFTab === 'genreList' && (
<ListViewConfig
title="Genre List"
defaultColumns={currentGenreColumns}
columnPicker={genreColumnPicker}
columnList={genreColumnList}
settingsConfig={{
columnList: 'genreListColumns',
rowHeight: 'genreListRowHeight',
fontSize: 'genreListFontSize',
}}
/>
)}
{currentLAFTab === 'miniList' && ( {currentLAFTab === 'miniList' && (
<ListViewConfig <ListViewConfig
title="Miniplayer List" title="Miniplayer List"

54
src/components/settings/ListViewColumns.ts

@ -536,3 +536,57 @@ export const artistColumnPicker = [
{ label: 'Favorite' }, { label: 'Favorite' },
{ label: 'Name' }, { label: 'Name' },
]; ];
export const genreColumnPicker = [
{ label: '#' },
{ label: 'Album Count' },
{ label: 'Name' },
{ label: 'Song Count' },
];
export const genreColumnList = [
{
label: '#',
value: {
id: '#',
dataKey: 'index',
alignment: 'center',
resizable: true,
width: 50,
label: '#',
},
},
{
label: 'Album Count',
value: {
id: 'Album Count',
dataKey: 'albumCount',
alignment: 'left',
resizable: true,
width: 100,
label: 'Album Count',
},
},
{
label: 'Name',
value: {
id: 'Name',
dataKey: 'name',
alignment: 'left',
resizable: true,
width: 300,
label: 'Name',
},
},
{
label: 'Song Count',
value: {
id: 'Song Count',
dataKey: 'songCount',
alignment: 'left',
resizable: true,
width: 100,
label: 'Song Count',
},
},
];

45
src/components/shared/setDefaultSettings.ts

@ -366,6 +366,51 @@ const setDefaultSettings = (force: boolean) => {
}, },
]); ]);
} }
if (force || !settings.hasSync('genreListFontSize')) {
settings.setSync('genreListFontSize', '14');
}
if (force || !settings.hasSync('genreListRowHeight')) {
settings.setSync('genreListRowHeight', '50');
}
if (force || !settings.hasSync('genreListColumns')) {
settings.setSync('genreListColumns', [
{
id: '#',
dataKey: 'index',
alignment: 'center',
resizable: true,
width: 50,
label: '#',
},
{
id: 'Name',
dataKey: 'name',
alignment: 'left',
resizable: true,
width: 300,
label: 'Name',
},
{
id: 'Album Count',
dataKey: 'albumCount',
alignment: 'left',
resizable: true,
width: 100,
label: 'Album Count',
},
{
id: 'Song Count',
dataKey: 'songCount',
alignment: 'left',
resizable: true,
width: 100,
label: 'Song Count',
},
]);
}
}; };
export default setDefaultSettings; export default setDefaultSettings;

Loading…
Cancel
Save