From ab031820f639a4800f447095335dfd9d0156bda6 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Fri, 6 Jan 2023 00:39:49 -0800 Subject: [PATCH] Add favorite/rating table columns --- src/renderer/api/jellyfin.api.ts | 23 ++++++++++---- src/renderer/api/navidrome.api.ts | 27 +++++++++++------ src/renderer/api/normalize.ts | 4 ++- src/renderer/api/types.ts | 25 ++++++++++++---- src/renderer/components/index.ts | 1 + src/renderer/components/rating/index.tsx | 10 +++++++ .../virtual-table/cells/favorite-cell.tsx | 17 +++++++++++ .../virtual-table/cells/rating-cell.tsx | 14 +++++++++ .../headers/generic-table-header.tsx | 10 +++++-- .../components/virtual-table/index.tsx | 30 +++++++++++++++++-- .../virtual-table/table-config-dropdown.tsx | 6 ++-- src/renderer/themes/default.scss | 20 +++++++++++++ 12 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 src/renderer/components/rating/index.tsx create mode 100644 src/renderer/components/virtual-table/cells/favorite-cell.tsx create mode 100644 src/renderer/components/virtual-table/cells/rating-cell.tsx diff --git a/src/renderer/api/jellyfin.api.ts b/src/renderer/api/jellyfin.api.ts index 589e6118..01d3e658 100644 --- a/src/renderer/api/jellyfin.api.ts +++ b/src/renderer/api/jellyfin.api.ts @@ -63,6 +63,7 @@ import { albumArtistListSortMap, UpdatePlaylistArgs, UpdatePlaylistResponse, + LibraryItem, } from '/@/renderer/api/types'; import { useAuthStore } from '/@/renderer/store'; import { ServerListItem, ServerType } from '/@/renderer/types'; @@ -652,7 +653,7 @@ const normalizeSong = ( id: item.Id, imagePlaceholderUrl: null, imageUrl: getSongCoverArtUrl({ baseUrl: server.url, item, size: imageSize || 300 }), - isFavorite: (item.UserData && item.UserData.IsFavorite) || false, + itemType: LibraryItem.SONG, lastPlayedAt: null, name: item.Name, path: (item.MediaSources && item.MediaSources[0]?.Path) || null, @@ -661,6 +662,7 @@ const normalizeSong = ( releaseDate: null, releaseYear: (item.ProductionYear && String(item.ProductionYear)) || null, serverId: server.id, + serverType: ServerType.JELLYFIN, size: item.MediaSources && item.MediaSources[0]?.Size, streamUrl: getStreamUrl({ container: item.MediaSources[0]?.Container, @@ -671,9 +673,10 @@ const normalizeSong = ( server, }), trackNumber: item.IndexNumber, - type: ServerType.JELLYFIN, uniqueId: nanoid(), updatedAt: item.DateCreated, + userFavorite: (item.UserData && item.UserData.IsFavorite) || false, + userRating: null, }; }; @@ -697,19 +700,21 @@ const normalizeAlbum = (item: JFAlbum, server: ServerListItem, imageSize?: numbe size: imageSize || 300, }), isCompilation: null, - isFavorite: item.UserData?.IsFavorite || false, + itemType: LibraryItem.ALBUM, lastPlayedAt: null, name: item.Name, playCount: item.UserData?.PlayCount || 0, - rating: null, releaseDate: item.PremiereDate?.split('T')[0] || null, releaseYear: item.ProductionYear, + serverId: server.id, serverType: ServerType.JELLYFIN, size: null, songCount: item?.ChildCount || null, songs: item.songs?.map((song) => normalizeSong(song, server, '', imageSize)), uniqueId: nanoid(), updatedAt: item?.DateLastMediaAdded || item.DateCreated, + userFavorite: item.UserData?.IsFavorite || false, + userRating: null, }; }; @@ -730,12 +735,15 @@ const normalizeAlbumArtist = ( item, size: imageSize || 300, }), - isFavorite: item.UserData.IsFavorite || false, + itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: null, name: item.Name, playCount: item.UserData.PlayCount, - rating: null, + serverId: server.id, + serverType: ServerType.JELLYFIN, songCount: null, + userFavorite: item.UserData.IsFavorite || false, + userRating: null, }; }; @@ -759,11 +767,14 @@ const normalizePlaylist = ( id: item.Id, imagePlaceholderUrl, imageUrl: imageUrl || null, + itemType: LibraryItem.PLAYLIST, name: item.Name, owner: null, ownerId: null, public: null, rules: null, + serverId: server.id, + serverType: ServerType.JELLYFIN, size: null, songCount: item?.ChildCount || null, sync: null, diff --git a/src/renderer/api/navidrome.api.ts b/src/renderer/api/navidrome.api.ts index 59ecf67b..84cade32 100644 --- a/src/renderer/api/navidrome.api.ts +++ b/src/renderer/api/navidrome.api.ts @@ -72,6 +72,7 @@ import { albumListSortMap, sortOrderMap, User, + LibraryItem, } from '/@/renderer/api/types'; import { toast } from '/@/renderer/components/toast'; import { useAuthStore } from '/@/renderer/store'; @@ -526,7 +527,7 @@ const normalizeSong = ( id, imagePlaceholderUrl, imageUrl, - isFavorite: item.starred, + itemType: LibraryItem.SONG, lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate, name: item.title, path: item.path, @@ -534,12 +535,14 @@ const normalizeSong = ( releaseDate: new Date(item.year, 0, 1).toISOString(), releaseYear: String(item.year), serverId: server.id, + serverType: ServerType.NAVIDROME, size: item.size, streamUrl: `${server.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`, trackNumber: item.trackNumber, - type: ServerType.NAVIDROME, uniqueId: nanoid(), updatedAt: item.updatedAt, + userFavorite: item.starred || false, + userRating: item.rating || null, }; }; @@ -566,25 +569,25 @@ const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: numbe imagePlaceholderUrl, imageUrl, isCompilation: item.compilation, - isFavorite: item.starred, + itemType: LibraryItem.ALBUM, lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate, name: item.name, playCount: item.playCount, - rating: item.rating, releaseDate: new Date(item.minYear, 0, 1).toISOString(), releaseYear: item.minYear, + serverId: server.id, serverType: ServerType.NAVIDROME, size: item.size, songCount: item.songCount, songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined, uniqueId: nanoid(), updatedAt: item.updatedAt, + userFavorite: item.starred, + userRating: item.rating, }; }; -const normalizeAlbumArtist = (item: NDAlbumArtist): AlbumArtist => { - console.log('item :>> ', item); - +const normalizeAlbumArtist = (item: NDAlbumArtist, server: ServerListItem): AlbumArtist => { return { albumCount: item.albumCount, backgroundImageUrl: null, @@ -593,12 +596,15 @@ const normalizeAlbumArtist = (item: NDAlbumArtist): AlbumArtist => { genres: item.genres, id: item.id, imageUrl: item.largeImageUrl || null, - isFavorite: item.starred, + itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate, name: item.name, playCount: item.playCount, - rating: item.rating, + serverId: server.id, + serverType: ServerType.NAVIDROME, songCount: item.songCount, + userFavorite: item.starred, + userRating: item.rating, }; }; @@ -623,11 +629,14 @@ const normalizePlaylist = ( id: item.id, imagePlaceholderUrl, imageUrl, + itemType: LibraryItem.PLAYLIST, name: item.name, owner: item.ownerName, ownerId: item.ownerId, public: item.public, rules: item?.rules || null, + serverId: server.id, + serverType: ServerType.NAVIDROME, size: item.size, songCount: item.songCount, sync: item.sync, diff --git a/src/renderer/api/normalize.ts b/src/renderer/api/normalize.ts index 0e48b40b..c89adeb1 100644 --- a/src/renderer/api/normalize.ts +++ b/src/renderer/api/normalize.ts @@ -161,7 +161,9 @@ const albumArtistList = ( ); break; case 'navidrome': - albumArtists = data?.items.map((item) => ndNormalize.albumArtist(item as NDAlbumArtist)); + albumArtists = data?.items.map((item) => + ndNormalize.albumArtist(item as NDAlbumArtist, server), + ); break; case 'subsonic': break; diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 6958fecc..172f619e 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -163,19 +163,21 @@ export type Album = { imagePlaceholderUrl: string | null; imageUrl: string | null; isCompilation: boolean | null; - isFavorite: boolean; + itemType: LibraryItem.ALBUM; lastPlayedAt: string | null; name: string; playCount: number | null; - rating: number | null; releaseDate: string | null; releaseYear: number | null; + serverId: string; serverType: ServerType; size: number | null; songCount: number | null; songs?: Song[]; uniqueId: string; updatedAt: string; + userFavorite: boolean; + userRating: number | null; } & { songs?: Song[] }; export type Song = { @@ -197,7 +199,7 @@ export type Song = { id: string; imagePlaceholderUrl: string | null; imageUrl: string | null; - isFavorite: boolean; + itemType: LibraryItem.SONG; lastPlayedAt: string | null; name: string; path: string | null; @@ -205,12 +207,14 @@ export type Song = { releaseDate: string | null; releaseYear: string | null; serverId: string; + serverType: ServerType; size: number; streamUrl: string; trackNumber: number; - type: ServerType; uniqueId: string; updatedAt: string; + userFavorite: boolean; + userRating: number | null; }; export type AlbumArtist = { @@ -221,12 +225,15 @@ export type AlbumArtist = { genres: Genre[]; id: string; imageUrl: string | null; - isFavorite: boolean; + itemType: LibraryItem.ALBUM_ARTIST; lastPlayedAt: string | null; name: string; playCount: number | null; - rating: number | null; + serverId: string; + serverType: ServerType; songCount: number | null; + userFavorite: boolean; + userRating: number | null; }; export type RelatedAlbumArtist = { @@ -238,9 +245,12 @@ export type Artist = { biography: string | null; createdAt: string; id: string; + itemType: LibraryItem.ARTIST; name: string; remoteCreatedAt: string | null; serverFolderId: string; + serverId: string; + serverType: ServerType; updatedAt: string; }; @@ -261,11 +271,14 @@ export type Playlist = { id: string; imagePlaceholderUrl: string | null; imageUrl: string | null; + itemType: LibraryItem.PLAYLIST; name: string; owner: string | null; ownerId: string | null; public: boolean | null; rules?: Record | null; + serverId: string; + serverType: ServerType; size: number | null; songCount: number | null; sync?: boolean | null; diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index 273f7ed1..a28e0cff 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -32,3 +32,4 @@ export * from './virtual-table'; export * from './motion'; export * from './context-menu'; export * from './query-builder'; +export * from './rating'; diff --git a/src/renderer/components/rating/index.tsx b/src/renderer/components/rating/index.tsx new file mode 100644 index 00000000..3a592a7d --- /dev/null +++ b/src/renderer/components/rating/index.tsx @@ -0,0 +1,10 @@ +import { Rating as MantineRating, RatingProps as MantineRatingProps } from '@mantine/core'; +import styled from 'styled-components'; + +type RatingProps = MantineRatingProps; + +const StyledRating = styled(MantineRating)``; + +export const Rating = ({ ...props }: RatingProps) => { + return ; +}; diff --git a/src/renderer/components/virtual-table/cells/favorite-cell.tsx b/src/renderer/components/virtual-table/cells/favorite-cell.tsx new file mode 100644 index 00000000..e834a68c --- /dev/null +++ b/src/renderer/components/virtual-table/cells/favorite-cell.tsx @@ -0,0 +1,17 @@ +import type { ICellRendererParams } from '@ag-grid-community/core'; +import { RiHeartFill, RiHeartLine } from 'react-icons/ri'; +import { Button } from '/@/renderer/components/button'; +import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; + +export const FavoriteCell = ({ value }: ICellRendererParams) => { + return ( + + + + ); +}; diff --git a/src/renderer/components/virtual-table/cells/rating-cell.tsx b/src/renderer/components/virtual-table/cells/rating-cell.tsx new file mode 100644 index 00000000..69ef22b3 --- /dev/null +++ b/src/renderer/components/virtual-table/cells/rating-cell.tsx @@ -0,0 +1,14 @@ +import type { ICellRendererParams } from '@ag-grid-community/core'; +import { Rating } from '/@/renderer/components/rating'; +import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; + +export const RatingCell = ({ value }: ICellRendererParams) => { + return ( + + + + ); +}; diff --git a/src/renderer/components/virtual-table/headers/generic-table-header.tsx b/src/renderer/components/virtual-table/headers/generic-table-header.tsx index ddbd67f9..a301fc1a 100644 --- a/src/renderer/components/virtual-table/headers/generic-table-header.tsx +++ b/src/renderer/components/virtual-table/headers/generic-table-header.tsx @@ -2,10 +2,11 @@ import type { ReactNode } from 'react'; import type { IHeaderParams } from '@ag-grid-community/core'; import { AiOutlineNumber } from 'react-icons/ai'; import { FiClock } from 'react-icons/fi'; +import { RiHeartLine, RiStarLine } from 'react-icons/ri'; import styled from 'styled-components'; import { _Text } from '/@/renderer/components/text'; -type Presets = 'duration' | 'rowIndex'; +type Presets = 'duration' | 'rowIndex' | 'userFavorite' | 'userRating'; type Options = { children?: ReactNode; @@ -39,7 +40,12 @@ const TextHeaderWrapper = styled(_Text)<{ position: Options['position'] }>` text-transform: uppercase; `; -const headerPresets = { duration: , rowIndex: }; +const headerPresets = { + duration: , + rowIndex: , + userFavorite: , + userRating: , +}; export const GenericTableHeader = ( { displayName }: IHeaderParams, diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index d4a48198..6231b60d 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -1,5 +1,4 @@ -import type { Ref } from 'react'; -import { forwardRef, useRef } from 'react'; +import { Ref, forwardRef, useRef } from 'react'; import type { ICellRendererParams, ValueGetterParams, @@ -25,6 +24,8 @@ import { GenericTableHeader } from '/@/renderer/components/virtual-table/headers import { AppRoute } from '/@/renderer/router/routes'; import { PersistedTableColumn } from '/@/renderer/store/settings.store'; import { TableColumn } from '/@/renderer/types'; +import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell'; +import { FavoriteCell } from '/@/renderer/components/virtual-table/cells/favorite-cell'; export * from './table-config-dropdown'; export * from './table-pagination'; @@ -266,6 +267,31 @@ const tableColumns: { [key: string]: ColDef } = { valueGetter: (params: ValueGetterParams) => (params.data ? params.data.trackNumber : undefined), width: 80, }, + userFavorite: { + cellClass: (params) => (params.value ? 'visible ag-cell-favorite' : 'ag-cell-favorite'), + cellRenderer: FavoriteCell, + colId: TableColumn.USER_FAVORITE, + field: 'userFavorite', + headerComponent: (params: IHeaderParams) => + GenericTableHeader(params, { position: 'center', preset: 'userFavorite' }), + headerName: 'Favorite', + suppressSizeToFit: true, + valueGetter: (params: ValueGetterParams) => + params.data ? params.data.userFavorite : undefined, + width: 50, + }, + userRating: { + cellClass: (params) => (params.value ? 'visible ag-cell-rating' : 'ag-cell-rating'), + cellRenderer: RatingCell, + colId: TableColumn.USER_RATING, + field: 'userRating', + headerComponent: (params: IHeaderParams) => + GenericTableHeader(params, { position: 'center', preset: 'userRating' }), + headerName: 'Rating', + suppressSizeToFit: true, + valueGetter: (params: ValueGetterParams) => (params.data ? params.data.userRating : undefined), + width: 95, + }, }; export const getColumnDef = (column: TableColumn) => { diff --git a/src/renderer/components/virtual-table/table-config-dropdown.tsx b/src/renderer/components/virtual-table/table-config-dropdown.tsx index 139aa46e..31cdff36 100644 --- a/src/renderer/components/virtual-table/table-config-dropdown.tsx +++ b/src/renderer/components/virtual-table/table-config-dropdown.tsx @@ -28,9 +28,9 @@ export const SONG_TABLE_COLUMNS = [ { label: 'Date Added', value: TableColumn.DATE_ADDED }, { label: 'Path', value: TableColumn.PATH }, { label: 'Plays', value: TableColumn.PLAY_COUNT }, - // { label: 'Favorite', value: TableColumn.FAVORITE }, - // { label: 'Rating', value: TableColumn.RATING }, - // { label: 'Size', value: TableColumn.SIZE }, + { label: 'Favorite', value: TableColumn.USER_FAVORITE }, + { label: 'Rating', value: TableColumn.USER_RATING }, + { label: 'Size', value: TableColumn.SIZE }, // { label: 'Skip', value: TableColumn.SKIP }, ]; diff --git a/src/renderer/themes/default.scss b/src/renderer/themes/default.scss index 7f60590b..869bf4fb 100644 --- a/src/renderer/themes/default.scss +++ b/src/renderer/themes/default.scss @@ -125,6 +125,26 @@ margin: 0.5rem 0.5rem; } + .ag-cell-rating, + .ag-cell-favorite { + display: none; + } + + .ag-cell-rating.visible { + display: block; + } + + .ag-cell-favorite.visible { + display: block; + } + + .ag-row-hover { + .ag-cell-rating, + .ag-cell-favorite { + display: block; + } + } + .ag-header-cell, .ag-header-group-cell { padding-left: 0.5rem;