diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index cfabc7aab..a532ba59a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/components/feature-carousel/feature-carousel.tsx b/src/renderer/components/feature-carousel/feature-carousel.tsx index 97338546a..432d74ee3 100644 --- a/src/renderer/components/feature-carousel/feature-carousel.tsx +++ b/src/renderer/components/feature-carousel/feature-carousel.tsx @@ -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} diff --git a/src/renderer/components/feature-carousel/single-feature-carousel.tsx b/src/renderer/components/feature-carousel/single-feature-carousel.tsx index baa692645..439bb95df 100644 --- a/src/renderer/components/feature-carousel/single-feature-carousel.tsx +++ b/src/renderer/components/feature-carousel/single-feature-carousel.tsx @@ -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} diff --git a/src/renderer/components/item-card/item-card.module.css b/src/renderer/components/item-card/item-card.module.css index 49d819c2a..026212d79 100644 --- a/src/renderer/components/item-card/item-card.module.css +++ b/src/renderer/components/item-card/item-card.module.css @@ -34,6 +34,7 @@ position: absolute; top: 0; left: 0; + z-index: 5; width: 100%; height: 100%; pointer-events: none; diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index f8725a6c2..cf7ce6792 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -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 = ({ { switch (itemType) { @@ -34,6 +35,7 @@ const getUnloaderIcon = (itemType: LibraryItem) => { const BaseItemImage = ( props: Omit & { + explicitStatus?: ExplicitStatus | null; id?: null | string; itemType: LibraryItem; serverId?: null | string; @@ -41,7 +43,8 @@ const BaseItemImage = ( type?: keyof z.infer['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 ( { })} enableDebounce={true} enableViewport={false} + explicitStatus={item?.explicitStatus} id={item?.imageId} itemType={item?._itemType} src={item?.imageUrl} diff --git a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx index ca9a9a47b..2acb9e032 100644 --- a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx @@ -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) => > ((_props, ref) => { { { )} enableDebounce={false} enableViewport={false} + explicitStatus={currentSong?.explicitStatus} fetchPriority="high" id={currentSong?.imageId} itemType={LibraryItem.SONG} diff --git a/src/renderer/features/player/components/mobile-playerbar.tsx b/src/renderer/features/player/components/mobile-playerbar.tsx index 3d708c96d..a07099d5e 100644 --- a/src/renderer/features/player/components/mobile-playerbar.tsx +++ b/src/renderer/features/player/components/mobile-playerbar.tsx @@ -94,6 +94,7 @@ export const MobilePlayerbar = () => { )} enableDebounce={false} enableViewport={false} + explicitStatus={currentSong.explicitStatus} fetchPriority="high" id={currentSong.imageId} itemType={LibraryItem.SONG} diff --git a/src/renderer/features/search/components/command-palette.tsx b/src/renderer/features/search/components/command-palette.tsx index 62ef70054..f7784e67e 100644 --- a/src/renderer/features/search/components/command-palette.tsx +++ b/src/renderer/features/search/components/command-palette.tsx @@ -166,6 +166,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { > {({ isHighlighted }) => ( { > {({ isHighlighted }) => ( { isHidden: false, title: t('setting.showRatings', { postProcess: 'sentenceCase' }), }, + { + control: ( + + setSettings({ + general: { + ...settings, + blurExplicitImages: e.currentTarget.checked, + }, + }) + } + /> + ), + description: t('setting.blurExplicitImages', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: false, + title: t('setting.blurExplicitImages', { postProcess: 'sentenceCase' }), + }, { control: ( @@ -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} diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 682aa403e..7b3de5f87 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -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, diff --git a/src/shared/components/image/image.module.css b/src/shared/components/image/image.module.css index 6caf706c1..65e22837c 100644 --- a/src/shared/components/image/image.module.css +++ b/src/shared/components/image/image.module.css @@ -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; diff --git a/src/shared/components/image/image.tsx b/src/shared/components/image/image.tsx index 7d1b73574..1275c6b5e 100644 --- a/src/shared/components/image/image.tsx +++ b/src/shared/components/image/image.tsx @@ -26,6 +26,7 @@ export interface ImageProps extends Omit, 's imageContainerProps?: Omit; includeLoader?: boolean; includeUnloader?: boolean; + isExplicit?: boolean; src: string | undefined; thumbHash?: string; unloaderIcon?: keyof typeof AppIcon; @@ -34,6 +35,7 @@ export interface ImageProps extends Omit, 's interface ImageContainerProps extends HTMLAttributes { 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({ {src ? ( @@ -135,6 +141,7 @@ function ImageWithDebounce({ imageContainerProps, includeLoader, includeUnloader, + isExplicit = false, src, unloaderIcon, ...props @@ -176,6 +183,7 @@ function ImageWithDebounce({ @@ -209,6 +217,7 @@ function ImageWithDebounce({ {effectiveSrc ? ( @@ -244,6 +253,7 @@ function ImageWithViewport({ imageContainerProps, includeLoader, includeUnloader, + isExplicit = false, src, unloaderIcon, ...props @@ -275,6 +285,7 @@ function ImageWithViewport({ @@ -347,19 +358,17 @@ export const Image = memo(BaseImage); const ImageContainer = forwardRef( ( - { children, className, enableAnimation, ...props }: ImageContainerProps, + { children, className, isExplicit, ...props }: ImageContainerProps, ref: ForwardedRef, ) => { - if (!enableAnimation) { - return ( -
- {children} -
- ); - } - return ( -
+
{children}
);