From 07ef323cccd37e5fa9f323019a42f0d4ffb1c082 Mon Sep 17 00:00:00 2001 From: Marc Plano-Lesay Date: Sun, 26 Jul 2026 22:59:17 +1000 Subject: [PATCH] Make ratings/favourites a bit more customizable (#2262) * Replace show ratings toggle with favorite/rating controls setting Swap `genera.showRatings` (previously a simple bool) for a `favoriteRatingControls` enum, exposed via derived `useShowRatings` / `useShowFavorites` hooks. Settings switch becomes a select, with a migration to a new v32 schema mapping the old boolean forward with no user change. * Gate favorite controls on `favoriteRatingControls` setting Hide the playerbar and mobile fullscreen favorite buttons and the context-menu favorite action when the setting excludes favorites, mirroring how ratings are already gated. * Gate favorite controls on item cards and detail list Drill a `showFavorite` flag through the item-card tree (mirroring `showRating`), and gate the card/detail-list favorite badges and hover buttons on the `favoriteRatingControls` setting. * Gate favorite controls in detail headers and album group controls * Keep the original `showRatings` switch and add a `showFavorites` * Gate album header favorite hander at definition --- src/i18n/locales/en.json | 4 +- .../item-card/item-card-controls.tsx | 4 +- .../components/item-card/item-card.tsx | 23 +++++++++-- .../item-detail-list/item-detail-list.tsx | 6 ++- .../item-table-list/album-group-controls.tsx | 39 ++++++++++--------- .../albums/components/album-detail-header.tsx | 23 ++++++----- .../routes/dummy-album-detail-route.tsx | 33 +++++++++------- .../components/album-artist-detail-header.tsx | 5 ++- .../actions/set-favorite-action.tsx | 7 +++- .../mobile-fullscreen-player-metadata.tsx | 24 +++++++----- .../components/mobile-fullscreen-player.tsx | 7 +++- .../player/components/right-controls.tsx | 8 ++-- .../general/application-settings.tsx | 20 ++++++++++ src/renderer/store/env-settings-overrides.ts | 1 + src/renderer/store/settings.store.ts | 8 +++- 15 files changed, 143 insertions(+), 69 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b251ec10a..f232c1c7a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1085,10 +1085,12 @@ "preferLocalLyrics": "Prefer local lyrics", "showLyricsInSidebar_description": "A panel will be added to the attached play queue that displays the lyrics", "showLyricsInSidebar": "Show lyrics in player sidebar", + "showFavorites": "Show favorites", + "showFavorites_description": "Controls if the favorite (heart) buttons show up in the interface", "showQueueInSidebar_description": "A panel will be added to the player sidebar that displays the play queue", "showQueueInSidebar": "Show play queue in player sidebar", - "showRatings_description": "Controls if the star ratings feature shows up in the interface", "showRatings": "Show star ratings", + "showRatings_description": "Controls if the star ratings feature shows up in the interface", "blurExplicitImages": "Blur explicit images", "blurExplicitImages_description": "Album and song artwork tagged as explicit will be blurred", "enableGridMultiSelect": "Enable grid multi-select", diff --git a/src/renderer/components/item-card/item-card-controls.tsx b/src/renderer/components/item-card/item-card-controls.tsx index 11800ee60..86471d43c 100644 --- a/src/renderer/components/item-card/item-card-controls.tsx +++ b/src/renderer/components/item-card/item-card-controls.tsx @@ -33,6 +33,7 @@ interface ItemCardControlsProps { internalState?: ItemListStateActions; item: Album | AlbumArtist | Artist | Genre | Playlist | Song | undefined; itemType: LibraryItem; + showFavorite: boolean; showRating: boolean; type?: 'compact' | 'default' | 'poster'; } @@ -205,6 +206,7 @@ export const ItemCardControls = ({ internalState, item, itemType, + showFavorite, showRating, type = 'default', }: ItemCardControlsProps) => { @@ -289,7 +291,7 @@ export const ItemCardControls = ({ )} - {controls?.onFavorite && ( + {controls?.onFavorite && showFavorite && ( )} {controls?.onRating && diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index 723907390..815e731ba 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -19,7 +19,7 @@ import { ItemControls } from '/@/renderer/components/item-list/types'; import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { AppRoute } from '/@/renderer/router/routes'; -import { useShowRatings } from '/@/renderer/store'; +import { useShowFavorites, useShowRatings } from '/@/renderer/store'; import { formatDateAbsolute, formatDateRelative, @@ -90,6 +90,7 @@ export const ItemCard = ({ withControls, }: ItemCardProps) => { const showRatings = useShowRatings(); + const showFavorites = useShowFavorites(); const imageUrl = getImageUrl(data); const rows = providedRows || []; @@ -110,6 +111,7 @@ export const ItemCard = ({ isRound={isRound} itemType={itemType} rows={rows} + showFavorite={showFavorites} showRating={showRatings} withControls={withControls} /> @@ -130,6 +132,7 @@ export const ItemCard = ({ isRound={isRound} itemType={itemType} rows={rows} + showFavorite={showFavorites} showRating={showRatings} withControls={withControls} /> @@ -150,6 +153,7 @@ export const ItemCard = ({ isRound={isRound} itemType={itemType} rows={rows} + showFavorite={showFavorites} showRating={showRatings} withControls={withControls} /> @@ -166,6 +170,7 @@ export interface ItemCardDerivativeProps extends Omit { imageUrl: string | undefined; internalState?: ItemListStateActions; rows: DataRow[]; + showFavorite: boolean; showRating: boolean; } @@ -186,6 +191,7 @@ const ItemCardStandardImageArea = memo(function ItemCardStandardImageArea({ isRound, itemType, navigationPath, + showFavorite, showRating, variant, withControls, @@ -204,6 +210,7 @@ const ItemCardStandardImageArea = memo(function ItemCardStandardImageArea({ isRound?: boolean; itemType: LibraryItem; navigationPath: null | string; + showFavorite: boolean; showRating: boolean; variant: 'default' | 'poster'; withControls?: boolean; @@ -259,7 +266,7 @@ const ItemCardStandardImageArea = memo(function ItemCardStandardImageArea({ type="itemCard" /> )} - {isFavorite &&
} + {showFavorite && isFavorite &&
} {hasRating &&
{userRating}
} {withControls && showControls && ( @@ -269,6 +276,7 @@ const ItemCardStandardImageArea = memo(function ItemCardStandardImageArea({ {...(variant === 'poster' ? { internalState } : {})} item={data} itemType={itemType} + showFavorite={showFavorite} showRating={showRating} type={variant} /> @@ -321,6 +329,7 @@ const CompactItemCardImageArea = memo(function CompactItemCardImageArea({ itemType, navigationPath, rows, + showFavorite, showRating, withControls, }: { @@ -338,6 +347,7 @@ const CompactItemCardImageArea = memo(function CompactItemCardImageArea({ itemType: LibraryItem; navigationPath: null | string; rows: DataRow[]; + showFavorite: boolean; showRating: boolean; withControls?: boolean; }) { @@ -393,7 +403,7 @@ const CompactItemCardImageArea = memo(function CompactItemCardImageArea({ type="itemCard" /> )} - {isFavorite &&
} + {showFavorite && isFavorite &&
} {hasRating &&
{userRating}
} {withControls && showControls && data && ( @@ -403,6 +413,7 @@ const CompactItemCardImageArea = memo(function CompactItemCardImageArea({ internalState={internalState} item={data} itemType={itemType} + showFavorite={showFavorite} showRating={showRating} type="compact" /> @@ -468,6 +479,7 @@ const CompactItemCard = ({ isRound, itemType, rows, + showFavorite, showRating, withControls, }: ItemCardDerivativeProps) => { @@ -634,6 +646,7 @@ const CompactItemCard = ({ itemType={itemType} navigationPath={navigationPath} rows={rows} + showFavorite={showFavorite} showRating={showRating} withControls={withControls} /> @@ -679,6 +692,7 @@ const DefaultItemCard = ({ isRound, itemType, rows, + showFavorite, showRating, withControls, }: ItemCardDerivativeProps) => { @@ -777,6 +791,7 @@ const DefaultItemCard = ({ isRound={isRound} itemType={itemType} navigationPath={navigationPath} + showFavorite={showFavorite} showRating={showRating} variant="default" withControls={withControls} @@ -840,6 +855,7 @@ const PosterItemCard = ({ isRound, itemType, rows, + showFavorite, showRating, withControls, }: ItemCardDerivativeProps) => { @@ -1005,6 +1021,7 @@ const PosterItemCard = ({ isRound={isRound} itemType={itemType} navigationPath={navigationPath} + showFavorite={showFavorite} showRating={showRating} variant="poster" withControls={withControls} diff --git a/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx b/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx index 8a78a0fe7..1679b7d45 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx +++ b/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx @@ -67,7 +67,7 @@ import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutatio import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { AppRoute } from '/@/renderer/router/routes'; -import { useSettingsStore, useShowRatings } from '/@/renderer/store'; +import { useSettingsStore, useShowFavorites, useShowRatings } from '/@/renderer/store'; import { formatDurationString, formatPartialIsoDateUTC } from '/@/renderer/utils'; import { SEPARATOR_STRING } from '/@/shared/api/utils'; import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator'; @@ -426,6 +426,7 @@ const MetadataSection = memo( ({ controls, internalState, item }: MetadataSectionProps) => { const { t } = useTranslation(); const showRatings = useShowRatings(); + const showFavorites = useShowFavorites(); const [isImageHovered, setIsImageHovered] = useState(false); const [isMetadataHovered, setIsMetadataHovered] = useState(false); @@ -570,7 +571,7 @@ const MetadataSection = memo( serverId={item._serverId} type="itemCard" /> - {isFavorite &&
} + {showFavorites && isFavorite &&
} {hasRating &&
{userRating}
} {controls && isImageHovered && ( @@ -580,6 +581,7 @@ const MetadataSection = memo( internalState={internalState} item={item} itemType={item._itemType} + showFavorite={showFavorites} showRating={true} type="compact" /> diff --git a/src/renderer/components/item-list/item-table-list/album-group-controls.tsx b/src/renderer/components/item-list/item-table-list/album-group-controls.tsx index 8df2d9da5..a13991436 100644 --- a/src/renderer/components/item-list/item-table-list/album-group-controls.tsx +++ b/src/renderer/components/item-list/item-table-list/album-group-controls.tsx @@ -9,7 +9,7 @@ import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating'; import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation'; -import { useShowRatings } from '/@/renderer/store'; +import { useShowFavorites, useShowRatings } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Rating } from '/@/shared/components/rating/rating'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types'; @@ -35,6 +35,7 @@ interface AlbumGroupControlsProps { export const AlbumGroupControls = ({ albumId, serverId, serverType }: AlbumGroupControlsProps) => { const showRatingsSetting = useShowRatings(); + const showFavorites = useShowFavorites(); const detailQuery = useAlbumGroupAlbum(albumId, serverId); const setFavorite = useSetFavorite(); const setRating = useSetRating(); @@ -77,23 +78,25 @@ export const AlbumGroupControls = ({ albumId, serverId, serverType }: AlbumGroup return (
- { - event.stopPropagation(); - event.preventDefault(); - }} - size="xs" - variant="transparent" - /> + {showFavorites && ( + { + event.stopPropagation(); + event.preventDefault(); + }} + size="xs" + variant="transparent" + /> + )} {showRating && ( ((_props, ref) => { const { t } = useTranslation(); const server = useCurrentServer(); const showRatings = useShowRatings(); + const showFavorites = useShowFavorites(); const queryClient = useQueryClient(); const albumRadioCount = useArtistRadioCount(); const detailQuery = useQuery( @@ -51,15 +52,17 @@ export const AlbumDetailHeader = forwardRef((_props, ref) => { const setRating = useSetRating(); const setFavorite = useSetFavorite(); - const handleFavorite = () => { - if (!detailQuery?.data) return; - setFavorite( - detailQuery.data._serverId, - [detailQuery.data.id], - LibraryItem.ALBUM, - !detailQuery.data.userFavorite, - ); - }; + const handleFavorite = showFavorites + ? () => { + if (!detailQuery?.data) return; + setFavorite( + detailQuery.data._serverId, + [detailQuery.data.id], + LibraryItem.ALBUM, + !detailQuery.data.userFavorite, + ); + } + : undefined; const handleUpdateRating = showRating ? (rating: number) => { diff --git a/src/renderer/features/albums/routes/dummy-album-detail-route.tsx b/src/renderer/features/albums/routes/dummy-album-detail-route.tsx index dc7db3c41..cb807de05 100644 --- a/src/renderer/features/albums/routes/dummy-album-detail-route.tsx +++ b/src/renderer/features/albums/routes/dummy-album-detail-route.tsx @@ -19,7 +19,7 @@ import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete- import { useFastAverageColor } from '/@/renderer/hooks'; import { queryClient } from '/@/renderer/lib/react-query'; import { AppRoute } from '/@/renderer/router/routes'; -import { useCurrentServer } from '/@/renderer/store'; +import { useCurrentServer, useShowFavorites } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { formatDurationString } from '/@/renderer/utils'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; @@ -38,6 +38,7 @@ const DummyAlbumDetailRoute = () => { const { albumId } = useParams() as { albumId: string }; const server = useCurrentServer(); + const showFavorites = useShowFavorites(); const queryKey = queryKeys.songs.detail(server?.id || '', albumId); const detailQuery = useSuspenseQuery({ queryFn: ({ signal }) => { @@ -178,20 +179,22 @@ const DummyAlbumDetailRoute = () => { handlePlay()} /> - + {showFavorites && ( + + )} { diff --git a/src/renderer/features/artists/components/album-artist-detail-header.tsx b/src/renderer/features/artists/components/album-artist-detail-header.tsx index 924a20a92..abe165088 100644 --- a/src/renderer/features/artists/components/album-artist-detail-header.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-header.tsx @@ -19,7 +19,7 @@ import { import { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite'; import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating'; import { AppRoute } from '/@/renderer/router/routes'; -import { useAppStore, useCurrentServer, useShowRatings } from '/@/renderer/store'; +import { useAppStore, useCurrentServer, useShowFavorites, useShowRatings } from '/@/renderer/store'; import { useArtistReleaseTypeItems, usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { formatDurationString } from '/@/renderer/utils'; import { hasFeature, SEPARATOR_STRING, sortAlbumList } from '/@/shared/api/utils'; @@ -105,6 +105,7 @@ export const AlbumArtistDetailHeader = forwardRef handlePlay(type)} onRating={showRating ? handleUpdateRating : undefined} diff --git a/src/renderer/features/context-menu/actions/set-favorite-action.tsx b/src/renderer/features/context-menu/actions/set-favorite-action.tsx index 42cbfa178..3236ca24c 100644 --- a/src/renderer/features/context-menu/actions/set-favorite-action.tsx +++ b/src/renderer/features/context-menu/actions/set-favorite-action.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; -import { useCurrentServerId } from '/@/renderer/store'; +import { useCurrentServerId, useShowFavorites } from '/@/renderer/store'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; import { LibraryItem } from '/@/shared/types/domain-types'; @@ -15,6 +15,7 @@ interface SetFavoriteActionProps { export const SetFavoriteAction = ({ ids, itemType }: SetFavoriteActionProps) => { const { t } = useTranslation(); const serverId = useCurrentServerId(); + const showFavorites = useShowFavorites(); const createFavoriteMutation = useCreateFavorite({}); const deleteFavoriteMutation = useDeleteFavorite({}); @@ -43,6 +44,10 @@ export const SetFavoriteAction = ({ ids, itemType }: SetFavoriteActionProps) => }); }, [deleteFavoriteMutation, ids, itemType, serverId]); + if (!showFavorites) { + return null; + } + return ( diff --git a/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx b/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx index ee90fb72b..23efe6568 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx @@ -19,6 +19,7 @@ interface MobileFullscreenPlayerMetadataProps { radioArtist?: string; radioStationName?: string; radioTitle?: string; + showFavorite?: boolean; showRating?: boolean; } @@ -30,6 +31,7 @@ export const MobileFullscreenPlayerMetadata = memo( radioArtist, radioStationName, radioTitle, + showFavorite, showRating, }: MobileFullscreenPlayerMetadataProps) => { const isRadio = radioTitle !== undefined || radioStationName !== undefined; @@ -77,16 +79,18 @@ export const MobileFullscreenPlayerMetadata = memo( )} {!isRadio && ( - + {showFavorite && ( + + )} {showRating && ( )} diff --git a/src/renderer/features/player/components/mobile-fullscreen-player.tsx b/src/renderer/features/player/components/mobile-fullscreen-player.tsx index a61f5bd64..f9c8e9733 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player.tsx @@ -35,10 +35,11 @@ import { useCurrentServer, useFullScreenPlayerStore, useFullScreenPlayerStoreActions, - useGeneralSettings, usePlayerData, usePlayerSong, useSetFullScreenPlayerStore, + useShowFavorites, + useShowRatings, } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Text } from '/@/shared/components/text/text'; @@ -387,7 +388,8 @@ export const MobileFullscreenPlayer = () => { const isPlayingRadio = isRadioActive && isRadioPlaying; const effectiveDynamicBackground = dynamicBackground && !isPlayingRadio; const setFavorite = useSetFavorite(); - const { showRatings: showRatingsSetting } = useGeneralSettings(); + const showRatingsSetting = useShowRatings(); + const showFavorites = useShowFavorites(); const setRating = useSetRating(); const [isPageHovered, setIsPageHovered] = useState(false); @@ -482,6 +484,7 @@ export const MobileFullscreenPlayer = () => { radioArtist={isPlayingRadio ? (radioMetadata?.artist ?? undefined) : undefined} radioStationName={isPlayingRadio ? (stationName ?? undefined) : undefined} radioTitle={isPlayingRadio ? (radioMetadata?.title ?? undefined) : undefined} + showFavorite={showFavorites} showRating={showRating} /> diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index baa285c2d..166c0679f 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -24,7 +24,6 @@ import { useAutoDJSettings, useCurrentServer, useFullScreenPlayerStore, - useGeneralSettings, useHotkeySettings, usePlaybackSettings, usePlaybackType, @@ -34,6 +33,8 @@ import { usePlayerVolume, useSetFullScreenPlayerStore, useSettingsStoreActions, + useShowFavorites, + useShowRatings, useSidebarRightExpanded, useSideQueueType, useVolumeMax, @@ -86,7 +87,8 @@ const calculateVolumeDown = (volume: number, volumeWheelStep: number) => { }; export const RightControls = () => { - const { showRatings } = useGeneralSettings(); + const showRatings = useShowRatings(); + const showFavorites = useShowFavorites(); return ( @@ -97,7 +99,7 @@ export const RightControls = () => { - + {showFavorites && } diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index 1a6ecffdb..2404aa915 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -562,6 +562,26 @@ export const ApplicationSettings = memo(() => { isHidden: settings.sideQueueType !== 'sideQueue', title: t('setting.sidePlayQueueLayout'), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + showFavorites: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.showFavorites', { + context: 'description', + }), + isHidden: false, + title: t('setting.showFavorites'), + }, { control: ( ; export type ItemDetailListProps = z.infer; + export type ItemListSettings = { detail?: ItemDetailListProps; display: ListDisplayType; @@ -1005,7 +1007,6 @@ export type PlayerFilter = z.infer; export type PlayerFilterField = z.infer; export type PlayerFilterOperator = z.infer; - export interface SettingsSlice extends z.infer { actions: { addCollection: (collection: SavedCollection) => void; @@ -1031,6 +1032,7 @@ export interface SettingsSlice extends z.infer { }; } export interface SettingsState extends z.infer {} + export type SidebarItemType = z.infer; export type SideQueueLayout = z.infer; @@ -1314,6 +1316,7 @@ const initialState: SettingsState = { primaryShade: 6, qobuz: true, resume: true, + showFavorites: true, showLyricsInSidebar: true, showQueueInSidebar: true, showRatings: true, @@ -2937,6 +2940,9 @@ export const usePlayerbarOpenDrawer = () => export const useShowRatings = () => useSettingsStore((state) => state.general.showRatings, shallow); +export const useShowFavorites = () => + useSettingsStore((state) => state.general.showFavorites, shallow); + export const useArtistRadioCount = () => useSettingsStore((state) => state.general.artistRadioCount, shallow);