From 5516daab6ea11c22b6e7e3e10cf2d97ea059fd43 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:46:06 -0800 Subject: [PATCH 1/7] enable comments, safer note --- .../api/jellyfin/jellyfin-normalize.ts | 1 + .../api/navidrome/navidrome-normalize.ts | 1 + src/renderer/api/navidrome/navidrome-types.ts | 1 + .../api/subsonic/subsonic-normalize.ts | 1 + src/renderer/api/types.ts | 1 + .../virtual-table/cells/note-cell.tsx | 17 ++------- .../components/album-detail-content.tsx | 16 +++++++- src/renderer/utils/linkify.tsx | 38 +++++++++++++++++++ 8 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 src/renderer/utils/linkify.tsx diff --git a/src/renderer/api/jellyfin/jellyfin-normalize.ts b/src/renderer/api/jellyfin/jellyfin-normalize.ts index 4d844174..ffef489a 100644 --- a/src/renderer/api/jellyfin/jellyfin-normalize.ts +++ b/src/renderer/api/jellyfin/jellyfin-normalize.ts @@ -214,6 +214,7 @@ const normalizeAlbum = ( name: entry.Name, })), backdropImageUrl: null, + comment: null, createdAt: item.DateCreated, duration: item.RunTimeTicks / 10000, genres: item.GenreItems?.map((entry) => ({ diff --git a/src/renderer/api/navidrome/navidrome-normalize.ts b/src/renderer/api/navidrome/navidrome-normalize.ts index 4aafb14e..717ba2ca 100644 --- a/src/renderer/api/navidrome/navidrome-normalize.ts +++ b/src/renderer/api/navidrome/navidrome-normalize.ts @@ -154,6 +154,7 @@ const normalizeAlbum = ( albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }], artists: [{ id: item.artistId, imageUrl: null, name: item.artist }], backdropImageUrl: imageBackdropUrl, + comment: item.comment || null, createdAt: item.createdAt.split('T')[0], duration: item.duration * 1000 || null, genres: item.genres?.map((genre) => ({ diff --git a/src/renderer/api/navidrome/navidrome-types.ts b/src/renderer/api/navidrome/navidrome-types.ts index d01174f6..b63b5442 100644 --- a/src/renderer/api/navidrome/navidrome-types.ts +++ b/src/renderer/api/navidrome/navidrome-types.ts @@ -111,6 +111,7 @@ const album = z.object({ allArtistIds: z.string(), artist: z.string(), artistId: z.string(), + comment: z.string().optional(), compilation: z.boolean(), coverArtId: z.string().optional(), // Removed after v0.48.0 coverArtPath: z.string().optional(), // Removed after v0.48.0 diff --git a/src/renderer/api/subsonic/subsonic-normalize.ts b/src/renderer/api/subsonic/subsonic-normalize.ts index 881e7fef..31c2f6c8 100644 --- a/src/renderer/api/subsonic/subsonic-normalize.ts +++ b/src/renderer/api/subsonic/subsonic-normalize.ts @@ -155,6 +155,7 @@ const normalizeAlbum = ( : [], artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [], backdropImageUrl: null, + comment: null, createdAt: item.created, duration: item.duration, genres: item.genre diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 5165c7fb..3abf9f64 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -147,6 +147,7 @@ export type Album = { albumArtists: RelatedArtist[]; artists: RelatedArtist[]; backdropImageUrl: string | null; + comment: string | null; createdAt: string; duration: number | null; genres: Genre[]; diff --git a/src/renderer/components/virtual-table/cells/note-cell.tsx b/src/renderer/components/virtual-table/cells/note-cell.tsx index 1191d597..eaa1b5f7 100644 --- a/src/renderer/components/virtual-table/cells/note-cell.tsx +++ b/src/renderer/components/virtual-table/cells/note-cell.tsx @@ -3,17 +3,7 @@ import { Skeleton } from '/@/renderer/components/skeleton'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; import { useMemo } from 'react'; import { Text } from '/@/renderer/components/text'; - -const URL_REGEX = - /((?:https?:\/\/)?(?:[\w-]{1,32}(?:\.[\w-]{1,32})+)(?:\/[\w\-./?%&=][^.|^\s]*)?)/g; - -const replaceURLWithHTMLLinks = (text: string) => { - const urlRegex = new RegExp(URL_REGEX, 'g'); - return text.replaceAll( - urlRegex, - (url) => `${url}`, - ); -}; +import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; export const NoteCell = ({ value }: ICellRendererParams) => { const formattedValue = useMemo(() => { @@ -39,9 +29,10 @@ export const NoteCell = ({ value }: ICellRendererParams) => { + > + {formattedValue} + ); }; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index d158d908..97addc46 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -1,7 +1,7 @@ import { MutableRefObject, useCallback, useMemo } from 'react'; import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { Box, Group, Stack } from '@mantine/core'; +import { Box, Group, Spoiler, Stack } from '@mantine/core'; import { useSetState } from '@mantine/hooks'; import { useTranslation } from 'react-i18next'; import { RiHeartFill, RiHeartLine, RiMoreFill, RiSettings2Fill } from 'react-icons/ri'; @@ -41,6 +41,7 @@ import { useTableSettings, } from '/@/renderer/store/settings.store'; import { Play } from '/@/renderer/types'; +import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; const isFullWidthRow = (node: RowNode) => { return node.id?.startsWith('disc-'); @@ -279,6 +280,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP }; const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false; + const comment = detailQuery?.data?.comment; const handleGeneralContextMenu = useHandleGeneralContextMenu( LibraryItem.ALBUM, @@ -395,6 +397,18 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP )} + {comment && ( + + + {replaceURLWithHTMLLinks(comment)} + + + )} { + const urlRegex = new RegExp(URL_REGEX, 'g'); + const matches = text.matchAll(urlRegex); + const elements = []; + let lastIndex = 0; + + for (const match of matches) { + const position = match.index!; + + if (position > lastIndex) { + elements.push(text.substring(lastIndex, position)); + } + + const link = match[0]; + elements.push( + + {link} + , + ); + + lastIndex = position + link.length; + } + + if (text.length > lastIndex) { + elements.push(text.substring(lastIndex)); + } + + return elements; +}; From ea67a1896272b9384a4d9f44dfdccf0c533c11d1 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:10:50 -0800 Subject: [PATCH 2/7] include lastfm/mbz links --- src/i18n/locales/en.json | 8 ++- .../api/jellyfin/jellyfin-normalize.ts | 3 ++ src/renderer/api/jellyfin/jellyfin-types.ts | 7 +++ .../api/navidrome/navidrome-normalize.ts | 3 ++ .../api/subsonic/subsonic-normalize.ts | 3 ++ src/renderer/api/types.ts | 3 ++ .../components/album-detail-content.tsx | 46 ++++++++++++++++ .../album-artist-detail-content.tsx | 54 ++++++++++++++++++- .../components/general/control-settings.tsx | 20 +++++++ src/renderer/store/settings.store.ts | 2 + 10 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3997bd64..aaa91ae7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -16,7 +16,11 @@ "removeFromQueue": "remove from queue", "setRating": "set rating", "toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor", - "viewPlaylists": "view $t(entity.playlist_other)" + "viewPlaylists": "view $t(entity.playlist_other)", + "openIn": { + "lastfm": "Open in Last.fm", + "musicbrainz": "Open in MusicBrainz" + } }, "common": { "action_one": "action", @@ -414,6 +418,8 @@ "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", "enableRemote": "enable remote control server", "enableRemote_description": "enables the remote control server to allow other devices to control the application", + "externalLinks": "show external links", + "externalLinks_description": "enables showing external links (Last.fm, MusicBrainz) on artist/album pages", "exitToTray": "exit to tray", "exitToTray_description": "exit the application to the system tray", "floatingQueueArea": "show floating queue hover area", diff --git a/src/renderer/api/jellyfin/jellyfin-normalize.ts b/src/renderer/api/jellyfin/jellyfin-normalize.ts index ffef489a..a351d6c9 100644 --- a/src/renderer/api/jellyfin/jellyfin-normalize.ts +++ b/src/renderer/api/jellyfin/jellyfin-normalize.ts @@ -202,6 +202,7 @@ const normalizeAlbum = ( imageSize?: number, ): Album => { return { + albumArtist: item.AlbumArtist, albumArtists: item.AlbumArtists.map((entry) => ({ id: entry.Id, @@ -233,6 +234,7 @@ const normalizeAlbum = ( isCompilation: null, itemType: LibraryItem.ALBUM, lastPlayedAt: null, + mbzId: item.ProviderIds?.MusicBrainzAlbum || null, name: item.Name, playCount: item.UserData?.PlayCount || 0, releaseDate: item.PremiereDate?.split('T')[0] || null, @@ -288,6 +290,7 @@ const normalizeAlbumArtist = ( }), itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: null, + mbz: item.ProviderIds?.MusicBrainzArtist || null, name: item.Name, playCount: item.UserData?.PlayCount || 0, serverId: server?.id || '', diff --git a/src/renderer/api/jellyfin/jellyfin-types.ts b/src/renderer/api/jellyfin/jellyfin-types.ts index 9721ae8a..41ec5da3 100644 --- a/src/renderer/api/jellyfin/jellyfin-types.ts +++ b/src/renderer/api/jellyfin/jellyfin-types.ts @@ -422,6 +422,11 @@ const song = z.object({ UserData: userData.optional(), }); +const providerIds = z.object({ + MusicBrainzAlbum: z.string().optional(), + MusicBrainzArtist: z.string().optional(), +}); + const albumArtist = z.object({ BackdropImageTags: z.array(z.string()), ChannelId: z.null(), @@ -435,6 +440,7 @@ const albumArtist = z.object({ LocationType: z.string(), Name: z.string(), Overview: z.string(), + ProviderIds: providerIds.optional(), RunTimeTicks: z.number(), ServerId: z.string(), Type: z.string(), @@ -466,6 +472,7 @@ const album = z.object({ ParentLogoItemId: z.string(), PremiereDate: z.string().optional(), ProductionYear: z.number(), + ProviderIds: providerIds.optional(), RunTimeTicks: z.number(), ServerId: z.string(), Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail diff --git a/src/renderer/api/navidrome/navidrome-normalize.ts b/src/renderer/api/navidrome/navidrome-normalize.ts index 717ba2ca..8ccc4210 100644 --- a/src/renderer/api/navidrome/navidrome-normalize.ts +++ b/src/renderer/api/navidrome/navidrome-normalize.ts @@ -151,6 +151,7 @@ const normalizeAlbum = ( const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null; return { + albumArtist: item.albumArtist, albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }], artists: [{ id: item.artistId, imageUrl: null, name: item.artist }], backdropImageUrl: imageBackdropUrl, @@ -169,6 +170,7 @@ const normalizeAlbum = ( isCompilation: item.compilation, itemType: LibraryItem.ALBUM, lastPlayedAt: normalizePlayDate(item), + mbzId: item.mbzAlbumId || null, name: item.name, playCount: item.playCount, releaseDate: new Date(item.minYear, 0, 1).toISOString(), @@ -217,6 +219,7 @@ const normalizeAlbumArtist = ( imageUrl: imageUrl || null, itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: normalizePlayDate(item), + mbz: item.mbzArtistId || null, name: item.name, playCount: item.playCount, serverId: server?.id || 'unknown', diff --git a/src/renderer/api/subsonic/subsonic-normalize.ts b/src/renderer/api/subsonic/subsonic-normalize.ts index 31c2f6c8..58c30026 100644 --- a/src/renderer/api/subsonic/subsonic-normalize.ts +++ b/src/renderer/api/subsonic/subsonic-normalize.ts @@ -126,6 +126,7 @@ const normalizeAlbumArtist = ( imageUrl, itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: null, + mbz: null, name: item.name, playCount: null, serverId: server?.id || 'unknown', @@ -150,6 +151,7 @@ const normalizeAlbum = ( }) || null; return { + albumArtist: item.artist, albumArtists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [], @@ -174,6 +176,7 @@ const normalizeAlbum = ( isCompilation: null, itemType: LibraryItem.ALBUM, lastPlayedAt: null, + mbzId: null, name: item.name, playCount: null, releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 3abf9f64..8193dcd1 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -144,6 +144,7 @@ export type Genre = { }; export type Album = { + albumArtist: string; albumArtists: RelatedArtist[]; artists: RelatedArtist[]; backdropImageUrl: string | null; @@ -157,6 +158,7 @@ export type Album = { isCompilation: boolean | null; itemType: LibraryItem.ALBUM; lastPlayedAt: string | null; + mbzId: string | null; name: string; playCount: number | null; releaseDate: string | null; @@ -229,6 +231,7 @@ export type AlbumArtist = { imageUrl: string | null; itemType: LibraryItem.ALBUM_ARTIST; lastPlayedAt: string | null; + mbz: string | null; name: string; playCount: number | null; serverId: string; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 97addc46..48b154bb 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -4,7 +4,9 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { Box, Group, Spoiler, Stack } from '@mantine/core'; import { useSetState } from '@mantine/hooks'; import { useTranslation } from 'react-i18next'; +import { FaLastfmSquare } from 'react-icons/fa'; import { RiHeartFill, RiHeartLine, RiMoreFill, RiSettings2Fill } from 'react-icons/ri'; +import { SiMusicbrainz } from 'react-icons/si'; import { generatePath, useParams } from 'react-router'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; @@ -36,6 +38,7 @@ import { useAppFocus, useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useCurrentSong, useCurrentStatus } from '/@/renderer/store'; import { + useGeneralSettings, usePlayButtonBehavior, useSettingsStoreActions, useTableSettings, @@ -76,6 +79,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP const status = useCurrentStatus(); const isFocused = useAppFocus(); const currentSong = useCurrentSong(); + const { externalLinks } = useGeneralSettings(); const columnDefs = useMemo( () => getColumnDefs(tableConfig.columns, false, 'albumDetail'), @@ -315,6 +319,8 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); + const mbzId = detailQuery?.data?.mbzId; + return ( @@ -397,6 +403,46 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP )} + {externalLinks ? ( + + + + {mbzId ? ( + + ) : null} + + + ) : null} {comment && ( { + const { t } = useTranslation(); + const { externalLinks } = useGeneralSettings(); const { albumArtistId } = useParams() as { albumArtistId: string }; const cq = useContainerQuery(); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -324,6 +329,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten detailQuery?.data?.biography !== undefined && detailQuery?.data?.biography !== null; const showTopSongs = topSongsQuery?.data?.items?.length; const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false; + const mbzId = detailQuery?.data?.mbz; const isLoading = detailQuery?.isLoading || @@ -411,6 +417,50 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten ) : null} + {externalLinks ? ( + + + + {mbzId ? ( + + ) : null} + + + ) : null} {showBiography ? ( { isHidden: false, title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + externalLinks: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.externalLinks', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.externalLinks', { postProcess: 'sentenceCase' }), + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 75144475..a795178f 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -170,6 +170,7 @@ export interface SettingsState { general: { accent: string; defaultFullPlaylist: boolean; + externalLinks: boolean; followSystemTheme: boolean; language: string; playButtonBehavior: Play; @@ -281,6 +282,7 @@ const initialState: SettingsState = { general: { accent: 'rgb(53, 116, 252)', defaultFullPlaylist: true, + externalLinks: false, followSystemTheme: false, language: 'en', playButtonBehavior: Play.NOW, From 6aba41c3d94eca1637ce21a0dd98d2d0f8d9fc56 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 1 Feb 2024 03:58:36 -0800 Subject: [PATCH 3/7] Add spoiler UI component --- src/renderer/components/index.ts | 1 + src/renderer/components/spoiler/index.tsx | 41 +++++++++++++++++++ .../components/spoiler/spoiler.module.scss | 9 ++++ 3 files changed, 51 insertions(+) create mode 100644 src/renderer/components/spoiler/index.tsx create mode 100644 src/renderer/components/spoiler/spoiler.module.scss diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index 226c5883..7d3b252f 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -27,6 +27,7 @@ export * from './select'; export * from './skeleton'; export * from './slider'; export * from './spinner'; +export * from './spoiler'; export * from './switch'; export * from './tabs'; export * from './text'; diff --git a/src/renderer/components/spoiler/index.tsx b/src/renderer/components/spoiler/index.tsx new file mode 100644 index 00000000..ef7a5d2d --- /dev/null +++ b/src/renderer/components/spoiler/index.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react'; +import { Spoiler as MantineSpoiler } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import styles from './spoiler.module.scss'; + +type SpoilerProps = { + children: ReactNode; + hideLabel?: boolean; + initialState?: boolean; + maxHeight: number; + showLabel?: ReactNode; + transitionDuration?: number; +}; + +export const Spoiler = ({ + hideLabel, + initialState, + maxHeight, + showLabel, + transitionDuration, + children, +}: SpoilerProps) => { + const { t } = useTranslation(); + + return ( + + {children} + + ); +}; diff --git a/src/renderer/components/spoiler/spoiler.module.scss b/src/renderer/components/spoiler/spoiler.module.scss new file mode 100644 index 00000000..a7b51068 --- /dev/null +++ b/src/renderer/components/spoiler/spoiler.module.scss @@ -0,0 +1,9 @@ +.control { + color: var(--btn-subtle-fg); + font-weight: 600; +} + +.control:hover { + color: var(--btn-subtle-fg-hover); + text-decoration: none; +} From 3daa1aef4bd6622fa0399377b49b27356e8fa245 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 1 Feb 2024 04:06:56 -0800 Subject: [PATCH 4/7] Normalize section spacing on album/artist detail pages --- .../components/album-detail-content.tsx | 20 +--- .../album-artist-detail-content.tsx | 93 +++++++++---------- 2 files changed, 48 insertions(+), 65 deletions(-) diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 48b154bb..d75216ee 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -1,7 +1,7 @@ import { MutableRefObject, useCallback, useMemo } from 'react'; import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { Box, Group, Spoiler, Stack } from '@mantine/core'; +import { Box, Group, Stack } from '@mantine/core'; import { useSetState } from '@mantine/hooks'; import { useTranslation } from 'react-i18next'; import { FaLastfmSquare } from 'react-icons/fa'; @@ -12,7 +12,7 @@ import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { queryKeys } from '/@/renderer/api/query-keys'; import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types'; -import { Button, Popover } from '/@/renderer/components'; +import { Button, Popover, Spoiler } from '/@/renderer/components'; import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel'; import { TableConfigDropdown, @@ -58,6 +58,7 @@ const ContentContainer = styled.div` const DetailContainer = styled.div` display: flex; flex-direction: column; + gap: 2rem; padding: 1rem 2rem 5rem; overflow: hidden; `; @@ -328,7 +329,6 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP @@ -380,10 +380,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP {showGenres && ( - + {detailQuery?.data?.genres?.map((genre) => ( - - - - + + handlePlay(playButtonBehavior)} /> + - + + + + + {showGenres ? ( @@ -418,11 +415,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten ) : null} {externalLinks ? ( - +