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

55
src/api/api.ts

@ -139,6 +139,7 @@ export const getStarred = async () => {
...data.starred2,
album: (data.starred2.album || []).map((entry: any, index: any) => ({
...entry,
albumId: entry.id,
image: getCoverArtUrl(entry),
index,
})),
@ -161,6 +162,7 @@ export const getAlbums = async (options: any, coverArtSize = 150) => {
...data.albumList2,
album: (data.albumList2.album || []).map((entry: any, index: any) => ({
...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, coverArtSize),
starred: entry.starred || '',
index,
@ -176,6 +178,7 @@ export const getAlbumsDirect = async (options: any, coverArtSize = 150) => {
const albums = (data.albumList2.album || []).map(
(entry: any, index: any) => ({
...entry,
albumId: entry.id,
image: getCoverArtUrl(entry, coverArtSize),
starred: entry.starred || '',
index,
@ -194,6 +197,7 @@ export const getAlbum = async (id: string, coverArtSize = 150) => {
return {
...data.album,
image: getCoverArtUrl(data.album, coverArtSize),
song: (data.album.song || []).map((entry: any, index: any) => ({
...entry,
streamUrl: getStreamUrl(entry.id),
@ -236,6 +240,39 @@ export const getArtists = async () => {
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 () => {
const { data } = await api.get(`/startScan`);
const scanStatus = data?.scanStatus;
@ -273,3 +310,21 @@ export const unstar = async (id: string, type: string) => {
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"
data={recentAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }}
cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px"
/>
@ -76,7 +80,11 @@ const Dashboard = () => {
title="Recently Added"
data={newestAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }}
cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px"
/>
@ -84,7 +92,11 @@ const Dashboard = () => {
title="Random"
data={randomAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }}
cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px"
/>
@ -92,7 +104,11 @@ const Dashboard = () => {
title="Most Played"
data={frequentAlbums.album}
cardTitle={{ prefix: 'album', property: 'name' }}
cardSubtitle={{ prefix: 'album', property: 'artist' }}
cardSubtitle={{
prefix: 'artist',
property: 'artist',
urlProperty: 'artistId',
}}
cardSize="175px"
/>
</>

30
src/components/layout/GenericPageHeader.tsx

@ -4,6 +4,7 @@ import ViewTypeButtons from '../viewtypes/ViewTypeButtons';
const GenericPageHeader = ({
image,
imageHeight,
title,
subtitle,
sidetitle,
@ -15,12 +16,18 @@ const GenericPageHeader = ({
showSearchBar,
handleListClick,
handleGridClick,
viewTypeSetting,
}: any) => {
return (
<>
{image && (
<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>
)}
@ -66,14 +73,18 @@ const GenericPageHeader = ({
<span style={{ display: 'inline-block' }}>
<InputGroup inside>
<Input
id="local-search-input"
size="md"
value={searchQuery}
placeholder="Search..."
onChange={handleSearch}
/>
{searchQuery !== '' && (
<InputGroup.Button appearance="subtle">
<Icon icon="close" onClick={clearSearchQuery} />
<InputGroup.Button
appearance="subtle"
onClick={clearSearchQuery}
>
<Icon icon="close" />
</InputGroup.Button>
)}
</InputGroup>
@ -88,7 +99,17 @@ const GenericPageHeader = ({
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' }}>
{subsidetitle && (
<span style={{ display: 'inline-block' }}>{subsidetitle}</span>
@ -98,6 +119,7 @@ const GenericPageHeader = ({
<ViewTypeButtons
handleListClick={handleListClick}
handleGridClick={handleGridClick}
viewTypeSetting={viewTypeSetting}
/>
</span>
)}

18
src/components/library/AlbumList.tsx

@ -1,5 +1,7 @@
import React from 'react';
import settings from 'electron-settings';
import GridViewType from '../viewtypes/GridViewType';
import ListViewType from '../viewtypes/ListViewType';
const AlbumList = ({ data, viewType }: any) => {
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 <></>;
};

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 [data, setData] = useState<any[]>([]);
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(
'artists',
getArtists,
@ -105,6 +105,7 @@ const LibraryView = () => {
handleSearch={(e: any) => setSearchQuery(e)}
clearSearchQuery={() => setSearchQuery('')}
showViewTypeButtons={currentPage === 'albums'}
viewTypeSetting="album"
showSearchBar
handleListClick={() => setViewType('list')}
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 ||
'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 ||
'Unknown title'}
</LinkButton>
@ -301,9 +311,20 @@ const PlayerBar = () => {
playQueue.entry[playQueue.currentIndex]?.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 ||
'Unknown artist'}
</LinkButton>
@ -321,77 +342,88 @@ const PlayerBar = () => {
>
<PlayerColumn center height="45px">
{/* Seek Backward Button */}
<PlayerControlIcon
tabIndex={0}
icon="backward"
size="lg"
fixedWidth
onClick={handleClickBackward}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickBackward();
}
}}
/>
<CustomTooltip text="Seek backward">
<PlayerControlIcon
tabIndex={0}
icon="backward"
size="lg"
fixedWidth
onClick={handleClickBackward}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickBackward();
}
}}
/>
</CustomTooltip>
{/* Previous Song Button */}
<PlayerControlIcon
tabIndex={0}
icon="step-backward"
size="lg"
fixedWidth
onClick={handleClickPrevious}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickPrevious();
}
}}
/>
<CustomTooltip text="Previous track">
<PlayerControlIcon
tabIndex={0}
icon="step-backward"
size="lg"
fixedWidth
onClick={handleClickPrevious}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickPrevious();
}
}}
/>
</CustomTooltip>
{/* Play/Pause Button */}
<PlayerControlIcon
tabIndex={0}
icon={
playQueue.status === 'PLAYING'
? '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();
<CustomTooltip text="Play/Pause">
<PlayerControlIcon
tabIndex={0}
icon={
playQueue.status === 'PLAYING'
? 'pause-circle'
: 'play-circle'
}
}}
/>
{/* Next Song Button */}
<PlayerControlIcon
tabIndex={0}
icon="step-forward"
size="lg"
fixedWidth
onClick={handleClickNext}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickNext();
spin={
playQueue.currentSeekable <= playQueue.currentSeek &&
playQueue.status === 'PLAYING'
}
}}
/>
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 */}
<PlayerControlIcon
tabIndex={0}
icon="forward"
size="lg"
fixedWidth
onClick={handleClickForward}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickForward();
}
}}
/>
<CustomTooltip text="Seek forward">
<PlayerControlIcon
tabIndex={0}
icon="forward"
size="lg"
fixedWidth
onClick={handleClickForward}
onKeyDown={(e: any) => {
if (e.keyCode === keyCodes.SPACEBAR) {
handleClickForward();
}
}}
/>
</CustomTooltip>
</PlayerColumn>
<PlayerColumn center height="35px">
<FlexboxGrid

11
src/components/player/styled.tsx

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

3
src/components/playlist/PlaylistList.tsx

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

2
src/components/playlist/PlaylistView.tsx

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

4
src/components/scrollingmenu/ScrollingMenu.tsx

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

196
src/components/settings/Config.tsx

@ -17,7 +17,7 @@ import GenericPageHeader from '../layout/GenericPageHeader';
const fsUtils = require('nodejs-fs-utils');
const columnList = [
const songColumnList = [
{
label: '#',
value: {
@ -119,7 +119,7 @@ const columnList = [
},
];
const columnPicker = [
const songColumnPicker = [
{
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 [isScanning, setIsScanning] = useState(false);
const [scanProgress, setScanProgress] = useState(0);
const [imgCacheSize, setImgCacheSize] = useState(0);
const [songCacheSize, setSongCacheSize] = useState(0);
const cols: any = settings.getSync('songListColumns');
const currentColumns = cols?.map((column: any) => column.label) || [];
const songCols: any = settings.getSync('songListColumns');
const albumCols: any = settings.getSync('albumListColumns');
const currentSongColumns = songCols?.map((column: any) => column.label) || [];
const currentAlbumColumns =
albumCols?.map((column: any) => column.label) || [];
useEffect(() => {
// Retrieve cache sizes on render
@ -270,16 +391,18 @@ const Config = () => {
columns will be displayed in the order selected below.
</div>
<div style={{ width: '100%', marginTop: '20px' }}>
<strong>Song List</strong>
<br />
<TagPicker
data={columnPicker}
defaultValue={currentColumns}
data={songColumnPicker}
defaultValue={currentSongColumns}
style={{ width: '500px' }}
onChange={(e) => {
const columns: any[] = [];
if (e) {
e.map((selected: string) => {
const selectedColumn = columnList.find(
const selectedColumn = songColumnList.find(
(column) => column.label === selected
);
if (selectedColumn) {
@ -324,6 +447,65 @@ const Config = () => {
/>
</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 header="Cache" bordered>
<div style={{ overflow: 'auto' }}>

56
src/components/settings/Login.tsx

@ -78,6 +78,14 @@ const Login = () => {
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')) {
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();
};

4
src/components/shared/CustomTooltip.tsx

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

62
src/components/starred/StarredView.tsx

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

3
src/components/viewtypes/ListViewType.tsx

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

14
src/components/viewtypes/ViewTypeButtons.tsx

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

Loading…
Cancel
Save