[feat] Add a dynamic image option to the fullscreen player (#526)
* Add an option for a dynamic background image in the fullscreen player * Center the background image and fix some more bugs * More cleaning up the background image * Add option for customizable blur amount * Fix missing translation key for image blur * Fix dynamic image shifting when player is opened * Hide image blur size config if dynamic background is disabled --------- Co-authored-by: Jeff <42182408+jeffvli@users.noreply.github.com>
This commit is contained in:
parent
a45e7f24e4
commit
d52d9136b8
3 changed files with 76 additions and 8 deletions
|
@ -311,6 +311,8 @@
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
"dynamicBackground": "dynamic background",
|
"dynamicBackground": "dynamic background",
|
||||||
|
"dynamicImageBlur": "image blur size",
|
||||||
|
"dynamicIsImage": "enable background image",
|
||||||
"followCurrentLyric": "follow current lyric",
|
"followCurrentLyric": "follow current lyric",
|
||||||
"lyricAlignment": "lyric alignment",
|
"lyricAlignment": "lyric alignment",
|
||||||
"lyricGap": "lyric gap",
|
"lyricGap": "lyric gap",
|
||||||
|
|
|
@ -60,7 +60,11 @@ const ResponsiveContainer = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BackgroundImageOverlay = styled.div`
|
interface BackgroundImageOverlayProps {
|
||||||
|
$blur: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackgroundImageOverlay = styled.div<BackgroundImageOverlayProps>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -68,12 +72,21 @@ const BackgroundImageOverlay = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--bg-header-overlay);
|
background: var(--bg-header-overlay);
|
||||||
|
backdrop-filter: blur(${({ $blur }) => $blur}rem);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const mainBackground = 'var(--main-bg)';
|
||||||
|
|
||||||
const Controls = () => {
|
const Controls = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { dynamicBackground, expanded, opacity, useImageAspectRatio } =
|
const {
|
||||||
useFullScreenPlayerStore();
|
dynamicBackground,
|
||||||
|
dynamicImageBlur,
|
||||||
|
dynamicIsImage,
|
||||||
|
expanded,
|
||||||
|
opacity,
|
||||||
|
useImageAspectRatio,
|
||||||
|
} = useFullScreenPlayerStore();
|
||||||
const { setStore } = useFullScreenPlayerStoreActions();
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const lyricConfig = useLyricsSettings();
|
const lyricConfig = useLyricsSettings();
|
||||||
|
@ -141,6 +154,45 @@ const Controls = () => {
|
||||||
/>
|
/>
|
||||||
</Option.Control>
|
</Option.Control>
|
||||||
</Option>
|
</Option>
|
||||||
|
{dynamicBackground && (
|
||||||
|
<Option>
|
||||||
|
<Option.Label>
|
||||||
|
{t('page.fullscreenPlayer.config.dynamicIsImage', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Switch
|
||||||
|
defaultChecked={dynamicIsImage}
|
||||||
|
onChange={(e) =>
|
||||||
|
setStore({
|
||||||
|
dynamicIsImage: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
)}
|
||||||
|
{dynamicBackground && dynamicIsImage && (
|
||||||
|
<Option>
|
||||||
|
<Option.Label>
|
||||||
|
{t('page.fullscreenPlayer.config.dynamicImageBlur', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Slider
|
||||||
|
defaultValue={dynamicImageBlur}
|
||||||
|
label={(e) => `${e} rem`}
|
||||||
|
max={6}
|
||||||
|
min={0}
|
||||||
|
step={0.5}
|
||||||
|
w="100%"
|
||||||
|
onChangeEnd={(e) => setStore({ dynamicImageBlur: Number(e) })}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
)}
|
||||||
{dynamicBackground && (
|
{dynamicBackground && (
|
||||||
<Option>
|
<Option>
|
||||||
<Option.Label>
|
<Option.Label>
|
||||||
|
@ -368,9 +420,13 @@ const containerVariants: Variants = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
open: (custom) => {
|
open: (custom) => {
|
||||||
const { dynamicBackground, background, windowBarStyle } = custom;
|
const { background, backgroundImage, dynamicBackground, windowBarStyle } = custom;
|
||||||
return {
|
return {
|
||||||
background: dynamicBackground ? background : 'var(--main-bg)',
|
background: dynamicBackground ? backgroundImage : mainBackground,
|
||||||
|
backgroundColor: dynamicBackground ? background : mainBackground,
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: 'cover',
|
||||||
height:
|
height:
|
||||||
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
|
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
|
||||||
? 'calc(100vh - 120px)'
|
? 'calc(100vh - 120px)'
|
||||||
|
@ -394,7 +450,7 @@ const containerVariants: Variants = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FullScreenPlayer = () => {
|
export const FullScreenPlayer = () => {
|
||||||
const { dynamicBackground } = useFullScreenPlayerStore();
|
const { dynamicBackground, dynamicImageBlur, dynamicIsImage } = useFullScreenPlayerStore();
|
||||||
const { setStore } = useFullScreenPlayerStoreActions();
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
|
@ -416,17 +472,23 @@ export const FullScreenPlayer = () => {
|
||||||
srcLoaded: true,
|
srcLoaded: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const imageUrl = currentSong?.imageUrl;
|
||||||
|
const backgroundImage =
|
||||||
|
imageUrl && dynamicIsImage
|
||||||
|
? `url("${imageUrl.replace(/size=\d+/g, 'size=500')}`
|
||||||
|
: mainBackground;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
animate="open"
|
animate="open"
|
||||||
custom={{ background, dynamicBackground, windowBarStyle }}
|
custom={{ background, backgroundImage, dynamicBackground, windowBarStyle }}
|
||||||
exit="closed"
|
exit="closed"
|
||||||
initial="closed"
|
initial="closed"
|
||||||
transition={{ duration: 2 }}
|
transition={{ duration: 2 }}
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
{dynamicBackground && <BackgroundImageOverlay />}
|
{dynamicBackground && <BackgroundImageOverlay $blur={dynamicImageBlur} />}
|
||||||
<ResponsiveContainer>
|
<ResponsiveContainer>
|
||||||
<FullScreenPlayerImage />
|
<FullScreenPlayerImage />
|
||||||
<FullScreenPlayerQueue />
|
<FullScreenPlayerQueue />
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { immer } from 'zustand/middleware/immer';
|
||||||
interface FullScreenPlayerState {
|
interface FullScreenPlayerState {
|
||||||
activeTab: string | 'queue' | 'related' | 'lyrics';
|
activeTab: string | 'queue' | 'related' | 'lyrics';
|
||||||
dynamicBackground?: boolean;
|
dynamicBackground?: boolean;
|
||||||
|
dynamicImageBlur: number;
|
||||||
|
dynamicIsImage?: boolean;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
useImageAspectRatio: boolean;
|
useImageAspectRatio: boolean;
|
||||||
|
@ -28,6 +30,8 @@ export const useFullScreenPlayerStore = create<FullScreenPlayerSlice>()(
|
||||||
},
|
},
|
||||||
activeTab: 'queue',
|
activeTab: 'queue',
|
||||||
dynamicBackground: true,
|
dynamicBackground: true,
|
||||||
|
dynamicImageBlur: 1.5,
|
||||||
|
dynamicIsImage: false,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
opacity: 60,
|
opacity: 60,
|
||||||
useImageAspectRatio: false,
|
useImageAspectRatio: false,
|
||||||
|
|
Reference in a new issue