add configuration to blur explicit album/song art

This commit is contained in:
jeffvli
2026-02-04 01:20:31 -08:00
parent 6e3275c05c
commit 4c256348fc
19 changed files with 101 additions and 15 deletions
+2
View File
@@ -936,6 +936,8 @@
"showLyricsInSidebar": "show lyrics in player sidebar",
"showRatings_description": "controls if the star ratings feature shows up in the interface",
"showRatings": "show star ratings",
"blurExplicitImages": "blur explicit images",
"blurExplicitImages_description": "album and song artwork tagged as explicit will be blurred",
"enableGridMultiSelect": "enable grid multi-select",
"enableGridMultiSelect_description": "when enabled, allows selecting multiple items in grid views. when disabled, clicking grid item images will navigate to the item page",
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
@@ -121,6 +121,7 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
containerClassName={styles.albumImageContainer}
enableDebounce={false}
enableViewport={false}
explicitStatus={album.explicitStatus}
fetchPriority="high"
id={album.imageId}
itemType={LibraryItem.ALBUM}
@@ -118,6 +118,7 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
containerClassName={styles.albumImageContainer}
enableDebounce={false}
enableViewport={false}
explicitStatus={album.explicitStatus}
fetchPriority="high"
id={album.imageId}
itemType={LibraryItem.ALBUM}
@@ -34,6 +34,7 @@
position: absolute;
top: 0;
left: 0;
z-index: 5;
width: 100%;
height: 100%;
pointer-events: none;
@@ -362,6 +362,9 @@ const CompactItemCard = ({
[styles.isRound]: isRound,
})}
enableDebounce={false}
explicitStatus={
'explicitStatus' in data && data ? data.explicitStatus : null
}
id={data?.imageId}
itemType={itemType}
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
@@ -596,6 +599,9 @@ const DefaultItemCard = ({
<ItemImage
className={clsx(styles.image, { [styles.isRound]: isRound })}
enableDebounce={false}
explicitStatus={
'explicitStatus' in data && data ? data.explicitStatus : null
}
id={data?.imageId}
itemType={itemType}
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
@@ -893,6 +899,9 @@ const PosterItemCard = ({
<ItemImage
className={clsx(styles.image, { [styles.isRound]: isRound })}
enableDebounce={false}
explicitStatus={
'explicitStatus' in data && data ? data.explicitStatus : null
}
id={(data as { imageId: string })?.imageId}
itemType={itemType}
src={(data as { imageUrl: string })?.imageUrl}
@@ -7,11 +7,12 @@ import {
getServerById,
useAuthStore,
useCurrentServerId,
useGeneralSettings,
useImageRes,
useSettingsStore,
} from '/@/renderer/store';
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
import { LibraryItem } from '/@/shared/types/domain-types';
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
const getUnloaderIcon = (itemType: LibraryItem) => {
switch (itemType) {
@@ -34,6 +35,7 @@ const getUnloaderIcon = (itemType: LibraryItem) => {
const BaseItemImage = (
props: Omit<ImageProps, 'id' | 'src'> & {
explicitStatus?: ExplicitStatus | null;
id?: null | string;
itemType: LibraryItem;
serverId?: null | string;
@@ -41,7 +43,8 @@ const BaseItemImage = (
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
},
) => {
const { serverId, src, ...rest } = props;
const { explicitStatus, serverId, src, ...rest } = props;
const { blurExplicitImages } = useGeneralSettings();
const imageUrl = useItemImageUrl({
id: props.id,
@@ -51,8 +54,11 @@ const BaseItemImage = (
type: props.type,
});
const isExplicit = blurExplicitImages && explicitStatus === ExplicitStatus.EXPLICIT;
return (
<BaseImage
isExplicit={isExplicit}
src={imageUrl}
unloaderIcon={getUnloaderIcon(props.itemType)}
{...rest}
@@ -90,6 +90,7 @@ const ImageColumnBase = (props: ItemTableListInnerColumn) => {
})}
enableDebounce={true}
enableViewport={false}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
src={item?.imageUrl}
@@ -106,6 +106,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
containerClassName={styles.image}
enableDebounce={true}
enableViewport={false}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
src={item?.imageUrl}
@@ -246,6 +247,7 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
>
<ItemImage
containerClassName={styles.image}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
serverId={item?._serverId}
@@ -220,6 +220,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
<LibraryHeader
item={{
children: headerItem,
explicitStatus: detailQuery?.data?.explicitStatus ?? null,
imageId: detailQuery?.data?.imageId,
imageUrl: detailQuery?.data?.imageUrl,
route: AppRoute.LIBRARY_ALBUMS,
@@ -127,6 +127,7 @@ const DummyAlbumDetailRoute = () => {
<LibraryHeader
imageUrl={imageUrl}
item={{
explicitStatus: detailQuery?.data?.explicitStatus ?? null,
imageId: detailQuery?.data?.imageId,
imageUrl: detailQuery?.data?.imageUrl,
route: AppRoute.LIBRARY_SONGS,
@@ -123,6 +123,7 @@ export const LeftControls = () => {
)}
enableDebounce={false}
enableViewport={false}
explicitStatus={currentSong?.explicitStatus}
fetchPriority="high"
id={currentSong?.imageId}
itemType={LibraryItem.SONG}
@@ -94,6 +94,7 @@ export const MobilePlayerbar = () => {
)}
enableDebounce={false}
enableViewport={false}
explicitStatus={currentSong.explicitStatus}
fetchPriority="high"
id={currentSong.imageId}
itemType={LibraryItem.SONG}
@@ -166,6 +166,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
>
{({ isHighlighted }) => (
<LibraryCommandItem
explicitStatus={album.explicitStatus}
id={album.id}
imageId={album.imageId}
imageUrl={album.imageUrl}
@@ -238,6 +239,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
>
{({ isHighlighted }) => (
<LibraryCommandItem
explicitStatus={song.explicitStatus}
id={song.id}
imageId={song.imageId}
imageUrl={song.imageUrl}
@@ -13,11 +13,12 @@ import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem, Song } from '/@/shared/types/domain-types';
import { ExplicitStatus, LibraryItem, Song } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
interface LibraryCommandItemProps {
disabled?: boolean;
explicitStatus?: ExplicitStatus | null;
id: string;
imageId: null | string;
imageUrl: null | string;
@@ -30,6 +31,7 @@ interface LibraryCommandItemProps {
export const LibraryCommandItem = ({
disabled,
explicitStatus,
id,
imageId,
imageUrl,
@@ -100,6 +102,7 @@ export const LibraryCommandItem = ({
<ItemImage
alt="cover"
className={styles.image}
explicitStatus={explicitStatus ?? song?.explicitStatus ?? null}
height={40}
id={imageId}
itemType={itemType}
@@ -621,6 +621,28 @@ export const ApplicationSettings = memo(() => {
isHidden: false,
title: t('setting.showRatings', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.blurExplicitImages', { postProcess: 'sentenceCase' })}
defaultChecked={settings.blurExplicitImages}
onChange={(e) =>
setSettings({
general: {
...settings,
blurExplicitImages: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.blurExplicitImages', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.blurExplicitImages', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
@@ -27,7 +27,7 @@ import { BaseImage } from '/@/shared/components/image/image';
import { Rating } from '/@/shared/components/rating/rating';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
interface LibraryHeaderProps {
@@ -37,6 +37,7 @@ interface LibraryHeaderProps {
imageUrl?: null | string;
item: {
children?: ReactNode;
explicitStatus?: ExplicitStatus | null;
imageId?: null | string;
imageUrl?: null | string;
route: string;
@@ -108,6 +109,7 @@ export const LibraryHeader = forwardRef(
enableDebounce={false}
enableViewport={false}
fetchPriority="high"
isExplicit={item.explicitStatus === ExplicitStatus.EXPLICIT}
src={imageUrl}
style={{
maxHeight: '100%',
@@ -120,7 +122,7 @@ export const LibraryHeader = forwardRef(
),
fullScreen: true,
});
}, [item.imageId, item.type]);
}, [item.explicitStatus, item.imageId, item.type]);
return (
<div className={clsx(styles.libraryHeader, containerClassName)} ref={ref}>
@@ -142,6 +144,7 @@ export const LibraryHeader = forwardRef(
containerClassName={styles.image}
enableDebounce={false}
enableViewport={false}
explicitStatus={item.explicitStatus ?? null}
fetchPriority="high"
id={item.imageId}
itemType={item.type as LibraryItem}
+2
View File
@@ -416,6 +416,7 @@ export const GeneralSettingsSchema = z.object({
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
artistRadioCount: z.number(),
artistReleaseTypeItems: z.array(SortableItemSchema(ArtistReleaseTypeItemSchema)),
blurExplicitImages: z.boolean(),
buttonSize: z.number(),
collections: z.array(CollectionSchema),
combinedLyricsAndVisualizer: z.boolean(),
@@ -988,6 +989,7 @@ const initialState: SettingsState = {
artistItems,
artistRadioCount: 20,
artistReleaseTypeItems,
blurExplicitImages: false,
buttonSize: 15,
collections: [],
combinedLyricsAndVisualizer: false,
@@ -27,11 +27,29 @@
}
.image-container {
position: relative;
display: flex;
width: 100%;
height: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
}
.censored .image {
filter: blur(10px);
}
.censored::after {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-weight: bold;
color: var(--theme-colors-background);
background-color: alpha(var(--theme-colors-background), 0.5);
border-radius: var(--theme-radius-md);
}
.unloader {
display: flex;
align-items: center;
+19 -10
View File
@@ -26,6 +26,7 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 's
imageContainerProps?: Omit<ImageContainerProps, 'children'>;
includeLoader?: boolean;
includeUnloader?: boolean;
isExplicit?: boolean;
src: string | undefined;
thumbHash?: string;
unloaderIcon?: keyof typeof AppIcon;
@@ -34,6 +35,7 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 's
interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
enableAnimation?: boolean;
isExplicit?: boolean;
}
interface ImageLoaderProps {
@@ -58,6 +60,7 @@ export function BaseImage({
imageContainerProps,
includeLoader = true,
includeUnloader = true,
isExplicit = false,
src,
unloaderIcon = 'emptyImage',
...props
@@ -72,6 +75,7 @@ export function BaseImage({
imageContainerProps={imageContainerProps}
includeLoader={includeLoader}
includeUnloader={includeUnloader}
isExplicit={isExplicit}
src={src}
unloaderIcon={unloaderIcon}
{...props}
@@ -88,6 +92,7 @@ export function BaseImage({
imageContainerProps={imageContainerProps}
includeLoader={includeLoader}
includeUnloader={includeUnloader}
isExplicit={isExplicit}
src={src}
unloaderIcon={unloaderIcon}
{...props}
@@ -101,6 +106,7 @@ export function BaseImage({
<ImageContainer
className={clsx(containerClassName, containerPropsClassName)}
enableAnimation={enableAnimation}
isExplicit={isExplicit}
{...restContainerProps}
>
{src ? (
@@ -135,6 +141,7 @@ function ImageWithDebounce({
imageContainerProps,
includeLoader,
includeUnloader,
isExplicit = false,
src,
unloaderIcon,
...props
@@ -176,6 +183,7 @@ function ImageWithDebounce({
<ImageContainer
className={clsx(containerClassName, containerPropsClassName)}
enableAnimation={enableAnimation}
isExplicit={isExplicit}
ref={ref}
{...restContainerProps}
>
@@ -209,6 +217,7 @@ function ImageWithDebounce({
<ImageContainer
className={clsx(containerClassName, containerPropsClassName)}
enableAnimation={enableAnimation}
isExplicit={isExplicit}
{...restContainerProps}
>
{effectiveSrc ? (
@@ -244,6 +253,7 @@ function ImageWithViewport({
imageContainerProps,
includeLoader,
includeUnloader,
isExplicit = false,
src,
unloaderIcon,
...props
@@ -275,6 +285,7 @@ function ImageWithViewport({
<ImageContainer
className={clsx(containerClassName, containerPropsClassName)}
enableAnimation={enableAnimation}
isExplicit={isExplicit}
ref={ref}
{...restContainerProps}
>
@@ -347,19 +358,17 @@ export const Image = memo(BaseImage);
const ImageContainer = forwardRef(
(
{ children, className, enableAnimation, ...props }: ImageContainerProps,
{ children, className, isExplicit, ...props }: ImageContainerProps,
ref: ForwardedRef<HTMLDivElement>,
) => {
if (!enableAnimation) {
return (
<div className={clsx(styles.imageContainer, className)} ref={ref} {...props}>
{children}
</div>
);
}
return (
<div className={clsx(styles.imageContainer, className)} ref={ref} {...props}>
<div
className={clsx(styles.imageContainer, className, {
[styles.censored]: isExplicit,
})}
ref={ref}
{...props}
>
{children}
</div>
);