From 82a566aee17ead244d70ced73c81c7f0f80d6eb1 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 29 Dec 2025 20:40:43 -0800 Subject: [PATCH] add artist release type configuration, fix page configurations --- src/i18n/locales/en.json | 2 + .../album-artist-detail-content.tsx | 59 +++++++++++--- .../general/application-settings.tsx | 6 +- .../components/general/artist-settings.tsx | 65 ++++++++------- .../components/general/draggable-items.tsx | 37 +++++++-- .../components/general/home-settings.tsx | 27 +------ .../components/general/sidebar-reorder.tsx | 37 +++++---- src/renderer/store/settings.store.ts | 79 ++++++++++++++++++- 8 files changed, 224 insertions(+), 88 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3b717c657..6776983f5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -698,6 +698,8 @@ "artistBackgroundBlur_description": "adjusts the amount of blur applied to the artist background image", "artistConfiguration": "album artist page configuration", "artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page", + "artistReleaseTypeConfiguration": "artist release type configuration", + "artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page", "audioDevice_description": "select the audio device to use for playback (web player only)", "audioDevice": "audio device", "audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio", diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index fa3c5012f..44e62b894 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -38,6 +38,7 @@ import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; import { AppRoute } from '/@/renderer/router/routes'; import { ArtistItem, + ArtistReleaseTypeItem, useAppStore, useCurrentServer, useCurrentServerId, @@ -998,8 +999,30 @@ const groupAlbumsByReleaseType = ( return grouped; }; +const releaseTypeToEnumMap: Record = { + album: ArtistReleaseTypeItem.RELEASE_TYPE_ALBUM, + 'appears-on': ArtistReleaseTypeItem.APPEARS_ON, + audiobook: ArtistReleaseTypeItem.RELEASE_TYPE_AUDIOBOOK, + 'audio drama': ArtistReleaseTypeItem.RELEASE_TYPE_AUDIO_DRAMA, + broadcast: ArtistReleaseTypeItem.RELEASE_TYPE_BROADCAST, + compilation: ArtistReleaseTypeItem.RELEASE_TYPE_COMPILATION, + demo: ArtistReleaseTypeItem.RELEASE_TYPE_DEMO, + 'dj-mix': ArtistReleaseTypeItem.RELEASE_TYPE_DJ_MIX, + ep: ArtistReleaseTypeItem.RELEASE_TYPE_EP, + 'field recording': ArtistReleaseTypeItem.RELEASE_TYPE_FIELD_RECORDING, + interview: ArtistReleaseTypeItem.RELEASE_TYPE_INTERVIEW, + live: ArtistReleaseTypeItem.RELEASE_TYPE_LIVE, + 'mixtape/street': ArtistReleaseTypeItem.RELEASE_TYPE_MIXTAPE_STREET, + other: ArtistReleaseTypeItem.RELEASE_TYPE_OTHER, + remix: ArtistReleaseTypeItem.RELEASE_TYPE_REMIX, + single: ArtistReleaseTypeItem.RELEASE_TYPE_SINGLE, + soundtrack: ArtistReleaseTypeItem.RELEASE_TYPE_SOUNDTRACK, + spokenword: ArtistReleaseTypeItem.RELEASE_TYPE_SPOKENWORD, +}; + const ArtistAlbums = () => { const { t } = useTranslation(); + const { artistReleaseTypeItems } = useGeneralSettings(); const serverId = useCurrentServerId(); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300); @@ -1042,21 +1065,33 @@ const ArtistAlbums = () => { }, [filteredAndSortedAlbums, routeId, groupingType]); const releaseTypeEntries = useMemo(() => { - const priorityOrder = [ - 'album', - 'ep', - 'single', - 'broadcast', - 'other', - 'compilation', - 'appears-on', - ]; + const enabledReleaseTypeEnums = new Set( + artistReleaseTypeItems.filter((item) => !item.disabled).map((item) => item.id), + ); + + const priorityMap = new Map(); + artistReleaseTypeItems + .filter((item) => !item.disabled) + .forEach((item, index) => { + const releaseTypeKey = Object.keys(releaseTypeToEnumMap).find( + (key) => releaseTypeToEnumMap[key] === item.id, + ); + if (releaseTypeKey) { + priorityMap.set(releaseTypeKey, index); + } + }); + const getPriority = (releaseType: string) => { - const index = priorityOrder.indexOf(releaseType); - return index === -1 ? 999 : index; + return priorityMap.get(releaseType) ?? 999; + }; + + const isReleaseTypeEnabled = (releaseType: string): boolean => { + const enumValue = releaseTypeToEnumMap[releaseType]; + return enumValue ? enabledReleaseTypeEnums.has(enumValue) : false; }; return Object.entries(albumsByReleaseType) + .filter(([releaseType]) => isReleaseTypeEnabled(releaseType)) .map(([releaseType, albums]) => { let displayName: React.ReactNode | string; switch (releaseType) { @@ -1156,7 +1191,7 @@ const ArtistAlbums = () => { return { albums, displayName, releaseType }; }) .sort((a, b) => getPriority(a.releaseType) - getPriority(b.releaseType)); - }, [albumsByReleaseType, t]); + }, [albumsByReleaseType, artistReleaseTypeItems, t]); const cq = useContainerQuery({ '2xl': 1280, diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index 995584de3..9b07b8be3 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -7,7 +7,10 @@ import { useTranslation } from 'react-i18next'; import i18n, { languages } from '/@/i18n/i18n'; import { ImageResolutionSettings } from '/@/renderer/features/settings/components/general/art-resolution-settings'; -import { ArtistSettings } from '/@/renderer/features/settings/components/general/artist-settings'; +import { + ArtistReleaseTypeSettings, + ArtistSettings, +} from '/@/renderer/features/settings/components/general/artist-settings'; import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings'; import { SettingOption, @@ -606,6 +609,7 @@ export const ApplicationSettings = () => { + } options={options} diff --git a/src/renderer/features/settings/components/general/artist-settings.tsx b/src/renderer/features/settings/components/general/artist-settings.tsx index a174e6fb5..a53733448 100644 --- a/src/renderer/features/settings/components/general/artist-settings.tsx +++ b/src/renderer/features/settings/components/general/artist-settings.tsx @@ -1,8 +1,7 @@ -import { useMemo } from 'react'; - import { DraggableItems } from '/@/renderer/features/settings/components/general/draggable-items'; import { ArtistItem, + ArtistReleaseTypeItem, SortableItem, useGeneralSettings, useSettingsStoreActions, @@ -12,7 +11,6 @@ const ARTIST_ITEMS: Array<[ArtistItem, string]> = [ [ArtistItem.BIOGRAPHY, 'table.column.biography'], [ArtistItem.TOP_SONGS, 'page.albumArtistDetail.topSongs'], [ArtistItem.RECENT_ALBUMS, 'page.albumArtistDetail.recentReleases'], - [ArtistItem.COMPILATIONS, 'page.albumArtistDetail.appearsOn'], [ArtistItem.SIMILAR_ARTISTS, 'page.albumArtistDetail.relatedArtists'], ]; @@ -20,36 +18,49 @@ export const ArtistSettings = () => { const { artistItems } = useGeneralSettings(); const { setArtistItems } = useSettingsStoreActions(); - const mergedArtistItems = useMemo(() => { - const settingsMap = new Map( - artistItems.map((item) => [item.id, item as SortableItem]), - ); - - const merged = artistItems.map((item) => ({ - ...item, - id: item.id as ArtistItem, - })); - - ARTIST_ITEMS.forEach(([itemId]) => { - const artistItemId = itemId as ArtistItem; - if (!settingsMap.has(artistItemId)) { - merged.push({ - disabled: true, - id: artistItemId, - }); - } - }); - - return merged; - }, [artistItems]); - return ( []} setItems={setArtistItems} - settings={mergedArtistItems} title="setting.artistConfiguration" /> ); }; + +const ARTIST_RELEASE_TYPE_ITEMS: Array<[ArtistReleaseTypeItem, string]> = [ + [ArtistReleaseTypeItem.APPEARS_ON, 'page.albumArtistDetail.appearsOn'], + [ArtistReleaseTypeItem.RELEASE_TYPE_ALBUM, 'releaseType.primary.album'], + [ArtistReleaseTypeItem.RELEASE_TYPE_EP, 'releaseType.primary.ep'], + [ArtistReleaseTypeItem.RELEASE_TYPE_SINGLE, 'releaseType.primary.single'], + [ArtistReleaseTypeItem.RELEASE_TYPE_BROADCAST, 'releaseType.primary.broadcast'], + [ArtistReleaseTypeItem.RELEASE_TYPE_COMPILATION, 'releaseType.secondary.compilation'], + [ArtistReleaseTypeItem.RELEASE_TYPE_AUDIO_DRAMA, 'releaseType.secondary.audioDrama'], + [ArtistReleaseTypeItem.RELEASE_TYPE_AUDIOBOOK, 'releaseType.secondary.audiobook'], + [ArtistReleaseTypeItem.RELEASE_TYPE_INTERVIEW, 'releaseType.secondary.interview'], + [ArtistReleaseTypeItem.RELEASE_TYPE_LIVE, 'releaseType.secondary.live'], + [ArtistReleaseTypeItem.RELEASE_TYPE_MIXTAPE_STREET, 'releaseType.secondary.mixtape'], + [ArtistReleaseTypeItem.RELEASE_TYPE_OTHER, 'releaseType.primary.other'], + [ArtistReleaseTypeItem.RELEASE_TYPE_REMIX, 'releaseType.secondary.remix'], + [ArtistReleaseTypeItem.RELEASE_TYPE_DJ_MIX, 'releaseType.secondary.djMix'], + [ArtistReleaseTypeItem.RELEASE_TYPE_DEMO, 'releaseType.secondary.demo'], + [ArtistReleaseTypeItem.RELEASE_TYPE_FIELD_RECORDING, 'releaseType.secondary.fieldRecording'], + [ArtistReleaseTypeItem.RELEASE_TYPE_SOUNDTRACK, 'releaseType.secondary.soundtrack'], + [ArtistReleaseTypeItem.RELEASE_TYPE_SPOKENWORD, 'releaseType.secondary.spokenWord'], +]; + +export const ArtistReleaseTypeSettings = () => { + const { artistReleaseTypeItems } = useGeneralSettings(); + const { setArtistReleaseTypeItems } = useSettingsStoreActions(); + + return ( + []} + setItems={setArtistReleaseTypeItems} + title="setting.artistReleaseTypeConfiguration" + /> + ); +}; diff --git a/src/renderer/features/settings/components/general/draggable-items.tsx b/src/renderer/features/settings/components/general/draggable-items.tsx index 948e617c1..927bbc934 100644 --- a/src/renderer/features/settings/components/general/draggable-items.tsx +++ b/src/renderer/features/settings/components/general/draggable-items.tsx @@ -12,16 +12,41 @@ import { Button } from '/@/shared/components/button/button'; export type DraggableItemsProps = { description: string; itemLabels: Array<[K, string]>; + items: T[]; setItems: (items: T[]) => void; - settings: T[]; title: string; }; +const mergeItems = >( + items: T[], + itemLabels: Array<[string, string]>, +): T[] => { + const allItemIds = itemLabels.map(([key]) => key); + + const missingItemIds = allItemIds.filter((id) => !items.some((item) => item.id === id)); + + const merged = [ + ...items, + ...(missingItemIds.map((id) => ({ + disabled: true, + id, + })) as T[]), + ]; + + // Remove any duplicates + const uniqueMerged = merged.filter( + (item, index, self) => index === self.findIndex((t) => t.id === item.id), + ); + + // Remove any that don't match the itemLabels + return uniqueMerged.filter((item) => itemLabels.some(([key]) => key === item.id)); +}; + export const DraggableItems = >({ description, itemLabels, + items, setItems, - settings, title, }: DraggableItemsProps) => { const { t } = useTranslation(); @@ -31,12 +56,12 @@ export const DraggableItems = >({ const translatedItemMap = useMemo( () => Object.fromEntries( - itemLabels.map((label) => [label[0], t(label[1], { postProcess: 'sentenceCase' })]), + itemLabels.map(([key, value]) => [key, t(value, { postProcess: 'sentenceCase' })]), ) as Record, [itemLabels, t], ); - const [localItems, setLocalItems] = useState(settings); + const [localItems, setLocalItems] = useState(mergeItems(items, itemLabels)); const handleChangeDisabled = useCallback((id: string, e: boolean) => { setLocalItems((items) => @@ -68,10 +93,10 @@ export const DraggableItems = >({ }, [description, keyword, title]); if (!shouldShow) { - return <>; + return null; } - const isSaveButtonDisabled = isEqual(settings, localItems); + const isSaveButtonDisabled = isEqual(items, localItems); const handleSave = () => { setItems(localItems); diff --git a/src/renderer/features/settings/components/general/home-settings.tsx b/src/renderer/features/settings/components/general/home-settings.tsx index 4ba73f953..cd4007a36 100644 --- a/src/renderer/features/settings/components/general/home-settings.tsx +++ b/src/renderer/features/settings/components/general/home-settings.tsx @@ -1,5 +1,3 @@ -import { useMemo } from 'react'; - import { DraggableItems } from '/@/renderer/features/settings/components/general/draggable-items'; import { HomeItem, @@ -21,35 +19,12 @@ export const HomeSettings = () => { const { homeItems } = useGeneralSettings(); const { setHomeItems } = useSettingsStoreActions(); - const mergedHomeItems = useMemo(() => { - const settingsMap = new Map( - homeItems.map((item) => [item.id, item as SortableItem]), - ); - - const merged = homeItems.map((item) => ({ - ...item, - id: item.id as HomeItem, - })); - - HOME_ITEMS.forEach(([itemId]) => { - const homeItemId = itemId as HomeItem; - if (!settingsMap.has(homeItemId)) { - merged.push({ - disabled: true, - id: homeItemId, - }); - } - }); - - return merged; - }, [homeItems]); - return ( []} setItems={setHomeItems} - settings={mergedHomeItems} title="setting.homeConfiguration" /> ); diff --git a/src/renderer/features/settings/components/general/sidebar-reorder.tsx b/src/renderer/features/settings/components/general/sidebar-reorder.tsx index dd3da6a96..68f18fc40 100644 --- a/src/renderer/features/settings/components/general/sidebar-reorder.tsx +++ b/src/renderer/features/settings/components/general/sidebar-reorder.tsx @@ -3,24 +3,26 @@ import { useMemo } from 'react'; import { DraggableItems } from '/@/renderer/features/settings/components/general/draggable-items'; import { sidebarItems as defaultSidebarItems, + SidebarItem, + SidebarItemType, useGeneralSettings, useSettingsStoreActions, } from '/@/renderer/store'; const SIDEBAR_ITEMS: Array<[string, string]> = [ - ['Albums', 'page.sidebar.albums'], - ['Artists', 'page.sidebar.albumArtists'], - ['Artists-all', 'page.sidebar.artists'], - ['Favorites', 'page.sidebar.favorites'], - ['Folders', 'page.sidebar.folders'], - ['Genres', 'page.sidebar.genres'], - ['Home', 'page.sidebar.home'], - ['Now Playing', 'page.sidebar.nowPlaying'], - ['Playlists', 'page.sidebar.playlists'], - ['Radio', 'page.sidebar.radio'], - ['Search', 'page.sidebar.search'], - ['Settings', 'page.sidebar.settings'], - ['Tracks', 'page.sidebar.tracks'], + [SidebarItem.ALBUMS, 'page.sidebar.albums'], + [SidebarItem.ARTISTS, 'page.sidebar.albumArtists'], + [SidebarItem.ARTISTS_ALL, 'page.sidebar.artists'], + [SidebarItem.FAVORITES, 'page.sidebar.favorites'], + [SidebarItem.FOLDERS, 'page.sidebar.folders'], + [SidebarItem.GENRES, 'page.sidebar.genres'], + [SidebarItem.HOME, 'page.sidebar.home'], + [SidebarItem.NOW_PLAYING, 'page.sidebar.nowPlaying'], + [SidebarItem.PLAYLISTS, 'page.sidebar.playlists'], + [SidebarItem.RADIO, 'page.sidebar.radio'], + [SidebarItem.SEARCH, 'page.sidebar.search'], + [SidebarItem.SETTINGS, 'page.sidebar.settings'], + [SidebarItem.TRACKS, 'page.sidebar.tracks'], ]; export const SidebarReorder = () => { @@ -48,15 +50,20 @@ export const SidebarReorder = () => { } }); - return merged; + // Remove any duplicates + const uniqueMerged = merged.filter( + (item, index, self) => index === self.findIndex((t) => t.id === item.id), + ); + + return uniqueMerged; }, [sidebarItems]); return ( ); diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index fd899e5f1..38916ef49 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -58,6 +58,27 @@ const ArtistItemSchema = z.enum([ 'topSongs', ]); +const ArtistReleaseTypeItemSchema = z.enum([ + 'releaseTypeAlbum', + 'releaseTypeEp', + 'releaseTypeSingle', + 'releaseTypeBroadcast', + 'releaseTypeOther', + 'releaseTypeCompilation', + 'appearsOn', + 'releaseTypeAudioDrama', + 'releaseTypeAudiobook', + 'releaseTypeDemo', + 'releaseTypeDjMix', + 'releaseTypeFieldRecording', + 'releaseTypeInterview', + 'releaseTypeLive', + 'releaseTypeMixtapeStreet', + 'releaseTypeRemix', + 'releaseTypeSoundtrack', + 'releaseTypeSpokenWord', +]); + const BindingActionsSchema = z.enum([ 'browserBack', 'browserForward', @@ -350,6 +371,7 @@ export const GeneralSettingsSchema = z.object({ artistBackgroundBlur: z.number(), artistItems: z.array(SortableItemSchema(ArtistItemSchema)), artistRadioCount: z.number(), + artistReleaseTypeItems: z.array(SortableItemSchema(ArtistReleaseTypeItemSchema)), buttonSize: z.number(), combinedLyricsAndVisualizer: z.boolean(), disabledContextMenu: z.record(z.string(), z.boolean()), @@ -587,6 +609,27 @@ export enum ArtistItem { TOP_SONGS = 'topSongs', } +export enum ArtistReleaseTypeItem { + APPEARS_ON = 'appearsOn', + RELEASE_TYPE_ALBUM = 'releaseTypeAlbum', + RELEASE_TYPE_AUDIO_DRAMA = 'releaseTypeAudioDrama', + RELEASE_TYPE_AUDIOBOOK = 'releaseTypeAudiobook', + RELEASE_TYPE_BROADCAST = 'releaseTypeBroadcast', + RELEASE_TYPE_COMPILATION = 'releaseTypeCompilation', + RELEASE_TYPE_DEMO = 'releaseTypeDemo', + RELEASE_TYPE_DJ_MIX = 'releaseTypeDjMix', + RELEASE_TYPE_EP = 'releaseTypeEp', + RELEASE_TYPE_FIELD_RECORDING = 'releaseTypeFieldRecording', + RELEASE_TYPE_INTERVIEW = 'releaseTypeInterview', + RELEASE_TYPE_LIVE = 'releaseTypeLive', + RELEASE_TYPE_MIXTAPE_STREET = 'releaseTypeMixtapeStreet', + RELEASE_TYPE_OTHER = 'releaseTypeOther', + RELEASE_TYPE_REMIX = 'releaseTypeRemix', + RELEASE_TYPE_SINGLE = 'releaseTypeSingle', + RELEASE_TYPE_SOUNDTRACK = 'releaseTypeSoundtrack', + RELEASE_TYPE_SPOKENWORD = 'releaseTypeSpokenWord', +} + export enum BarAlign { BOTTOM = 'bottom', CENTER = 'center', @@ -662,6 +705,22 @@ export enum PlayerbarSliderType { WAVEFORM = 'waveform', } +export enum SidebarItem { + ALBUMS = 'Albums', + ARTISTS = 'Artists', + ARTISTS_ALL = 'Artists-all', + FAVORITES = 'Favorites', + FOLDERS = 'Folders', + GENRES = 'Genres', + HOME = 'Home', + NOW_PLAYING = 'Now Playing', + PLAYLISTS = 'Playlists', + RADIO = 'Radio', + SEARCH = 'Search', + SETTINGS = 'Settings', + TRACKS = 'Tracks', +} + export type DataGridProps = { itemGap: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; itemsPerRow: number; @@ -690,6 +749,7 @@ export interface SettingsSlice extends z.infer { reset: () => void; resetSampleRate: () => void; setArtistItems: (item: SortableItem[]) => void; + setArtistReleaseTypeItems: (item: SortableItem[]) => void; setGenreBehavior: (target: GenreTarget) => void; setHomeItems: (item: SortableItem[]) => void; setList: (type: ItemListKey, data: DeepPartial) => void; @@ -707,7 +767,7 @@ export type SidebarItemType = z.infer; export type SideQueueType = z.infer; -export type SortableItem = { +export type SortableItem = { disabled: boolean; id: T; }; @@ -802,6 +862,11 @@ const artistItems = Object.values(ArtistItem).map((item) => ({ id: item, })); +const artistReleaseTypeItems = Object.values(ArtistReleaseTypeItem).map((item) => ({ + disabled: false, + id: item, +})); + // Determines the default/initial windowBarStyle value based on the current platform. const getPlatformDefaultWindowBarStyle = (): Platform => { if (utils?.isWindows()) { @@ -854,6 +919,7 @@ const initialState: SettingsState = { artistBackgroundBlur: 3, artistItems, artistRadioCount: 20, + artistReleaseTypeItems, buttonSize: 15, combinedLyricsAndVisualizer: false, disabledContextMenu: {}, @@ -1513,12 +1579,18 @@ const getInitialState = (): SettingsState => { id: item, })); + const freshArtistReleaseTypeItems = Object.values(ArtistReleaseTypeItem).map((item) => ({ + disabled: false, + id: item, + })); + // Deep clone using JSON to ensure all nested objects/arrays are fresh copies const clonedState = JSON.parse(JSON.stringify(initialState)) as SettingsState; // Replace arrays that need fresh references clonedState.general.homeItems = freshHomeItems; clonedState.general.artistItems = freshArtistItems; + clonedState.general.artistReleaseTypeItems = freshArtistReleaseTypeItems; clonedState.general.sidebarItems = JSON.parse( JSON.stringify(sidebarItems), ) as SidebarItemType[]; @@ -1573,6 +1645,11 @@ export const useSettingsStore = createWithEqualityFn()( state.general.artistItems = items; }); }, + setArtistReleaseTypeItems: (items: SortableItem[]) => { + set((state) => { + state.general.artistReleaseTypeItems = items; + }); + }, setGenreBehavior: (target: GenreTarget) => { set((state) => { state.general.genreTarget = target;