Browse Source

added views, improvements, styling

- add artist view/route
- add album view/route
- add links from other components to artist/album routes
- separate viewtype settings by type
- add list types for albums
master
jeffvli 3 years ago
parent
commit
74c8cf66f1
  1. 49
      src/App.tsx
  2. 55
      src/api/api.ts
  3. 24
      src/components/dashboard/Dashboard.tsx
  4. 30
      src/components/layout/GenericPageHeader.tsx
  5. 18
      src/components/library/AlbumList.tsx
  6. 161
      src/components/library/AlbumView.tsx
  7. 212
      src/components/library/ArtistView.tsx
  8. 3
      src/components/library/LibraryView.tsx
  9. 11
      src/components/library/styled.tsx
  10. 172
      src/components/player/PlayerBar.tsx
  11. 11
      src/components/player/styled.tsx
  12. 3
      src/components/playlist/PlaylistList.tsx
  13. 2
      src/components/playlist/PlaylistView.tsx
  14. 4
      src/components/scrollingmenu/ScrollingMenu.tsx
  15. 196
      src/components/settings/Config.tsx
  16. 56
      src/components/settings/Login.tsx
  17. 4
      src/components/shared/CustomTooltip.tsx
  18. 62
      src/components/starred/StarredView.tsx
  19. 3
      src/components/viewtypes/ListViewType.tsx
  20. 14
      src/components/viewtypes/ViewTypeButtons.tsx

49
src/App.tsx

@ -1,4 +1,5 @@
import React from 'react'; import React, { useCallback } from 'react';
import { GlobalHotKeys } from 'react-hotkeys';
import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import './styles/App.global.css'; import './styles/App.global.css';
import Layout from './components/layout/Layout'; import Layout from './components/layout/Layout';
@ -11,8 +12,18 @@ 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 LibraryView from './components/library/LibraryView';
import PlayerBar from './components/player/PlayerBar'; import PlayerBar from './components/player/PlayerBar';
import AlbumView from './components/library/AlbumView';
import ArtistView from './components/library/ArtistView';
const keyMap = {
FOCUS_SEARCH: 'ctrl+f',
};
const App = () => { const App = () => {
const focusSearchInput = useCallback(() => {
document.getElementById('local-search-input')?.focus();
}, []);
if (!localStorage.getItem('server')) { if (!localStorage.getItem('server')) {
return ( return (
<Layout <Layout
@ -33,22 +44,28 @@ const App = () => {
); );
} }
const handlers = {
FOCUS_SEARCH: focusSearchInput,
};
return ( return (
<Router> <GlobalHotKeys keyMap={keyMap} handlers={handlers}>
<Layout footer={<PlayerBar />}> <Router>
<Switch> <Layout footer={<PlayerBar />}>
<Route exact path="/library/artist/:id" component={NowPlayingView} /> <Switch>
<Route exact path="/library/album/:id" component={NowPlayingView} /> <Route exact path="/library/artist/:id" component={ArtistView} />
<Route exact path="/library" component={LibraryView} /> <Route exact path="/library/album/:id" component={AlbumView} />
<Route exact path="/nowplaying" component={NowPlayingView} /> <Route exact path="/library" component={LibraryView} />
<Route exact path="/playlist/:id" component={PlaylistView} /> <Route exact path="/nowplaying" component={NowPlayingView} />
<Route exact path="/playlists" component={PlaylistList} /> <Route exact path="/playlist/:id" component={PlaylistView} />
<Route exact path="/starred" component={StarredView} /> <Route exact path="/playlists" component={PlaylistList} />
<Route exact path="/config" component={Config} /> <Route exact path="/starred" component={StarredView} />
<Route path="/" component={Dashboard} /> <Route exact path="/config" component={Config} />
</Switch> <Route path="/" component={Dashboard} />
</Layout> </Switch>
</Router> </Layout>
</Router>
</GlobalHotKeys>
); );
}; };

55
src/api/api.ts

@ -139,6 +139,7 @@ export const getStarred = async () => {
...data.starred2, ...data.starred2,
album: (data.starred2.album || []).map((entry: any, index: any) => ({ album: (data.starred2.album || []).map((entry: any, index: any) => ({
...entry, ...entry,
albumId: entry.id,
image: getCoverArtUrl(entry), image: getCoverArtUrl(entry),
index, index,
})), })),
@ -161,6 +162,7 @@ export const getAlbums = async (options: any, coverArtSize = 150) => {
...data.albumList2, ...data.albumList2,
album: (data.albumList2.album || []).map((entry: any, index: any) => ({ album: (data.albumList2.album || []).map((entry: any, index: any) => ({
...entry, ...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, coverArtSize), image: getCoverArtUrl(entry, coverArtSize),
starred: entry.starred || '', starred: entry.starred || '',
index, index,
@ -176,6 +178,7 @@ export const getAlbumsDirect = async (options: any, coverArtSize = 150) => {
const albums = (data.albumList2.album || []).map( const albums = (data.albumList2.album || []).map(
(entry: any, index: any) => ({ (entry: any, index: any) => ({
...entry, ...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, coverArtSize), image: getCoverArtUrl(entry, coverArtSize),
starred: entry.starred || '', starred: entry.starred || '',
index, index,
@ -194,6 +197,7 @@ export const getAlbum = async (id: string, coverArtSize = 150) => {
return { return {
...data.album, ...data.album,
image: getCoverArtUrl(data.album, coverArtSize),
song: (data.album.song || []).map((entry: any, index: any) => ({ song: (data.album.song || []).map((entry: any, index: any) => ({
...entry, ...entry,
streamUrl: getStreamUrl(entry.id), streamUrl: getStreamUrl(entry.id),
@ -236,6 +240,39 @@ export const getArtists = async () => {
return artistList; return artistList;
}; };
export const getArtist = async (id: string, coverArtSize = 150) => {
const { data } = await api.get(`/getArtist`, {
params: {
id,
},
});
return {
...data.artist,
image: getCoverArtUrl(data.artist, coverArtSize),
album: (data.artist.album || []).map((entry: any, index: any) => ({
...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, coverArtSize),
starred: entry.starred || '',
index,
})),
};
};
export const getArtistInfo = async (id: string, count = 10) => {
const { data } = await api.get(`/getArtistInfo2`, {
params: {
id,
count,
},
});
return {
...data.artistInfo2,
};
};
export const startScan = async () => { export const startScan = async () => {
const { data } = await api.get(`/startScan`); const { data } = await api.get(`/startScan`);
const scanStatus = data?.scanStatus; const scanStatus = data?.scanStatus;
@ -273,3 +310,21 @@ export const unstar = async (id: string, type: string) => {
return data; return data;
}; };
export const getSimilarSongs = async (
id: string,
count: number,
coverArtSize = 150
) => {
const { data } = await api.get(`/getSimilarSongs2`, {
params: { id, count },
});
return {
song: (data.similarSongs2.song || []).map((entry: any, index: any) => ({
...entry,
image: getCoverArtUrl(entry, coverArtSize),
index,
})),
};
};

24
src/components/dashboard/Dashboard.tsx

@ -68,7 +68,11 @@ const Dashboard = () => {
title="Recently Played" title="Recently Played"
data={recentAlbums.album} data={recentAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }} cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }} cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px" cardSize="175px"
/> />
@ -76,7 +80,11 @@ const Dashboard = () => {
title="Recently Added" title="Recently Added"
data={newestAlbums.album} data={newestAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }} cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }} cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px" cardSize="175px"
/> />
@ -84,7 +92,11 @@ const Dashboard = () => {
title="Random" title="Random"
data={randomAlbums.album} data={randomAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }} cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }} cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px" cardSize="175px"
/> />
@ -92,7 +104,11 @@ const Dashboard = () => {
title="Most Played" title="Most Played"
data={frequentAlbums.album} data={frequentAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }} cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }} cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px" cardSize="175px"
/> />
</> </>

30
src/components/layout/GenericPageHeader.tsx

@ -4,6 +4,7 @@ import ViewTypeButtons from '../viewtypes/ViewTypeButtons';
const GenericPageHeader = ({ const GenericPageHeader = ({
image, image,
imageHeight,
title, title,
subtitle, subtitle,
sidetitle, sidetitle,
@ -15,12 +16,18 @@ const GenericPageHeader = ({
showSearchBar, showSearchBar,
handleListClick, handleListClick,
handleGridClick, handleGridClick,
viewTypeSetting,
}: any) => { }: any) => {
return ( return (
<> <>
{image && ( {image && (
<div style={{ display: 'inline-block' }}> <div style={{ display: 'inline-block' }}>
<img src={image} alt="header-img" height="145px" width="145px" /> <img
src={image}
alt="header-img"
height={imageHeight || '145px'}
width={imageHeight || '145px'}
/>
</div> </div>
)} )}
@ -66,14 +73,18 @@ const GenericPageHeader = ({
<span style={{ display: 'inline-block' }}> <span style={{ display: 'inline-block' }}>
<InputGroup inside> <InputGroup inside>
<Input <Input
id="local-search-input"
size="md" size="md"
value={searchQuery} value={searchQuery}
placeholder="Search..." placeholder="Search..."
onChange={handleSearch} onChange={handleSearch}
/> />
{searchQuery !== '' && ( {searchQuery !== '' && (
<InputGroup.Button appearance="subtle"> <InputGroup.Button
<Icon icon="close" onClick={clearSearchQuery} /> appearance="subtle"
onClick={clearSearchQuery}
>
<Icon icon="close" />
</InputGroup.Button> </InputGroup.Button>
)} )}
</InputGroup> </InputGroup>
@ -88,7 +99,17 @@ const GenericPageHeader = ({
height: '50%', height: '50%',
}} }}
> >
<span style={{ alignSelf: 'center' }}>{subtitle}</span> <span
style={{
alignSelf: 'center',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
width: '55%',
}}
>
{subtitle}
</span>
<span style={{ alignSelf: 'center' }}> <span style={{ alignSelf: 'center' }}>
{subsidetitle && ( {subsidetitle && (
<span style={{ display: 'inline-block' }}>{subsidetitle}</span> <span style={{ display: 'inline-block' }}>{subsidetitle}</span>
@ -98,6 +119,7 @@ const GenericPageHeader = ({
<ViewTypeButtons <ViewTypeButtons
handleListClick={handleListClick} handleListClick={handleListClick}
handleGridClick={handleGridClick} handleGridClick={handleGridClick}
viewTypeSetting={viewTypeSetting}
/> />
</span> </span>
)} )}

18
src/components/library/AlbumList.tsx

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import settings from 'electron-settings';
import GridViewType from '../viewtypes/GridViewType'; import GridViewType from '../viewtypes/GridViewType';
import ListViewType from '../viewtypes/ListViewType';
const AlbumList = ({ data, viewType }: any) => { const AlbumList = ({ data, viewType }: any) => {
if (viewType === 'grid') { if (viewType === 'grid') {
@ -19,6 +21,22 @@ const AlbumList = ({ data, viewType }: any) => {
/> />
); );
} }
if (viewType === 'list') {
return (
<ListViewType
data={data}
tableColumns={settings.getSync('albumListColumns')}
rowHeight={Number(settings.getSync('albumListRowHeight'))}
fontSize={settings.getSync('albumListFontSize')}
cacheImages={{
enabled: settings.getSync('cacheImages'),
cacheType: 'album',
}}
virtualized
/>
);
}
return <></>; return <></>;
}; };

161
src/components/library/AlbumView.tsx

@ -0,0 +1,161 @@
import React, { useState } from 'react';
import settings from 'electron-settings';
import { ButtonToolbar, Tag } from 'rsuite';
import { useQuery } from 'react-query';
import { useParams, useHistory } from 'react-router-dom';
import {
DeleteButton,
EditButton,
PlayAppendButton,
PlayButton,
PlayShuffleAppendButton,
PlayShuffleButton,
} from '../shared/ToolbarButtons';
import { getAlbum } from '../../api/api';
import { useAppDispatch } from '../../redux/hooks';
import { fixPlayer2Index, setPlayQueue } from '../../redux/playQueueSlice';
import {
toggleSelected,
setRangeSelected,
toggleRangeSelected,
setSelected,
clearSelected,
} from '../../redux/multiSelectSlice';
import useSearchQuery from '../../hooks/useSearchQuery';
import GenericPage from '../layout/GenericPage';
import ListViewType from '../viewtypes/ListViewType';
import Loader from '../loader/Loader';
import GenericPageHeader from '../layout/GenericPageHeader';
import { TagLink } from './styled';
interface AlbumParams {
id: string;
}
const AlbumView = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const { id } = useParams<AlbumParams>();
const { isLoading, isError, data, error }: any = useQuery(['album', id], () =>
getAlbum(id)
);
const [searchQuery, setSearchQuery] = useState('');
const filteredData = useSearchQuery(searchQuery, data?.song, [
'title',
'artist',
'album',
'genre',
]);
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(data.song));
} else {
dispatch(setSelected(rowData));
}
}, 300);
}
};
const handleRowDoubleClick = (e: any) => {
window.clearTimeout(timeout);
timeout = null;
const newPlayQueue = data.song.slice([e.index], data.song.length);
dispatch(clearSelected());
dispatch(setPlayQueue(newPlayQueue));
dispatch(fixPlayer2Index());
};
if (isLoading) {
return <Loader />;
}
if (isError) {
return <span>Error: {error.message}</span>;
}
return (
<GenericPage
header={
<GenericPageHeader
image={data.image}
title={data.name}
subtitle={
<div>
<div
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{data.artist && (
<Tag>
<TagLink
onClick={() =>
history.push(`/library/artist/${data.artistId}`)
}
>
Artist: {data.artist}
</TagLink>
</Tag>
)}
{data.year && (
<Tag>
<TagLink>Year: {data.year}</TagLink>
</Tag>
)}
{data.genre && (
<Tag>
<TagLink>Genre: {data.genre}</TagLink>
</Tag>
)}
</div>
<div style={{ marginTop: '10px' }}>
<ButtonToolbar>
<PlayButton appearance="primary" size="lg" circle />
<PlayShuffleButton />
<PlayAppendButton />
<PlayShuffleAppendButton />
<EditButton />
<DeleteButton />
</ButtonToolbar>
</div>
</div>
}
searchQuery={searchQuery}
handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')}
showSearchBar
/>
}
>
<ListViewType
data={searchQuery !== '' ? filteredData : data.song}
tableColumns={settings.getSync('songListColumns')}
handleRowClick={handleRowClick}
handleRowDoubleClick={handleRowDoubleClick}
tableHeight={700}
virtualized
rowHeight={Number(settings.getSync('songListRowHeight'))}
fontSize={Number(settings.getSync('songListFontSize'))}
cacheImages={{
enabled: settings.getSync('cacheImages'),
cacheType: 'album',
}}
/>
</GenericPage>
);
};
export default AlbumView;

212
src/components/library/ArtistView.tsx

@ -0,0 +1,212 @@
import React, { useState } from 'react';
import settings from 'electron-settings';
import { ButtonToolbar, Tag, Whisper, Button, Popover, TagGroup } from 'rsuite';
import { useQuery } from 'react-query';
import { useParams, useHistory } from 'react-router-dom';
import {
PlayAppendButton,
PlayButton,
PlayShuffleAppendButton,
PlayShuffleButton,
EditButton,
} from '../shared/ToolbarButtons';
import { getArtist, getArtistInfo } from '../../api/api';
import { useAppDispatch } from '../../redux/hooks';
import { fixPlayer2Index, setPlayQueue } from '../../redux/playQueueSlice';
import {
toggleSelected,
setRangeSelected,
toggleRangeSelected,
setSelected,
clearSelected,
} from '../../redux/multiSelectSlice';
import useSearchQuery from '../../hooks/useSearchQuery';
import GenericPage from '../layout/GenericPage';
import ListViewType from '../viewtypes/ListViewType';
import GridViewType from '../viewtypes/GridViewType';
import Loader from '../loader/Loader';
import GenericPageHeader from '../layout/GenericPageHeader';
import CustomTooltip from '../shared/CustomTooltip';
import { TagLink } from './styled';
interface ArtistParams {
id: string;
}
const ArtistView = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const [viewType, setViewType] = useState(
settings.getSync('albumViewType') || 'list'
);
const { id } = useParams<ArtistParams>();
const { isLoading, isError, data, error }: any = useQuery(
['artist', id],
() => getArtist(id)
);
const {
isLoading: isLoadingAI,
isError: isErrorAI,
data: artistInfo,
error: errorAI,
}: any = useQuery(['artistInfo', id], () => getArtistInfo(id, 8));
const [searchQuery, setSearchQuery] = useState('');
const filteredData = useSearchQuery(searchQuery, data?.song, [
'title',
'artist',
'album',
'genre',
]);
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(data.album));
} else {
dispatch(setSelected(rowData));
}
}, 300);
}
};
const handleRowDoubleClick = (e: any) => {
window.clearTimeout(timeout);
timeout = null;
const newPlayQueue = data.album.slice([e.index], data.album.length);
dispatch(clearSelected());
dispatch(setPlayQueue(newPlayQueue));
dispatch(fixPlayer2Index());
};
if (isLoading || isLoadingAI) {
return <Loader />;
}
if (isError || isErrorAI) {
return (
<span>
Error: {error.message} {errorAI.message}
</span>
);
}
return (
<GenericPage
header={
<GenericPageHeader
image={data.image}
imageHeight={145}
title={data.name}
subtitle={
<>
<CustomTooltip
text={artistInfo.biography
?.replace(/<[^>]*>/, '')
.replace('Read more on Last.fm</a>', '')}
placement="bottomStart"
>
<span>
{artistInfo.biography
?.replace(/<[^>]*>/, '')
.replace('Read more on Last.fm</a>', '') !== ''
? `${artistInfo.biography
?.replace(/<[^>]*>/, '')
.replace('Read more on Last.fm</a>', '')}`
: 'No artist biography found'}
</span>
</CustomTooltip>
<div style={{ marginTop: '10px' }}>
<ButtonToolbar>
<PlayButton appearance="primary" size="lg" circle />
<PlayShuffleButton />
<PlayAppendButton />
<PlayShuffleAppendButton />
<EditButton style={{ marginRight: '10px' }} />
<Whisper
placement="bottomStart"
trigger="click"
speaker={
<Popover style={{ width: '400px' }}>
<TagGroup>
{artistInfo.similarArtist?.map((artist: any) => (
<Tag key={artist.id}>
<TagLink
onClick={() =>
history.push(`/library/artist/${artist.id}`)
}
>
{artist.name}
</TagLink>
</Tag>
))}
</TagGroup>
</Popover>
}
>
<Button>Related Artists</Button>
</Whisper>
</ButtonToolbar>
</div>
</>
}
searchQuery={searchQuery}
handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')}
showSearchBar
showViewTypeButtons
viewTypeSetting="album"
handleListClick={() => setViewType('list')}
handleGridClick={() => setViewType('grid')}
/>
}
>
<>
{viewType === 'list' && (
<ListViewType
data={searchQuery !== '' ? filteredData : data.album}
tableColumns={settings.getSync('albumListColumns')}
handleRowClick={handleRowClick}
handleRowDoubleClick={handleRowDoubleClick}
virtualized
rowHeight={Number(settings.getSync('albumListRowHeight'))}
fontSize={Number(settings.getSync('albumListFontSize'))}
cacheImages={{
enabled: settings.getSync('cacheImages'),
cacheType: 'album',
}}
/>
)}
{viewType === 'grid' && (
<GridViewType
data={searchQuery === '' ? data.album : filteredData}
cardTitle={{
prefix: '/library/album',
property: 'name',
urlProperty: 'albumId',
}}
cardSubtitle={{
property: 'songCount',
unit: ' tracks',
}}
playClick={{ type: 'album', idProperty: 'id' }}
size="150px"
cacheType="album"
/>
)}
</>
</GenericPage>
);
};
export default ArtistView;

3
src/components/library/LibraryView.tsx

@ -28,7 +28,7 @@ const LibraryView = () => {
const [sortBy, setSortBy] = useState(null); const [sortBy, setSortBy] = useState(null);
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const [viewType, setViewType] = useState(settings.getSync('viewType')); const [viewType, setViewType] = useState(settings.getSync('albumViewType'));
const { isLoading: isLoadingArtists, data: artists }: any = useQuery( const { isLoading: isLoadingArtists, data: artists }: any = useQuery(
'artists', 'artists',
getArtists, getArtists,
@ -105,6 +105,7 @@ const LibraryView = () => {
handleSearch={(e: any) => setSearchQuery(e)} handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')} clearSearchQuery={() => setSearchQuery('')}
showViewTypeButtons={currentPage === 'albums'} showViewTypeButtons={currentPage === 'albums'}
viewTypeSetting="album"
showSearchBar showSearchBar
handleListClick={() => setViewType('list')} handleListClick={() => setViewType('list')}
handleGridClick={() => setViewType('grid')} handleGridClick={() => setViewType('grid')}

11
src/components/library/styled.tsx

@ -0,0 +1,11 @@
import styled from 'styled-components';
export const TagLink = styled.a`
color: #e9ebf0;
cursor: pointer;
&:hover {
color: #e9ebf0;
text-decoration: none;
}
`;

172
src/components/player/PlayerBar.tsx

@ -278,9 +278,19 @@ const PlayerBar = () => {
playQueue.entry[playQueue.currentIndex]?.title || playQueue.entry[playQueue.currentIndex]?.title ||
'Unknown title' 'Unknown title'
} }
delay={800} placement="topStart"
> >
<LinkButton tabIndex={0}> <LinkButton
tabIndex={0}
onClick={() =>
history.push(
`/library/album/${
playQueue.entry[playQueue.currentIndex]
?.albumId
}`
)
}
>
{playQueue.entry[playQueue.currentIndex]?.title || {playQueue.entry[playQueue.currentIndex]?.title ||
'Unknown title'} 'Unknown title'}
</LinkButton> </LinkButton>
@ -301,9 +311,20 @@ const PlayerBar = () => {
playQueue.entry[playQueue.currentIndex]?.artist || playQueue.entry[playQueue.currentIndex]?.artist ||
'Unknown artist' 'Unknown artist'
} }
delay={800} placement="topStart"
> >
<LinkButton tabIndex={0} subtitle="true"> <LinkButton
tabIndex={0}
subtitle="true"
onClick={() => {
history.push(
`/library/artist/${
playQueue.entry[playQueue.currentIndex]
?.artistId
}`
);
}}
>
{playQueue.entry[playQueue.currentIndex]?.artist || {playQueue.entry[playQueue.currentIndex]?.artist ||
'Unknown artist'} 'Unknown artist'}
</LinkButton> </LinkButton>
@ -321,77 +342,88 @@ const PlayerBar = () => {
> >
<PlayerColumn center height="45px"> <PlayerColumn center height="45px">
{/* Seek Backward Button */} {/* Seek Backward Button */}
<PlayerControlIcon <CustomTooltip text="Seek backward">
tabIndex={0} <PlayerControlIcon
icon="backward" tabIndex={0}
size="lg" icon="backward"
fixedWidth size="lg"
onClick={handleClickBackward} fixedWidth
onKeyDown={(e: any) => { onClick={handleClickBackward}
if (e.keyCode === keyCodes.SPACEBAR) { onKeyDown={(e: any) => {
handleClickBackward(); if (e.keyCode === keyCodes.SPACEBAR) {
} handleClickBackward();
}} }
/> }}
/>
</CustomTooltip>
{/* Previous Song Button */} {/* Previous Song Button */}
<PlayerControlIcon <CustomTooltip text="Previous track">
tabIndex={0} <PlayerControlIcon
icon="step-backward" tabIndex={0}
size="lg" icon="step-backward"
fixedWidth size="lg"
onClick={handleClickPrevious} fixedWidth
onKeyDown={(e: any) => { onClick={handleClickPrevious}
if (e.keyCode === keyCodes.SPACEBAR) { onKeyDown={(e: any) => {
handleClickPrevious(); if (e.keyCode === keyCodes.SPACEBAR) {
} handleClickPrevious();
}} }
/> }}
/>
</CustomTooltip>
{/* Play/Pause Button */} {/* Play/Pause Button */}
<PlayerControlIcon <CustomTooltip text="Play/Pause">
tabIndex={0} <PlayerControlIcon
icon={ tabIndex={0}
playQueue.status === 'PLAYING' icon={
? 'pause-circle' playQueue.status === 'PLAYING'
: 'play-circle' ? 'pause-circle'
} : 'play-circle'
spin={
playQueue.currentSeekable <= playQueue.currentSeek &&
playQueue.status === 'PLAYING'
}
size="3x"
onClick={handleClickPlayPause}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickPlayPause();
} }
}} spin={
/> playQueue.currentSeekable <= playQueue.currentSeek &&
{/* Next Song Button */} playQueue.status === 'PLAYING'
<PlayerControlIcon
tabIndex={0}
icon="step-forward"
size="lg"
fixedWidth
onClick={handleClickNext}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickNext();
} }
}} size="3x"
/> onClick={handleClickPlayPause}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickPlayPause();
}
}}
/>
</CustomTooltip>
{/* Next Song Button */}
<CustomTooltip text="Next track">
<PlayerControlIcon
tabIndex={0}
icon="step-forward"
size="lg"
fixedWidth
onClick={handleClickNext}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickNext();
}
}}
/>
</CustomTooltip>
{/* Seek Forward Button */} {/* Seek Forward Button */}
<PlayerControlIcon <CustomTooltip text="Seek forward">
tabIndex={0} <PlayerControlIcon
icon="forward" tabIndex={0}
size="lg" icon="forward"
fixedWidth size="lg"
onClick={handleClickForward} fixedWidth
onKeyDown={(e: any) => { onClick={handleClickForward}
if (e.keyCode === keyCodes.SPACEBAR) { onKeyDown={(e: any) => {
handleClickForward(); if (e.keyCode === keyCodes.SPACEBAR) {
} handleClickForward();
}} }
/> }}
/>
</CustomTooltip>
</PlayerColumn> </PlayerColumn>
<PlayerColumn center height="35px"> <PlayerColumn center height="35px">
<FlexboxGrid <FlexboxGrid

11
src/components/player/styled.tsx

@ -28,8 +28,8 @@ export const PlayerColumn = styled.div<{
export const PlayerControlIcon = styled(Icon)` export const PlayerControlIcon = styled(Icon)`
color: #b3b3b3; color: #b3b3b3;
padding-left: 5px; padding-left: 10px;
padding-right: 5px; padding-right: 10px;
&:hover { &:hover {
color: #ffffff; color: #ffffff;
} }
@ -55,6 +55,13 @@ export const LinkButton = styled.a<{ subtitle?: string }>`
: props.theme.titleText}; : props.theme.titleText};
cursor: pointer; cursor: pointer;
} }
&:active {
color: ${(props) =>
props.subtitle === 'true'
? props.theme.subtitleText
: props.theme.titleText};
}
`; `;
export const CustomSlider = styled(Slider)` export const CustomSlider = styled(Slider)`

3
src/components/playlist/PlaylistList.tsx

@ -51,7 +51,7 @@ const PlaylistList = () => {
const history = useHistory(); const history = useHistory();
const [sortBy, setSortBy] = useState(''); const [sortBy, setSortBy] = useState('');
const [viewType, setViewType] = useState( const [viewType, setViewType] = useState(
settings.getSync('viewType') || 'list' settings.getSync('playlistViewType') || 'list'
); );
const { isLoading, isError, data: playlists, error }: any = useQuery( const { isLoading, isError, data: playlists, error }: any = useQuery(
['playlists', sortBy], ['playlists', sortBy],
@ -85,6 +85,7 @@ const PlaylistList = () => {
handleSearch={(e: any) => setSearchQuery(e)} handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')} clearSearchQuery={() => setSearchQuery('')}
showViewTypeButtons showViewTypeButtons
viewTypeSetting="playlist"
showSearchBar showSearchBar
handleListClick={() => setViewType('list')} handleListClick={() => setViewType('list')}
handleGridClick={() => setViewType('grid')} handleGridClick={() => setViewType('grid')}

2
src/components/playlist/PlaylistView.tsx

@ -135,7 +135,7 @@ const PlaylistView = () => {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{data.comment} {data.comment ? data.comment : ' '}
</div> </div>
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
<ButtonToolbar> <ButtonToolbar>

4
src/components/scrollingmenu/ScrollingMenu.tsx

@ -59,8 +59,8 @@ const ScrollingMenu = ({
subtitle={item[cardSubtitle.property]} subtitle={item[cardSubtitle.property]}
key={item.id} key={item.id}
coverArt={item.image} coverArt={item.image}
url={`${cardTitle.prefix}/${item[cardTitle.property]}`} url={`library/${cardTitle.prefix}/${item.id}`}
subUrl={`${cardSubtitle.prefix}/${item[cardSubtitle.property]}`} subUrl={`library/${cardSubtitle.prefix}/${item.artistId}`}
playClick={{ type: 'album', id: item.id }} playClick={{ type: 'album', id: item.id }}
hasHoverButtons hasHoverButtons
size={cardSize} size={cardSize}

196
src/components/settings/Config.tsx

@ -17,7 +17,7 @@ import GenericPageHeader from '../layout/GenericPageHeader';
const fsUtils = require('nodejs-fs-utils'); const fsUtils = require('nodejs-fs-utils');
const columnList = [ const songColumnList = [
{ {
label: '#', label: '#',
value: { value: {
@ -119,7 +119,7 @@ const columnList = [
}, },
]; ];
const columnPicker = [ const songColumnPicker = [
{ {
label: '#', label: '#',
}, },
@ -149,14 +149,135 @@ const columnPicker = [
}, },
]; ];
const albumColumnList = [
{
label: '#',
value: {
id: '#',
dataKey: 'index',
alignment: 'center',
resizable: true,
width: 40,
label: '#',
},
},
{
label: 'Artist',
value: {
id: 'Artist',
dataKey: 'artist',
alignment: 'left',
resizable: true,
width: 300,
label: 'Artist',
},
},
{
label: 'Created',
value: {
id: 'Created',
dataKey: 'created',
alignment: 'left',
resizable: true,
width: 100,
label: 'Created',
},
},
{
label: 'Duration',
value: {
id: 'Duration',
dataKey: 'duration',
alignment: 'center',
resizable: true,
width: 65,
label: 'Duration',
},
},
{
label: 'Genre',
value: {
id: 'Genre',
dataKey: 'genre',
alignment: 'center',
resizable: true,
width: 70,
label: 'Genre',
},
},
{
label: 'Track Count',
value: {
id: 'Tracks',
dataKey: 'songCount',
alignment: 'center',
resizable: true,
width: 70,
label: 'Track Count',
},
},
{
label: 'Title',
value: {
id: 'Title',
dataKey: 'name',
alignment: 'left',
resizable: true,
width: 350,
label: 'Title',
},
},
{
label: 'Title (Combined)',
value: {
id: 'Title',
dataKey: 'combinedtitle',
alignment: 'left',
resizable: true,
width: 350,
label: 'Title (Combined)',
},
},
];
const albumColumnPicker = [
{
label: '#',
},
{
label: 'Artist',
},
{
label: 'Created',
},
{
label: 'Duration',
},
{
label: 'Genre',
},
{
label: 'Title',
},
{
label: 'Title (Combined)',
},
{
label: 'Track Count',
},
];
const Config = () => { const Config = () => {
const [isScanning, setIsScanning] = useState(false); const [isScanning, setIsScanning] = useState(false);
const [scanProgress, setScanProgress] = useState(0); const [scanProgress, setScanProgress] = useState(0);
const [imgCacheSize, setImgCacheSize] = useState(0); const [imgCacheSize, setImgCacheSize] = useState(0);
const [songCacheSize, setSongCacheSize] = useState(0); const [songCacheSize, setSongCacheSize] = useState(0);
const cols: any = settings.getSync('songListColumns'); const songCols: any = settings.getSync('songListColumns');
const currentColumns = cols?.map((column: any) => column.label) || []; const albumCols: any = settings.getSync('albumListColumns');
const currentSongColumns = songCols?.map((column: any) => column.label) || [];
const currentAlbumColumns =
albumCols?.map((column: any) => column.label) || [];
useEffect(() => { useEffect(() => {
// Retrieve cache sizes on render // Retrieve cache sizes on render
@ -270,16 +391,18 @@ const Config = () => {
columns will be displayed in the order selected below. columns will be displayed in the order selected below.
</div> </div>
<div style={{ width: '100%', marginTop: '20px' }}> <div style={{ width: '100%', marginTop: '20px' }}>
<strong>Song List</strong>
<br />
<TagPicker <TagPicker
data={columnPicker} data={songColumnPicker}
defaultValue={currentColumns} defaultValue={currentSongColumns}
style={{ width: '500px' }} style={{ width: '500px' }}
onChange={(e) => { onChange={(e) => {
const columns: any[] = []; const columns: any[] = [];
if (e) { if (e) {
e.map((selected: string) => { e.map((selected: string) => {
const selectedColumn = columnList.find( const selectedColumn = songColumnList.find(
(column) => column.label === selected (column) => column.label === selected
); );
if (selectedColumn) { if (selectedColumn) {
@ -324,6 +447,65 @@ const Config = () => {
/> />
</div> </div>
</div> </div>
<div style={{ width: '100%', marginTop: '20px' }}>
<strong>Album List</strong>
<br />
<TagPicker
data={albumColumnPicker}
defaultValue={currentAlbumColumns}
style={{ width: '500px' }}
onChange={(e) => {
const columns: any[] = [];
if (e) {
e.map((selected: string) => {
const selectedColumn = albumColumnList.find(
(column) => column.label === selected
);
if (selectedColumn) {
return columns.push(selectedColumn.value);
}
return null;
});
}
settings.setSync('albumListColumns', columns);
}}
labelKey="label"
valueKey="label"
/>
<div style={{ marginTop: '20px' }}>
<ControlLabel>Row height</ControlLabel>
<InputNumber
defaultValue={
String(settings.getSync('albumListRowHeight')) || '0'
}
step={1}
min={30}
max={100}
onChange={(e) => {
settings.setSync('albumListRowHeight', e);
}}
style={{ width: '150px' }}
/>
</div>
<div style={{ marginTop: '20px' }}>
<ControlLabel>Font Size</ControlLabel>
<InputNumber
defaultValue={
String(settings.getSync('albumListFontSize')) || '0'
}
step={0.5}
min={1}
max={100}
onChange={(e) => {
settings.setSync('albumListFontSize', e);
}}
style={{ width: '150px' }}
/>
</div>
</div>
</ConfigPanel> </ConfigPanel>
<ConfigPanel header="Cache" bordered> <ConfigPanel header="Cache" bordered>
<div style={{ overflow: 'auto' }}> <div style={{ overflow: 'auto' }}>

56
src/components/settings/Login.tsx

@ -78,6 +78,14 @@ const Login = () => {
settings.setSync('fadeDuration', '5.0'); settings.setSync('fadeDuration', '5.0');
} }
if (!settings.hasSync('playlistViewType')) {
settings.setSync('playlistViewType', 'list');
}
if (!settings.hasSync('albumViewType')) {
settings.setSync('albumViewType', 'list');
}
if (!settings.hasSync('songListFontSize')) { if (!settings.hasSync('songListFontSize')) {
settings.setSync('songListFontSize', '14'); settings.setSync('songListFontSize', '14');
} }
@ -122,6 +130,54 @@ const Login = () => {
}, },
]); ]);
} }
if (!settings.hasSync('albumListFontSize')) {
settings.setSync('albumListFontSize', '14');
}
if (!settings.hasSync('albumListRowHeight')) {
settings.setSync('albumListRowHeight', '60.0');
}
if (!settings.hasSync('albumListColumns')) {
settings.setSync('albumListColumns', [
{
id: '#',
dataKey: 'index',
alignment: 'center',
resizable: true,
width: 40,
label: '#',
},
{
id: 'Title',
dataKey: 'combinedtitle',
alignment: 'left',
resizable: true,
width: 350,
label: 'Title (Combined)',
},
{
label: 'Track Count',
value: {
id: 'Tracks',
dataKey: 'songCount',
alignment: 'center',
resizable: true,
width: 70,
label: 'Track Count',
},
},
{
id: 'Duration',
dataKey: 'duration',
alignment: 'center',
resizable: true,
width: 70,
label: 'Duration',
},
]);
}
window.location.reload(); window.location.reload();
}; };

4
src/components/shared/CustomTooltip.tsx

@ -3,10 +3,10 @@ import { Tooltip, Whisper } from 'rsuite';
export const tooltip = (text: string) => <Tooltip>{text}</Tooltip>; export const tooltip = (text: string) => <Tooltip>{text}</Tooltip>;
const CustomTooltip = ({ children, text, delay }: any) => { const CustomTooltip = ({ children, text, delay, ...rest }: any) => {
return ( return (
<Whisper <Whisper
placement="top" placement={rest.placement || 'top'}
trigger="hover" trigger="hover"
delay={delay || 300} delay={delay || 300}
speaker={tooltip(text)} speaker={tooltip(text)}

62
src/components/starred/StarredView.tsx

@ -19,49 +19,11 @@ import Loader from '../loader/Loader';
import ListViewType from '../viewtypes/ListViewType'; import ListViewType from '../viewtypes/ListViewType';
import GridViewType from '../viewtypes/GridViewType'; import GridViewType from '../viewtypes/GridViewType';
const albumTableColumns = [
{
id: '#',
dataKey: 'index',
alignment: 'center',
width: 70,
},
{
id: 'Title',
dataKey: 'name',
alignment: 'left',
resizable: true,
width: 350,
},
{
id: 'Artist',
dataKey: 'artist',
alignment: 'center',
resizable: true,
width: 300,
},
{
id: 'Tracks',
dataKey: 'songCount',
alignment: 'center',
resizable: true,
width: 300,
},
{
id: 'Duration',
dataKey: 'duration',
alignment: 'center',
resizable: true,
width: 70,
},
];
const StarredView = () => { const StarredView = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [currentPage, setCurrentPage] = useState('Tracks'); const [currentPage, setCurrentPage] = useState('Tracks');
const [viewType, setViewType] = useState( const [viewType, setViewType] = useState(
settings.getSync('viewType') || 'list' settings.getSync('albumViewType') || 'list'
); );
const { isLoading, isError, data, error }: any = useQuery( const { isLoading, isError, data, error }: any = useQuery(
'starred', 'starred',
@ -75,7 +37,7 @@ const StarredView = () => {
: currentPage === 'Albums' : currentPage === 'Albums'
? data?.album ? data?.album
: data?.song, : data?.song,
['title', 'artist', 'album'] ['title', 'artist', 'album', 'name', 'genre']
); );
let timeout: any = null; let timeout: any = null;
@ -145,6 +107,7 @@ const StarredView = () => {
handleSearch={(e: any) => setSearchQuery(e)} handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')} clearSearchQuery={() => setSearchQuery('')}
showViewTypeButtons={currentPage !== 'Tracks'} showViewTypeButtons={currentPage !== 'Tracks'}
viewTypeSetting="song"
showSearchBar showSearchBar
handleListClick={() => setViewType('list')} handleListClick={() => setViewType('list')}
handleGridClick={() => setViewType('grid')} handleGridClick={() => setViewType('grid')}
@ -171,8 +134,14 @@ const StarredView = () => {
{viewType === 'list' && ( {viewType === 'list' && (
<ListViewType <ListViewType
data={searchQuery !== '' ? filteredData : data.album} data={searchQuery !== '' ? filteredData : data.album}
tableColumns={albumTableColumns} tableColumns={settings.getSync('albumListColumns')}
rowHeight={Number(settings.getSync('albumListRowHeight'))}
fontSize={settings.getSync('albumListFontSize')}
handleRowClick={handleRowClick} handleRowClick={handleRowClick}
cacheImages={{
enabled: settings.getSync('cacheImages'),
cacheType: 'album',
}}
virtualized virtualized
/> />
)} )}
@ -180,11 +149,16 @@ const StarredView = () => {
<GridViewType <GridViewType
data={searchQuery === '' ? data.album : filteredData} data={searchQuery === '' ? data.album : filteredData}
cardTitle={{ cardTitle={{
prefix: 'playlist', prefix: '/library/album',
property: 'name', property: 'name',
urlProperty: 'id', urlProperty: 'albumId',
}}
cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
unit: '',
}} }}
cardSubtitle={{ prefix: 'playlist', property: 'songCount' }}
playClick={{ type: 'album', idProperty: 'id' }} playClick={{ type: 'album', idProperty: 'id' }}
size="150px" size="150px"
cacheType="album" cacheType="album"

3
src/components/viewtypes/ListViewType.tsx

@ -251,6 +251,7 @@ const ListViewType = ({
onRowContextMenu={(e) => { onRowContextMenu={(e) => {
console.log(e); console.log(e);
}} }}
hover={false}
affixHeader affixHeader
affixHorizontalScrollbar affixHorizontalScrollbar
shouldUpdateScroll={false} shouldUpdateScroll={false}
@ -479,7 +480,7 @@ const ListViewType = ({
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
{rowData.title} {rowData.title || rowData.name}
</span> </span>
</Row> </Row>
<Row <Row

14
src/components/viewtypes/ViewTypeButtons.tsx

@ -2,7 +2,11 @@ import React from 'react';
import { ButtonToolbar, ButtonGroup, IconButton, Icon } from 'rsuite'; import { ButtonToolbar, ButtonGroup, IconButton, Icon } from 'rsuite';
import settings from 'electron-settings'; import settings from 'electron-settings';
const ViewTypeButtons = ({ handleListClick, handleGridClick }: any) => { const ViewTypeButtons = ({
handleListClick,
handleGridClick,
viewTypeSetting,
}: any) => {
return ( return (
<ButtonToolbar> <ButtonToolbar>
<ButtonGroup> <ButtonGroup>
@ -11,8 +15,8 @@ const ViewTypeButtons = ({ handleListClick, handleGridClick }: any) => {
appearance="subtle" appearance="subtle"
onClick={async () => { onClick={async () => {
handleListClick(); handleListClick();
localStorage.setItem('viewType', 'list'); localStorage.setItem(`${viewTypeSetting}ViewType`, 'list');
settings.setSync('viewType', 'list'); settings.setSync(`${viewTypeSetting}ViewType`, 'list');
}} }}
/> />
<IconButton <IconButton
@ -20,8 +24,8 @@ const ViewTypeButtons = ({ handleListClick, handleGridClick }: any) => {
appearance="subtle" appearance="subtle"
onClick={async () => { onClick={async () => {
handleGridClick(); handleGridClick();
localStorage.setItem('viewType', 'grid'); localStorage.setItem(`${viewTypeSetting}ViewType`, 'grid');
settings.setSync('viewType', 'grid'); settings.setSync(`${viewTypeSetting}ViewType`, 'grid');
}} }}
/> />
</ButtonGroup> </ButtonGroup>

Loading…
Cancel
Save