Skip to content

Commit

Permalink
app: Add theme color configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
evanpurkhiser committed Dec 28, 2020
1 parent 39a37e7 commit d51f3d7
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 22 deletions.
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -70,6 +70,7 @@
"@emotion/styled": "^10.0.27",
"@octokit/request": "^5.4.5",
"@octokit/types": "^5.0.1",
"@popperjs/core": "^2.6.0",
"@sentry/apm": "^5.16.1",
"@sentry/browser": "^5.27.4",
"@sentry/node": "^5.16.1",
Expand All @@ -88,6 +89,7 @@
"@types/oauth": "^0.9.1",
"@types/object-path": "^0.11.0",
"@types/react": "^17.0.0",
"@types/react-color": "^3.0.4",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.5",
"@types/react-select": "^3.0.19",
Expand Down Expand Up @@ -131,8 +133,10 @@
"prolink-connect": "^0.3.0",
"public-ip": "^4.0.1",
"react": "^17.0.1",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-feather": "^2.0.8",
"react-popper": "^2.2.4",
"react-refresh": "^0.9.0",
"react-router-dom": "^5.2.0",
"react-select": "^3.1.0",
Expand All @@ -148,6 +152,7 @@
"terser-webpack-plugin": "^4.1.0",
"ts-node": "^9.0.0",
"typescript": "^4.0.2",
"use-onclickoutside": "^0.3.1",
"webpack": "^4.44.1",
"webpack-merge": "^4.2.2"
},
Expand Down
170 changes: 170 additions & 0 deletions src/overlay/overlays/nowPlaying/ColorConfig.tsx
@@ -0,0 +1,170 @@
import * as React from 'react';
import {ChromePicker, ColorResult} from 'react-color';
import {X} from 'react-feather';
import {usePopper} from 'react-popper';
import styled from '@emotion/styled';
import {AnimatePresence, motion} from 'framer-motion';
import {set} from 'mobx';
import {observer} from 'mobx-react';
import useOnClickOutside from 'use-onclickoutside';

import {NowPlayingConfig} from '.';

type Props = {
config: NowPlayingConfig;
defaultColors: Record<string, string>;
trimPrefix?: string;
};

const ColorConfig = observer(({config, defaultColors, trimPrefix}: Props) => (
<Container>
{Object.entries(defaultColors).map(([name, color]) => (
<Color
key={name}
name={name}
trimPrefix={trimPrefix}
color={config?.colors?.[name] ?? color}
defaultColor={defaultColors[name]}
onReset={() => set(config, {colors: {...config?.colors, [name]: undefined}})}
onChange={color => {
console.log(color);
set(config, {colors: {...config?.colors, [name]: color}});
}}
/>
))}
</Container>
));

type ColorProps = {
name: string;
color: string;
defaultColor: string;
trimPrefix?: string;
onChange: (color: string) => void;
onReset: () => void;
};

const rgba = (color: ColorResult) =>
`rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`;

const Color = ({color, defaultColor, name, trimPrefix, onChange}: ColorProps) => {
const [showPicker, openPicker] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
const [indicatorEl, setIndicatorEl] = React.useState<HTMLDivElement | null>(null);
const [pickerEl, setPickerEl] = React.useState<HTMLDivElement | null>(null);
const {styles} = usePopper(indicatorEl, pickerEl, {
placement: 'top',
modifiers: [{name: 'arrow'}, {name: 'offset', options: {offset: [0, 15]}}],
});

useOnClickOutside(containerRef, () => openPicker(false));

return (
<div ref={containerRef}>
<ColorPill onClick={() => openPicker(!showPicker)} key={name}>
<ColorBlock ref={setIndicatorEl} style={{backgroundColor: color}} />
{name.replace(trimPrefix ?? '', '')}
{color !== defaultColor && (
<ResetButton
onClick={e => {
console.log('resetting');
e.stopPropagation();
onChange(defaultColor);
}}
/>
)}
</ColorPill>
<AnimatePresence>
{showPicker && (
<Picker style={styles.popper} ref={setPickerEl}>
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
transition={{duration: 0.15}}
>
<ChromePicker
color={color}
onChange={newColor => onChange(rgba(newColor))}
/>
<Arrow data-popper-arrow style={styles.arrow} />
</motion.div>
</Picker>
)}
</AnimatePresence>
</div>
);
};

const Container = styled('div')`
display: flex;
flex-direction: row;
gap: 0.75rem;
flex-wrap: wrap;
`;

const Picker = styled('div')`
padding: 0 2rem;
z-index: 100;
.chrome-picker {
box-shadow: 0 0 40px rgba(0, 0, 0, 0.2) !important;
}
`;

const Arrow = styled('div')`
height: 0;
width: 0;
border: 10px solid transparent;
border-top-color: #fff;
`;

const ColorPill = styled('div')`
cursor: pointer;
background-color: ${p => p.color};
padding: 0.125rem 0.5rem;
padding-left: 0.25rem;
border-radius: 3px;
background: #f3f3f3;
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
overflow: hidden;
`;

const ColorBlock = styled('div')`
content: '';
display: block;
width: 15px;
height: 15px;
border-radius: 25%;
`;

const ResetButton = styled('button')`
position: relative;
padding: 0 0.25rem;
color: #555;
background: #eee;
border: none;
display: flex;
align-items: center;
margin: -0.125rem -0.5rem;
margin-left: 0;
align-self: normal;
svg {
z-index: 1;
}
&:hover {
background: #ddd;
}
`;

ResetButton.defaultProps = {
type: 'button',
children: <X size="0.75rem" />,
};

export default ColorConfig;
2 changes: 2 additions & 0 deletions src/overlay/overlays/nowPlaying/ThemeAsot.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import styled from '@emotion/styled';
import {AnimatePresence, motion} from 'framer-motion';
import {toJS} from 'mobx';
import {observer} from 'mobx-react';

import {PlayedTrack} from 'src/shared/store';
Expand Down Expand Up @@ -243,6 +244,7 @@ type Props = {
const ThemeAsot: React.FC<Props> = observer(({config, history}) =>
history.length === 0 ? null : (
<CurrentTrack
style={toJS(config.colors)}
className="track-current"
alignRight={config.alignRight}
tags={config.tags}
Expand Down
6 changes: 4 additions & 2 deletions src/overlay/overlays/nowPlaying/ThemeModern.tsx
Expand Up @@ -3,6 +3,7 @@ import {Disc, X} from 'react-feather';
import styled from '@emotion/styled';
import {formatDistance} from 'date-fns';
import {AnimatePresence, motion} from 'framer-motion';
import {toJS} from 'mobx';
import {observer} from 'mobx-react';

import TimeTicker from 'src/shared/components/TimeTicker';
Expand Down Expand Up @@ -245,7 +246,7 @@ type BaseTrackProps = MotionDivProps & {
alignRight?: boolean;
hideArtwork?: boolean;
/**
* Disables animation of the artwork
* Enables animation of the artwork
*/
firstPlayed?: boolean;
/**
Expand Down Expand Up @@ -373,6 +374,7 @@ const ThemeModern: React.FC<Props> = observer(({config, history}) =>
history.length === 0 ? null : (
<React.Fragment>
<CurrentTrack
style={toJS(config.colors)}
className="track-current"
alignRight={config.alignRight}
hideArtwork={config.hideArtwork}
Expand All @@ -381,7 +383,7 @@ const ThemeModern: React.FC<Props> = observer(({config, history}) =>
played={history[0]}
/>
{(config.historyCount ?? 0) > 0 && history.length > 1 && (
<RecentWrapper className="track-recents">
<RecentWrapper className="track-recents" style={toJS(config.colors)}>
<AnimatePresence>
{history
.slice(1, config.historyCount ? config.historyCount + 1 : 0)
Expand Down
51 changes: 35 additions & 16 deletions src/overlay/overlays/nowPlaying/index.tsx
Expand Up @@ -12,6 +12,7 @@ import Select from 'src/renderer/components/form/Select';
import store, {PlayedTrack} from 'src/shared/store';
import useRandomHistory from 'src/utils/useRandomHistory';

import ColorConfig from './ColorConfig';
import {availableTags, Tags} from './tags';
import themeAsot from './ThemeAsot';
import themeModern from './ThemeModern';
Expand Down Expand Up @@ -54,6 +55,10 @@ export type NowPlayingConfig = {
* The specific set of tags to display
*/
tags?: Tags;
/**
* Customized theme colors
*/
colors?: Record<string, string>;
};

export type ThemeDescriptor = {
Expand Down Expand Up @@ -118,7 +123,7 @@ const EmptyExample = styled('div')`
}
`;

const HistoryOverlay: React.FC<{config: NowPlayingConfig}> = observer(({config}) => {
const NowPlayingOverlay: React.FC<{config: NowPlayingConfig}> = observer(({config}) => {
const Overlay = themes[config.theme].component;
const history = store.mixstatus.trackHistory.slice().reverse();

Expand All @@ -129,7 +134,7 @@ const valueTransform = <T extends readonly string[]>(t: T) =>
t.map(v => ({label: v, value: v}));

const ConfigInterface: React.FC<{config: NowPlayingConfig}> = observer(({config}) => {
const {enabledConfigs} = themes[config.theme];
const {enabledConfigs, colors} = themes[config.theme];

return (
<div>
Expand Down Expand Up @@ -185,27 +190,41 @@ const ConfigInterface: React.FC<{config: NowPlayingConfig}> = observer(({config}
/>
</Field>
)}
<Field
size="full"
name="Additional Tags"
description="Select the additional tags you want to show in the metadata. Emptying the list will stop any attributes from showing"
>
<Select
isMulti
placeholder="Add metadata items to display..."
options={valueTransform(availableTags)}
value={valueTransform(config.tags ?? [])}
onChange={values => set(config, {tags: values?.map((v: any) => v.value) ?? []})}
/>
</Field>
{enabledConfigs.includes('tags') && (
<Field
size="full"
name="Additional Tags"
description="Select the additional tags you want to show in the metadata. Emptying the list will stop any attributes from showing"
>
<Select
isMulti
placeholder="Add metadata items to display..."
options={valueTransform(availableTags)}
value={valueTransform(config.tags ?? [])}
onChange={values =>
set(config, {tags: values?.map((v: any) => v.value) ?? []})
}
/>
</Field>
)}
{Object.keys(colors).length > 0 && (
<Field
size="full"
name="Theme Colors"
htmlFor="none"
description="Customize the colors of this now playing theme"
>
<ColorConfig trimPrefix="--pt-np-" config={config} defaultColors={colors} />
</Field>
)}
</div>
);
});

const descriptor: OverlayDescriptor<TaggedNowPlaying> = {
type: 'nowPlaying',
name: 'Live now playing metadata overlay, including themes',
component: HistoryOverlay,
component: NowPlayingOverlay,
example: Example,
configInterface: ConfigInterface,
defaultConfig: {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/form/Field.tsx
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import {css} from '@emotion/core';
import styled from '@emotion/styled';

type Props = React.HTMLAttributes<HTMLLabelElement> & {
type Props = React.ComponentProps<'label'> & {
size?: 'sm' | 'md' | 'lg' | 'fit' | 'full';
noCenter?: boolean;
top?: boolean;
Expand Down

0 comments on commit d51f3d7

Please sign in to comment.