Browse Source

Update album page

- Add additional album stats
- Add blurred header image

Add taglink, title color to album

Increase album view image size
master
jeffvli 3 years ago
committed by Jeff
parent
commit
03decf26c8
  1. 4
      src/components/layout/GenericPage.tsx
  2. 37
      src/components/layout/GenericPageHeader.tsx
  3. 1
      src/components/layout/Layout.tsx
  4. 80
      src/components/layout/styled.tsx
  5. 80
      src/components/library/AlbumView.tsx
  6. 13
      src/components/shared/TagLink.tsx
  7. 16
      src/components/shared/styled.ts
  8. 16
      src/shared/utils.ts

4
src/components/layout/GenericPage.tsx

@ -34,7 +34,7 @@ const GenericPage = ({ header, children, hideDivider, ...rest }: any) => {
id="page-container" id="page-container"
$backgroundSrc={ $backgroundSrc={
misc.dynamicBackground misc.dynamicBackground
? !backgroundImage.match('placeholder') ? !backgroundImage?.match('placeholder')
? backgroundImage ? backgroundImage
: undefined : undefined
: undefined : undefined
@ -43,7 +43,7 @@ const GenericPage = ({ header, children, hideDivider, ...rest }: any) => {
<PageHeader <PageHeader
id="page-header" id="page-header"
padding={rest.padding} padding={rest.padding}
style={{ paddingBottom: hideDivider && !rest.padding ? '20px' : '0px' }} style={{ paddingBottom: hideDivider && !rest.padding ? '10px' : '0px' }}
> >
{header} {header}
</PageHeader> </PageHeader>

37
src/components/layout/GenericPageHeader.tsx

@ -10,7 +10,12 @@ import {
StyledInputGroup, StyledInputGroup,
StyledInputGroupButton, StyledInputGroupButton,
} from '../shared/styled'; } from '../shared/styled';
import { CoverArtWrapper, PageHeaderTitle } from './styled'; import {
CoverArtWrapper,
PageHeaderSubtitleWrapper,
PageHeaderTitle,
PageHeaderWrapper,
} from './styled';
import cacheImage from '../shared/cacheImage'; import cacheImage from '../shared/cacheImage';
import CustomTooltip from '../shared/CustomTooltip'; import CustomTooltip from '../shared/CustomTooltip';
@ -31,6 +36,7 @@ const GenericPageHeader = ({
viewTypeSetting, viewTypeSetting,
cacheImages, cacheImages,
showTitleTooltip, showTitleTooltip,
isDark,
}: any) => { }: any) => {
const history = useHistory(); const history = useHistory();
const [openSearch, setOpenSearch] = useState(false); const [openSearch, setOpenSearch] = useState(false);
@ -47,8 +53,8 @@ const GenericPageHeader = ({
<LazyLoadImage <LazyLoadImage
src={image} src={image}
alt="header-img" alt="header-img"
height={imageHeight || '145px'} height={imageHeight || '195px'}
width={imageHeight || '145px'} width={imageHeight || '195px'}
visibleByDefault visibleByDefault
afterLoad={() => { afterLoad={() => {
if (cacheImages.enabled) { if (cacheImages.enabled) {
@ -62,13 +68,7 @@ const GenericPageHeader = ({
</CoverArtWrapper> </CoverArtWrapper>
)} )}
<div <PageHeaderWrapper isDark={isDark} hasImage={image} imageHeight={imageHeight || 195}>
style={{
display: image ? 'inline-block' : 'undefined',
width: image ? 'calc(100% - 160px)' : '100%',
marginLeft: image ? '15px' : '0px',
}}
>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -104,6 +104,7 @@ const GenericPageHeader = ({
<Icon icon="search" /> <Icon icon="search" />
</InputGroup.Addon> </InputGroup.Addon>
<StyledInput <StyledInput
opacity={0.6}
id="local-search-input" id="local-search-input"
value={searchQuery} value={searchQuery}
onChange={handleSearch} onChange={handleSearch}
@ -160,18 +161,8 @@ const GenericPageHeader = ({
height: '50%', height: '50%',
}} }}
> >
<span <PageHeaderSubtitleWrapper>{subtitle}</PageHeaderSubtitleWrapper>
style={{ <span style={{ alignSelf: 'flex-end' }}>
alignSelf: 'center',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
width: '60%',
}}
>
{subtitle}
</span>
<span style={{ alignSelf: 'center' }}>
{subsidetitle && <span style={{ display: 'inline-block' }}>{subsidetitle}</span>} {subsidetitle && <span style={{ display: 'inline-block' }}>{subsidetitle}</span>}
{showViewTypeButtons && ( {showViewTypeButtons && (
<span style={{ display: 'inline-block' }}> <span style={{ display: 'inline-block' }}>
@ -184,7 +175,7 @@ const GenericPageHeader = ({
)} )}
</span> </span>
</div> </div>
</div> </PageHeaderWrapper>
</> </>
); );
}; };

1
src/components/layout/Layout.tsx

@ -107,6 +107,7 @@ const Layout = ({ footer, children, disableSidebar, font }: any) => {
<FlexboxGrid <FlexboxGrid
justify="space-between" justify="space-between"
style={{ style={{
zIndex: 2,
padding: '0 10px 0 10px', padding: '0 10px 0 10px',
margin: '10px 5px 5px 5px', margin: '10px 5px 5px 5px',
}} }}

80
src/components/layout/styled.tsx

@ -156,6 +156,7 @@ export const PageHeader = styled(Header)<{ padding?: string }>`
export const PageContent = styled(Content)<{ padding?: string }>` export const PageContent = styled(Content)<{ padding?: string }>`
position: relative; position: relative;
padding: ${(props) => (props.padding ? props.padding : '10px')}; padding: ${(props) => (props.padding ? props.padding : '10px')};
z-index: 1;
`; `;
// Sidebar.tsx // Sidebar.tsx
@ -205,5 +206,82 @@ export const PageHeaderTitle = styled.h1`
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
font-size: ${(props) => props.theme.fonts.size.pageTitle}; font-size: 4vw;
@media screen and (min-width: 1280px) {
font-size: 48px;
}
`;
export const PageHeaderWrapper = styled.div<{ $hasImage: boolean; $imageHeight: string }>`
display: ${(props) => (props.$hasImage ? 'inline-block' : 'undefined')};
width: ${(props) => (props.$hasImage ? `calc(100% - ${props.$imageHeight + 15}px)` : '100%')};
margin-left: ${(props) => (props.$hasImage ? '15px' : '0px')};
vertical-align: top;
color: ${(props) => (props.$hasImage ? '#D8D8D8' : props.theme.colors.layout.page.color)};
`;
export const PageHeaderSubtitleWrapper = styled.span`
align-self: center;
width: 70%;
font-size: 14px;
`;
export const PageHeaderSubtitleDataLine = styled.div<{ $top?: boolean }>`
margin-top: ${(props) => (props.$top ? '0px' : '10px')};
`;
export const FlatBackground = styled.div<{ $expanded: boolean; $color: string }>`
background: ${(props) => props.$color};
top: 32px;
left: ${(props) => (props.$expanded ? '165px' : '56px')};
height: 200px;
position: absolute;
width: ${(props) => (props.$expanded ? `calc(100% - 165px)` : 'calc(100% - 56px)')};
z-index: 1;
user-select: none;
pointer-events: none;
`;
export const BlurredBackgroundWrapper = styled.div<{ $expanded: boolean }>`
clip: rect(0, auto, auto, 0);
-webkit-clip-path: inset(0 0);
clip-path: inset(0 0);
position: absolute;
left: ${(props) => (props.$expanded ? '165px' : '56px')};
width: ${(props) => (props.$expanded ? `calc(100% - 165px)` : 'calc(100% - 56px)')};
top: 32px;
z-index: 1;
display: block;
background: #0b0908;
`;
export const BlurredBackground = styled.img<{ $expanded: boolean; $image: string }>`
background-image: ${(props) => `url(${props.$image})`};
background-position: center 30%;
background-size: cover;
filter: blur(10px) brightness(0.4);
outline: none !important;
border: none !important;
margin: 0px !important;
padding: 0px !important;
width: 100%;
height: 202px;
z-index: -1;
user-select: none;
pointer-events: none;
display: block;
`;
export const GradientBackground = styled.div<{ $expanded: boolean; $color: string }>`
background: ${(props) => `linear-gradient(0deg, transparent 10%, ${props.$color} 100%)`};
top: 32px;
left: ${(props) => (props.$expanded ? '165px' : '56px')};
height: calc(100% - 130px);
position: absolute;
width: ${(props) => (props.$expanded ? `calc(100% - 165px)` : 'calc(100% - 56px)')};
z-index: 1;
user-select: none;
pointer-events: none;
`; `;

80
src/components/library/AlbumView.tsx

@ -34,9 +34,20 @@ import GenericPageHeader from '../layout/GenericPageHeader';
import { setStatus } from '../../redux/playerSlice'; import { setStatus } from '../../redux/playerSlice';
import { addModalPage } from '../../redux/miscSlice'; import { addModalPage } from '../../redux/miscSlice';
import { notifyToast } from '../shared/toast'; import { notifyToast } from '../shared/toast';
import { filterPlayQueue, getPlayedSongsNotification, isCached } from '../../shared/utils'; import {
import { StyledLink } from '../shared/styled'; filterPlayQueue,
formatDate,
formatDuration,
getPlayedSongsNotification,
isCached,
} from '../../shared/utils';
import { StyledTagLink } from '../shared/styled';
import { setActive } from '../../redux/albumSlice'; import { setActive } from '../../redux/albumSlice';
import {
BlurredBackground,
BlurredBackgroundWrapper,
PageHeaderSubtitleDataLine,
} from '../layout/styled';
interface AlbumParams { interface AlbumParams {
id: string; id: string;
@ -171,10 +182,24 @@ const AlbumView = ({ ...rest }: any) => {
} }
return ( return (
<>
{!rest.isModal && (
<BlurredBackgroundWrapper
image={!data?.image.match('placeholder') ? data.image : null}
expanded={misc.expandSidebar}
>
<BlurredBackground
image={!data?.image.match('placeholder') ? data.image : null}
expanded={misc.expandSidebar}
/>
</BlurredBackgroundWrapper>
)}
<GenericPage <GenericPage
hideDivider hideDivider
header={ header={
<GenericPageHeader <GenericPageHeader
isDark={!rest.isModal}
image={ image={
isCached(`${misc.imageCachePath}album_${albumId}.jpg`) isCached(`${misc.imageCachePath}album_${albumId}.jpg`)
? `${misc.imageCachePath}album_${albumId}.jpg` ? `${misc.imageCachePath}album_${albumId}.jpg`
@ -185,20 +210,29 @@ const AlbumView = ({ ...rest }: any) => {
cacheType: 'album', cacheType: 'album',
id: data.albumId, id: data.albumId,
}} }}
imageHeight={200}
title={data.name} title={data.name}
showTitleTooltip showTitleTooltip
subtitle={ subtitle={
<div> <div>
<div <PageHeaderSubtitleDataLine $top>
style={{ <strong>ALBUM</strong> {' • '} {data.songCount} songs,{' '}
overflow: 'hidden', {formatDuration(data.duration)}
textOverflow: 'ellipsis', {data.year && (
whiteSpace: 'nowrap', <>
}} {' • '}
> {data.year}
</>
)}
</PageHeaderSubtitleDataLine>
<PageHeaderSubtitleDataLine>
Added {formatDate(data.created)}
</PageHeaderSubtitleDataLine>
<PageHeaderSubtitleDataLine>
{data.artist && ( {data.artist && (
<StyledLink <StyledTagLink
tabIndex={0} tabIndex={0}
tooltip={data.artist}
onClick={() => { onClick={() => {
if (!rest.isModal) { if (!rest.isModal) {
history.push(`/library/artist/${data.artistId}`); history.push(`/library/artist/${data.artistId}`);
@ -228,13 +262,13 @@ const AlbumView = ({ ...rest }: any) => {
}} }}
> >
{data.artist} {data.artist}
</StyledLink> </StyledTagLink>
)} )}
{data.genre && ( {data.genre && (
<> <>
{' • '} <StyledTagLink
<StyledLink
tabIndex={0} tabIndex={0}
tooltip={data.genre}
onClick={() => { onClick={() => {
if (!rest.isModal) { if (!rest.isModal) {
dispatch(setActive({ ...album.active, filter: data.genre })); dispatch(setActive({ ...album.active, filter: data.genre }));
@ -270,31 +304,24 @@ const AlbumView = ({ ...rest }: any) => {
}} }}
> >
{data.genre} {data.genre}
</StyledLink> </StyledTagLink>
</>
)}
{data.year && (
<>
{' • '}
{data.year}
</> </>
)} )}
</div> </PageHeaderSubtitleDataLine>
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
<ButtonToolbar> <ButtonToolbar>
<PlayButton appearance="primary" size="md" onClick={handlePlay} /> <PlayButton appearance="primary" size="lg" onClick={handlePlay} />
<PlayAppendNextButton <PlayAppendNextButton
appearance="primary" appearance="primary"
size="md" size="lg"
onClick={() => handlePlayAppend('next')} onClick={() => handlePlayAppend('next')}
/> />
<PlayAppendButton <PlayAppendButton
appearance="primary" appearance="primary"
size="md" size="lg"
onClick={() => handlePlayAppend('later')} onClick={() => handlePlayAppend('later')}
/> />
<FavoriteButton size="md" isFavorite={data.starred} onClick={handleFavorite} /> <FavoriteButton size="lg" isFavorite={data.starred} onClick={handleFavorite} />
</ButtonToolbar> </ButtonToolbar>
</div> </div>
</div> </div>
@ -331,6 +358,7 @@ const AlbumView = ({ ...rest }: any) => {
handleFavorite={handleRowFavorite} handleFavorite={handleRowFavorite}
/> />
</GenericPage> </GenericPage>
</>
); );
}; };

13
src/components/shared/TagLink.tsx

@ -0,0 +1,13 @@
import React from 'react';
import { Tag } from 'rsuite';
import CustomTooltip from './CustomTooltip';
const TagLink = ({ tooltip, children, ...rest }: any) => {
return (
<CustomTooltip text={tooltip}>
<Tag {...rest}>{children}</Tag>
</CustomTooltip>
);
};
export default TagLink;

16
src/components/shared/styled.ts

@ -17,6 +17,7 @@ import {
Tag, Tag,
} from 'rsuite'; } from 'rsuite';
import styled from 'styled-components'; import styled from 'styled-components';
import TagLink from './TagLink';
export const HeaderButton = styled(Button)` export const HeaderButton = styled(Button)`
margin-left: 5px; margin-left: 5px;
@ -140,7 +141,7 @@ export const StyledInputNumber = styled(InputNumber)<{ width: number }>`
width: ${(props) => `${props.width}px`}; width: ${(props) => `${props.width}px`};
`; `;
export const StyledInput = styled(Input)<{ width: number }>` export const StyledInput = styled(Input)<{ width: number; opacity?: number }>`
border: 1px #3c3f43 solid !important; border: 1px #3c3f43 solid !important;
border-radius: ${(props) => props.theme.other.input.borderRadius} !important; border-radius: ${(props) => props.theme.other.input.borderRadius} !important;
@ -148,6 +149,7 @@ export const StyledInput = styled(Input)<{ width: number }>`
background: ${(props) => props.theme.colors.input.background} !important; background: ${(props) => props.theme.colors.input.background} !important;
width: ${(props) => `${props.width}px`}; width: ${(props) => `${props.width}px`};
border-radius: ${(props) => props.theme.other.input.borderRadius}; border-radius: ${(props) => props.theme.other.input.borderRadius};
opacity: ${(props) => props.opacity};
`; `;
export const StyledCheckbox = styled(Checkbox)` export const StyledCheckbox = styled(Checkbox)`
@ -512,5 +514,17 @@ export const StyledTag = styled(Tag)`
color: ${(props) => props.theme.colors.tag.text} !important; color: ${(props) => props.theme.colors.tag.text} !important;
background: ${(props) => props.theme.colors.tag.background}; background: ${(props) => props.theme.colors.tag.background};
border-radius: ${(props) => props.theme.other.tag.borderRadius}; border-radius: ${(props) => props.theme.other.tag.borderRadius};
cursor: pointer;
`;
export const StyledTagLink = styled(TagLink)`
color: ${(props) => props.theme.colors.tag.text} !important;
background: ${(props) => props.theme.colors.tag.background};
border-radius: ${(props) => props.theme.other.tag.borderRadius};
max-width: 13rem;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer; cursor: pointer;
`; `;

16
src/shared/utils.ts

@ -118,6 +118,22 @@ export const formatSongDuration = (duration: number) => {
return `${minutes}:${seconds}`; return `${minutes}:${seconds}`;
}; };
export const formatDuration = (duration: number) => {
const hours = Math.floor(duration / 60 / 60);
const minutes = Math.floor((duration / 60) % 60);
const seconds = String(duration % 60).padStart(2, '0');
if (hours > 0) {
return `${hours} hr ${minutes} min ${seconds} sec`;
}
if (Number.isNaN(minutes)) {
return null;
}
return `${minutes} min ${seconds} sec`;
};
export const formatDate = (date: string) => { export const formatDate = (date: string) => {
return moment(date).format('MMM D YYYY'); return moment(date).format('MMM D YYYY');
}; };

Loading…
Cancel
Save