diff --git a/src/renderer/api/jellyfin.api.ts b/src/renderer/api/jellyfin.api.ts index 37a30730..f939cdcf 100644 --- a/src/renderer/api/jellyfin.api.ts +++ b/src/renderer/api/jellyfin.api.ts @@ -848,7 +848,7 @@ const normalizeAlbum = (item: JFAlbum, server: ServerListItem, imageSize?: numbe name: item.Name, playCount: item.UserData?.PlayCount || 0, releaseDate: item.PremiereDate?.split('T')[0] || null, - releaseYear: item.ProductionYear, + releaseYear: item.ProductionYear || null, serverId: server.id, serverType: ServerType.JELLYFIN, size: null, diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 75b62635..ca004dfb 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -7,7 +7,6 @@ import { ModalsProvider } from '@mantine/modals'; import { initSimpleImg } from 'react-simple-img'; import { BaseContextModal } from './components'; import { useTheme } from './hooks'; -import { Notifications } from '@mantine/notifications'; import { AppRouter } from './router/app-router'; import { useSettingsStore } from './store/settings.store'; import './styles/global.scss'; @@ -38,7 +37,21 @@ export const App = () => { withNormalizeCSS theme={{ colorScheme: theme as 'light' | 'dark', - components: { Modal: { styles: { body: { padding: '.5rem' } } } }, + components: { + Modal: { + styles: { + body: { background: 'var(--modal-bg)', padding: '1rem !important' }, + close: { marginRight: '0.5rem' }, + content: { borderRadius: '10px' }, + header: { + background: 'var(--modal-bg)', + borderBottom: '1px solid var(--generic-border-color)', + paddingBottom: '1rem', + }, + title: { fontSize: 'medium', fontWeight: 'bold' }, + }, + }, + }, defaultRadius: 'xs', dir: 'ltr', focusRing: 'auto', @@ -85,8 +98,8 @@ export const App = () => { { > - diff --git a/src/renderer/components/dropdown-menu/index.tsx b/src/renderer/components/dropdown-menu/index.tsx index 6a317e38..fa808729 100644 --- a/src/renderer/components/dropdown-menu/index.tsx +++ b/src/renderer/components/dropdown-menu/index.tsx @@ -6,7 +6,6 @@ import type { MenuDropdownProps as MantineMenuDropdownProps, } from '@mantine/core'; import { Menu as MantineMenu, createPolymorphicComponent } from '@mantine/core'; -import { RiArrowLeftSFill } from 'react-icons/ri'; import styled from 'styled-components'; type MenuProps = MantineMenuProps; @@ -56,18 +55,10 @@ const StyledMenuItem = styled(MantineMenu.Item)` background-color: var(--dropdown-menu-bg-hover); } - & .mantine-Menu-itemIcon { - margin-right: 1rem; - } - & .mantine-Menu-itemLabel { + margin-right: 2rem; + margin-left: 1rem; color: ${(props) => (props.$danger ? 'var(--danger-color)' : 'var(--dropdown-menu-fg)')}; - font-weight: 500; - } - - & .mantine-Menu-itemRightSection { - display: flex; - margin-left: 2rem !important; } cursor: default; @@ -110,7 +101,7 @@ const pMenuItem = ({ $isActive, $danger, children, ...props }: MenuItemProps) => } + // rightSection={$isActive && } {...props} > {children} diff --git a/src/renderer/components/page-header/index.tsx b/src/renderer/components/page-header/index.tsx index 60dd4483..86367139 100644 --- a/src/renderer/components/page-header/index.tsx +++ b/src/renderer/components/page-header/index.tsx @@ -12,6 +12,7 @@ const Container = styled(motion(Flex))<{ z-index: 2000; width: 100%; height: ${(props) => props.height || '65px'}; + background: var(--titlebar-bg); `; const Header = styled(motion.div)<{ $isHidden?: boolean; $padRight?: boolean }>` @@ -19,7 +20,7 @@ const Header = styled(motion.div)<{ $isHidden?: boolean; $padRight?: boolean }>` z-index: 15; width: 100%; height: 100%; - margin-right: ${(props) => props.$padRight && '170px'}; + margin-right: ${(props) => (props.$padRight ? '140px' : '1rem')}; user-select: ${(props) => (props.$isHidden ? 'none' : 'auto')}; pointer-events: ${(props) => (props.$isHidden ? 'none' : 'auto')}; -webkit-app-region: drag; @@ -66,6 +67,7 @@ export interface PageHeaderProps const TitleWrapper = styled(motion.div)` position: absolute; + display: flex; width: 100%; height: 100%; `; diff --git a/src/renderer/components/query-builder/index.tsx b/src/renderer/components/query-builder/index.tsx index f12cb424..5707e553 100644 --- a/src/renderer/components/query-builder/index.tsx +++ b/src/renderer/components/query-builder/index.tsx @@ -1,7 +1,7 @@ import { Group, Stack } from '@mantine/core'; import { Select } from '/@/renderer/components/select'; import { AnimatePresence, motion } from 'framer-motion'; -import { RiAddLine, RiMore2Line } from 'react-icons/ri'; +import { RiAddFill, RiAddLine, RiDeleteBinFill, RiMore2Line, RiRestartLine } from 'react-icons/ri'; import { Button } from '/@/renderer/components/button'; import { DropdownMenu } from '/@/renderer/components/dropdown-menu'; import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option'; @@ -87,8 +87,11 @@ export const QueryBuilder = ({ }; return ( - - + + ); @@ -178,14 +182,17 @@ export const QueryBuilderOption = ({ const ml = (level + 1) * 10; return ( - + {field ? ( @@ -204,7 +211,7 @@ export const QueryBuilderOption = ({ maxWidth={170} size="sm" type={operator === 'inTheRange' ? 'dateRange' : fieldType} - width="20%" + width="25%" onChange={handleChangeValue} /> ) : ( @@ -213,7 +220,7 @@ export const QueryBuilderOption = ({ defaultValue={value} maxWidth={170} size="sm" - width="20%" + width="25%" onChange={handleChangeValue} /> )} diff --git a/src/renderer/components/scroll-area/index.tsx b/src/renderer/components/scroll-area/index.tsx index 4fc2dfc6..fabb0b25 100644 --- a/src/renderer/components/scroll-area/index.tsx +++ b/src/renderer/components/scroll-area/index.tsx @@ -31,11 +31,11 @@ const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string }>` overflow-y: overlay !important; &::-webkit-scrollbar-track { - margin-top: ${(props) => props.scrollBarOffset || '35px'}; + margin-top: ${(props) => props.scrollBarOffset || '65px'}; } &::-webkit-scrollbar-thumb { - margin-top: ${(props) => props.scrollBarOffset || '35px'}; + margin-top: ${(props) => props.scrollBarOffset || '65px'}; } `; @@ -78,7 +78,7 @@ export const NativeScrollArea = forwardRef( const [hideHeader, setHideHeader] = useState(true); const { start, clear } = useTimeout( () => setHideScrollbar(true), - scrollHideDelay !== undefined ? scrollHideDelay * 1000 : 1000, + scrollHideDelay !== undefined ? scrollHideDelay * 1000 : 0, ); const containerRef = useRef(null); diff --git a/src/renderer/components/search-input/index.tsx b/src/renderer/components/search-input/index.tsx index 2154abc1..9e59ee63 100644 --- a/src/renderer/components/search-input/index.tsx +++ b/src/renderer/components/search-input/index.tsx @@ -43,10 +43,10 @@ export const SearchInput = ({ } + icon={showIcon && } size="md" styles={{ - icon: { svg: { fill: 'var(--btn-default-fg)' } }, + icon: { svg: { fill: 'var(--titlebar-fg)' } }, input: { backgroundColor: isOpened ? 'inherit' : 'transparent !important', border: 'none !important', diff --git a/src/renderer/components/toast/index.tsx b/src/renderer/components/toast/index.tsx index 03daa037..b71b4988 100644 --- a/src/renderer/components/toast/index.tsx +++ b/src/renderer/components/toast/index.tsx @@ -30,15 +30,19 @@ const showToast = ({ type, ...props }: NotificationProps) => { ? 'Error' : 'Info'; - const defaultDuration = type === 'error' ? 3500 : 2000; + const defaultDuration = type === 'error' ? 4000 : 2000; return showNotification({ autoClose: defaultDuration, styles: () => ({ - closeButton: {}, + closeButton: { + '&:hover': { + background: 'transparent', + }, + }, description: { color: 'var(--toast-description-fg)', - fontSize: '.9em', + fontSize: '1rem', }, loader: { margin: '1rem', @@ -46,10 +50,12 @@ const showToast = ({ type, ...props }: NotificationProps) => { root: { '&::before': { backgroundColor: color }, background: 'var(--toast-bg)', + border: '2px solid var(--generic-border-color)', + bottom: '90px', }, title: { color: 'var(--toast-title-fg)', - fontSize: '1em', + fontSize: '1.3rem', }, }), title: defaultTitle, diff --git a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx index f20322c2..6402dc95 100644 --- a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx +++ b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx @@ -29,13 +29,13 @@ interface VirtualGridProps extends Omit void; } -const constrainWidth = (width: number) => { - if (width < 1920) { - return width; - } +// const constrainWidth = (width: number) => { +// if (width < 1920) { +// return width; +// } - return 1920; -}; +// return 1920; +// }; export const VirtualInfiniteGrid = forwardRef( ( @@ -65,9 +65,7 @@ export const VirtualInfiniteGrid = forwardRef( const loader = useRef(null); const { itemHeight, rowCount, columnCount } = useMemo(() => { - const itemsPerRow = Math.floor( - (constrainWidth(Number(width)) - itemGap + 3) / (itemSize! + itemGap + 2), - ); + const itemsPerRow = Math.floor((Number(width) - itemGap + 3) / (itemSize! + itemGap + 2)); return { columnCount: itemsPerRow, diff --git a/src/renderer/components/virtual-table/cells/favorite-cell.tsx b/src/renderer/components/virtual-table/cells/favorite-cell.tsx index 403c5348..5bbacada 100644 --- a/src/renderer/components/virtual-table/cells/favorite-cell.tsx +++ b/src/renderer/components/virtual-table/cells/favorite-cell.tsx @@ -6,11 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { HTTPError } from 'ky'; import { api } from '/@/renderer/api'; import { RawFavoriteResponse, FavoriteArgs, LibraryItem } from '/@/renderer/api/types'; -import { - useCurrentServer, - useSetAlbumListItemDataById, - useSetQueueFavorite, -} from '/@/renderer/store'; +import { useCurrentServer, useSetAlbumListItemDataById } from '/@/renderer/store'; const useCreateFavorite = () => { const server = useCurrentServer(); @@ -50,9 +46,6 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => { const createMutation = useCreateFavorite(); const deleteMutation = useDeleteFavorite(); - // Since the queue is using client-side state, we need to update it manually - const setFavorite = useSetQueueFavorite(); - const handleToggleFavorite = () => { const newFavoriteValue = !value; @@ -66,10 +59,6 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => { }, { onSuccess: () => { - if (data.itemType === LibraryItem.SONG) { - setFavorite([data.id], newFavoriteValue); - } - node.setData({ ...data, userFavorite: newFavoriteValue }); }, }, @@ -84,10 +73,6 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => { }, { onSuccess: () => { - if (data.itemType === LibraryItem.SONG) { - setFavorite([data.id], newFavoriteValue); - } - node.setData({ ...data, userFavorite: newFavoriteValue }); }, }, diff --git a/src/renderer/components/virtual-table/cells/rating-cell.tsx b/src/renderer/components/virtual-table/cells/rating-cell.tsx index 011dc320..74f68a44 100644 --- a/src/renderer/components/virtual-table/cells/rating-cell.tsx +++ b/src/renderer/components/virtual-table/cells/rating-cell.tsx @@ -1,47 +1,55 @@ -import { MouseEvent, useState } from 'react'; +import { MouseEvent } from 'react'; import type { ICellRendererParams } from '@ag-grid-community/core'; import { Rating } from '/@/renderer/components/rating'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; import { useUpdateRating } from '/@/renderer/components/virtual-table/hooks/use-rating'; -export const RatingCell = ({ value }: ICellRendererParams) => { +export const RatingCell = ({ value, node }: ICellRendererParams) => { const updateRatingMutation = useUpdateRating(); - const [ratingValue, setRatingValue] = useState(value?.userRating); const handleUpdateRating = (rating: number) => { if (!value) return; - updateRatingMutation.mutate({ - _serverId: value?.serverId, - query: { - item: [value], - rating, + updateRatingMutation.mutate( + { + _serverId: value?.serverId, + query: { + item: [value], + rating, + }, }, - }); - - setRatingValue(rating); + { + onSuccess: () => { + node.setData({ ...node.data, userRating: rating }); + }, + }, + ); }; const handleClearRating = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - updateRatingMutation.mutate({ - _serverId: value?.serverId, - query: { - item: [value], - rating: 0, + updateRatingMutation.mutate( + { + _serverId: value?.serverId, + query: { + item: [value], + rating: 0, + }, }, - }); - - setRatingValue(0); + { + onSuccess: () => { + node.setData({ ...node.data, userRating: 0 }); + }, + }, + ); }; return ( diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 5ab03df7..dbae4205 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -254,6 +254,7 @@ const tableColumns: { [key: string]: ColDef } = { colId: TableColumn.TITLE_COMBINED, headerName: 'Title', initialWidth: 500, + minWidth: 150, valueGetter: (params: ValueGetterParams) => params.data ? { @@ -292,7 +293,7 @@ const tableColumns: { [key: string]: ColDef } = { width: 50, }, userRating: { - cellClass: (params) => (params.value ? 'visible ag-cell-rating' : 'ag-cell-rating'), + cellClass: (params) => (params.value.userRating ? 'visible ag-cell-rating' : 'ag-cell-rating'), cellRenderer: RatingCell, colId: TableColumn.USER_RATING, field: 'userRating', @@ -427,6 +428,7 @@ export const VirtualTable = forwardRef( ref={mergedRef} animateRows maintainColumnOrder + suppressAsyncEvents suppressContextMenu suppressCopyRowsToClipboard suppressMoveWhenRowDragging diff --git a/src/renderer/features/action-required/components/server-required.tsx b/src/renderer/features/action-required/components/server-required.tsx index 7e50379e..7a8673ab 100644 --- a/src/renderer/features/action-required/components/server-required.tsx +++ b/src/renderer/features/action-required/components/server-required.tsx @@ -1,10 +1,24 @@ -import { Text } from '/@/renderer/components'; +import { RiMenuFill } from 'react-icons/ri'; +import { Button, DropdownMenu, Text } from '/@/renderer/components'; +import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu'; export const ServerRequired = () => { return ( <> No server selected. - Add or select a server in the file menu. + + + + + + + + ); }; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index faaf55de..38217476 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -3,28 +3,28 @@ import { Button, getColumnDefs, GridCarousel, + Text, TextTitle, useFixedTableHeader, VirtualTable, } from '/@/renderer/components'; -import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; +import { ColDef, 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 { useSetState } from '@mantine/hooks'; -import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri'; +import { RiDiscFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri'; import { generatePath, useParams } from 'react-router'; import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query'; -import { useSongListStore } from '/@/renderer/store'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { AppRoute } from '/@/renderer/router/routes'; import { useContainerQuery } from '/@/renderer/hooks'; -import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; +import { PersistedTableColumn, usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { useHandleGeneralContextMenu, useHandleTableContextMenu, } from '/@/renderer/features/context-menu'; -import { Play } from '/@/renderer/types'; +import { Play, ServerType, TableColumn } from '/@/renderer/types'; import { ALBUM_CONTEXT_MENU_ITEMS, SONG_CONTEXT_MENU_ITEMS, @@ -34,11 +34,14 @@ import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-que import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types'; import { usePlayQueueAdd } from '/@/renderer/features/player'; +const isFullWidthRow = (node: RowNode) => { + return node.id?.includes('disc-'); +}; + const ContentContainer = styled.div` position: relative; display: flex; flex-direction: column; - max-width: 1920px; padding: 1rem 2rem 5rem; overflow: hidden; @@ -61,13 +64,82 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => { const cq = useContainerQuery(); const handlePlayQueueAdd = usePlayQueueAdd(); - const page = useSongListStore(); + // TODO: Make this customizable + const columnDefs: ColDef[] = useMemo(() => { + const userRatingColumn = + detailQuery?.data?.serverType !== ServerType.JELLYFIN + ? [ + { + column: TableColumn.USER_RATING, + width: 0, + }, + ] + : []; - const columnDefs: ColDef[] = useMemo( - () => - getColumnDefs(page.table.columns).filter((c) => c.colId !== 'album' && c.colId !== 'artist'), - [page.table.columns], - ); + const cols: PersistedTableColumn[] = [ + { + column: TableColumn.TRACK_NUMBER, + width: 0, + }, + { + column: TableColumn.TITLE_COMBINED, + width: 0, + }, + + { + column: TableColumn.DURATION, + width: 0, + }, + { + column: TableColumn.BIT_RATE, + width: 0, + }, + { + column: TableColumn.PLAY_COUNT, + width: 0, + }, + { + column: TableColumn.LAST_PLAYED, + width: 0, + }, + ...userRatingColumn, + { + column: TableColumn.USER_FAVORITE, + width: 0, + }, + ]; + return getColumnDefs(cols).filter((c) => c.colId !== 'album' && c.colId !== 'artist'); + }, [detailQuery?.data?.serverType]); + + const getRowHeight = useCallback((params: RowHeightParams) => { + if (isFullWidthRow(params.node)) { + return 45; + } + + return 60; + }, []); + + const songsRowData = useMemo(() => { + if (!detailQuery.data?.songs) { + return []; + } + + const uniqueDiscNumbers = new Set(detailQuery.data?.songs.map((s) => s.discNumber)); + + if (uniqueDiscNumbers.size === 1) { + return detailQuery.data?.songs; + } + + const rowData: (QueueSong | { id: string; name: string })[] = []; + + for (const discNumber of uniqueDiscNumbers.values()) { + const songsByDiscNumber = detailQuery.data?.songs.filter((s) => s.discNumber === discNumber); + rowData.push({ id: `disc-${discNumber}`, name: `DISC ${discNumber}` }); + rowData.push(...songsByDiscNumber); + } + + return rowData; + }, [detailQuery.data?.songs]); const [pagination, setPagination] = useSetState({ artist: 0, @@ -261,9 +333,29 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => { suppressRowDrag columnDefs={columnDefs} enableCellChangeFlash={false} + fullWidthCellRenderer={(data: any) => { + if (!data.data) return null; + return ( + + + {data.data.name} + + ); + }} + getRowHeight={getRowHeight} getRowId={(data) => data.data.id} - rowData={detailQuery.data?.songs} - rowHeight={60} + isFullWidthRow={(data) => { + return isFullWidthRow(data.rowNode) || false; + }} + isRowSelectable={(data) => { + if (isFullWidthRow(data.data)) return false; + return true; + }} + rowData={songsRowData} rowSelection="multiple" onCellContextMenu={handleContextMenu} onRowDoubleClicked={handleRowDoubleClick} diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index 488c1907..c0a74362 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -114,7 +114,7 @@ export const AlbumListContent = ({ ); const albums = api.normalize.albumList(albumsRes, server); - params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined); + params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0); }, rowCount: undefined, }; @@ -341,7 +341,7 @@ export const AlbumListContent = ({ itemGap={20} itemSize={150 + page.grid?.size} itemType={LibraryItem.ALBUM} - loading={!itemCount} + loading={itemCount === undefined || itemCount === null} minimumBatchSize={40} route={{ route: AppRoute.LIBRARY_ALBUMS_DETAIL, @@ -366,7 +366,7 @@ export const AlbumListContent = ({ blockLoadDebounceMillis={200} columnDefs={columnDefs} getRowId={(data) => data.data.id} - infiniteInitialRowCount={itemCount || 1} + infiniteInitialRowCount={itemCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} paginationPageSize={page.table.pagination.itemsPerPage || 100} diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx index 8bd85e67..0b44f0a0 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -1,4 +1,4 @@ -import { MutableRefObject, useCallback, MouseEvent, ChangeEvent } from 'react'; +import { MutableRefObject, useCallback, MouseEvent, ChangeEvent, useMemo } from 'react'; import { IDatasource } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Flex, Group, Stack } from '@mantine/core'; @@ -8,13 +8,13 @@ import { RiSortAsc, RiSortDesc, RiFolder2Line, - RiFilter3Line, RiMoreFill, RiAddBoxFill, RiPlayFill, RiAddCircleFill, RiRefreshLine, RiSettings3Fill, + RiFilterFill, } from 'react-icons/ri'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; @@ -189,7 +189,7 @@ export const AlbumListHeaderFilters = ({ ); const albums = api.normalize.albumList(albumsRes, server); - params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined); + params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0); }, rowCount: undefined, }; @@ -371,6 +371,20 @@ export const AlbumListHeaderFilters = ({ } }; + const isFilterApplied = useMemo(() => { + const isNavidromeFilterApplied = + server?.type === ServerType.NAVIDROME && + page.filter.ndParams && + Object.values(page.filter.ndParams).some((value) => value !== undefined); + + const isJellyfinFilterApplied = + server?.type === ServerType.JELLYFIN && + page.filter.jfParams && + Object.values(page.filter.jfParams).some((value) => value !== undefined); + + return isNavidromeFilterApplied || isJellyfinFilterApplied; + }, [page.filter.jfParams, page.filter.ndParams, server?.type]); + return ( )} - + + - - - Select a server - {serverList.map((s) => { - const isNavidromeExpired = s.type === ServerType.NAVIDROME && !s.ndCredential; - const isJellyfinExpired = false; - const isSessionExpired = isNavidromeExpired || isJellyfinExpired; + <> + Select a server + {serverList.map((s) => { + const isNavidromeExpired = s.type === ServerType.NAVIDROME && !s.ndCredential; + const isJellyfinExpired = false; + const isSessionExpired = isNavidromeExpired || isJellyfinExpired; - return ( - - ) - } - onClick={() => { - if (!isSessionExpired) return handleSetCurrentServer(s); - return handleCredentialsModal(s); - }} - > - {s.name} - - ); - })} - - } - > - Search - - } - onClick={handleSettingsModal} - > - Settings - - - } - onClick={handleManageServersModal} - > - Manage servers - - - + return ( + : } + onClick={() => { + if (!isSessionExpired) return handleSetCurrentServer(s); + return handleCredentialsModal(s); + }} + > + {s.name} + + ); + })} + + } + onClick={handleManageServersModal} + > + Manage servers + + } + onClick={handleSettingsModal} + > + Settings + + ); }; diff --git a/src/renderer/features/titlebar/components/titlebar.tsx b/src/renderer/features/titlebar/components/titlebar.tsx index 37605b0d..72b89333 100644 --- a/src/renderer/features/titlebar/components/titlebar.tsx +++ b/src/renderer/features/titlebar/components/titlebar.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react'; import { Group } from '@mantine/core'; import styled from 'styled-components'; import { WindowControls } from '../../window-controls'; -import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu'; interface TitlebarProps { children?: ReactNode; @@ -51,10 +50,6 @@ export const Titlebar = ({ children }: TitlebarProps) => { {children} - <> - {/* */} - - diff --git a/src/renderer/features/window-controls/components/window-controls.tsx b/src/renderer/features/window-controls/components/window-controls.tsx index e200ca03..50ad2857 100644 --- a/src/renderer/features/window-controls/components/window-controls.tsx +++ b/src/renderer/features/window-controls/components/window-controls.tsx @@ -23,7 +23,7 @@ export const WindowsButton = styled.div<{ $exit?: boolean }>` justify-content: center; -webkit-app-region: no-drag; width: 50px; - height: 30px; + height: 65px; img { width: 35%; diff --git a/src/renderer/hooks/use-should-pad-titlebar.tsx b/src/renderer/hooks/use-should-pad-titlebar.tsx index e9b3e505..190b4d2a 100644 --- a/src/renderer/hooks/use-should-pad-titlebar.tsx +++ b/src/renderer/hooks/use-should-pad-titlebar.tsx @@ -1,9 +1,15 @@ +import { useLocation } from 'react-router'; +import { AppRoute } from '/@/renderer/router/routes'; import { useSidebarRightExpanded } from '/@/renderer/store'; import { useGeneralSettings } from '/@/renderer/store/settings.store'; export const useShouldPadTitlebar = () => { + const location = useLocation(); const isSidebarExpanded = useSidebarRightExpanded(); + const isQueuePage = location.pathname === AppRoute.NOW_PLAYING; const { sideQueueType } = useGeneralSettings(); - return !(isSidebarExpanded && sideQueueType === 'sideQueue'); + // If the sidebar is expanded, the sidebar queue is enabled, and the user is not on the queue page + + return !(isSidebarExpanded && sideQueueType === 'sideQueue' && !isQueuePage); }; diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 9ec9d7ea..06c09124 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -1,3 +1,4 @@ +import { Notifications } from '@mantine/notifications'; import { QueryClientProvider } from '@tanstack/react-query'; import { createRoot } from 'react-dom/client'; import { App } from './app'; @@ -8,6 +9,10 @@ const root = createRoot(container); root.render( + , ); diff --git a/src/renderer/layouts/default-layout.tsx b/src/renderer/layouts/default-layout.tsx index 65a0379d..46823a34 100644 --- a/src/renderer/layouts/default-layout.tsx +++ b/src/renderer/layouts/default-layout.tsx @@ -30,7 +30,7 @@ const Layout = styled.div` grid-template-areas: 'main-content' 'player'; - grid-template-rows: calc(100vh - 85px) 85px; + grid-template-rows: calc(100vh - 90px) 90px; grid-template-columns: 1fr; gap: 0; height: 100%; @@ -97,8 +97,9 @@ const ResizeHandle = styled.div<{ `; const QueueDrawer = styled(motion.div)` - background: var(--sidebar-bg); - border-left: var(--sidebar-border); + background: transparent; + border-top-left-radius: 5px; + border-top-right-radius: 5px; `; const QueueDrawerArea = styled(motion.div)` @@ -162,7 +163,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => { const queueDrawerVariants: Variants = { closed: { - height: 'calc(100vh - 170px)', + height: 'calc(100vh - 190px)', position: 'absolute', right: 0, top: '75px', @@ -174,12 +175,11 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => { x: '50vw', }, open: { - boxShadow: '1px 1px 10px 5px rgba(0, 0, 0, 0.3)', - height: 'calc(100vh - 170px)', + boxShadow: '0px 0px 10px 0px rgba(0, 0, 0, 0.8)', + height: 'calc(100vh - 190px)', position: 'absolute', right: '20px', top: '75px', - transition: { damping: 10, delay: 0, diff --git a/src/renderer/router/titlebar-outlet.tsx b/src/renderer/router/titlebar-outlet.tsx index e73e4e72..54377ad7 100644 --- a/src/renderer/router/titlebar-outlet.tsx +++ b/src/renderer/router/titlebar-outlet.tsx @@ -7,7 +7,7 @@ const TitlebarContainer = styled.header` top: 0; right: 0; z-index: 5000; - height: 30px; + height: 65px; background: var(--titlebar-controls-bg); -webkit-app-region: drag; `; diff --git a/src/renderer/styles/global.scss b/src/renderer/styles/global.scss index c29de7e4..918d9562 100644 --- a/src/renderer/styles/global.scss +++ b/src/renderer/styles/global.scss @@ -64,7 +64,7 @@ html { } ::-webkit-scrollbar-thumb:hover { - background: rgb(136, 136, 136); + background: var(--scrollbar-thumb-bg-hover); } a { diff --git a/src/renderer/themes/default.scss b/src/renderer/themes/default.scss index 3379e74c..dd939bc9 100644 --- a/src/renderer/themes/default.scss +++ b/src/renderer/themes/default.scss @@ -2,7 +2,7 @@ --root-font-size: 13px; --icon-color: rgb(255, 255, 255); - --primary-color: rgb(34, 104, 255); + --primary-color: rgb(53, 116, 252); --secondary-color: rgb(255, 120, 120); --success-color: green; --warning-color: orange; @@ -14,7 +14,7 @@ --main-fg-secondary: rgb(150, 150, 150); --titlebar-fg: rgb(255, 255, 255); - --titlebar-bg: rgb(7, 7, 7); + --titlebar-bg: rgb(24, 24, 24); --titlebar-controls-bg: rgba(0, 0, 0, 0); --sidebar-bg: rgb(0, 0, 0); @@ -40,10 +40,11 @@ --tooltip-fg: #000000; --scrollbar-track-bg: transparent; - --scrollbar-thumb-bg: rgba(90, 90, 90, 0.5); + --scrollbar-thumb-bg: rgba(160, 160, 160, 0.3); + --scrollbar-thumb-bg-hover: rgba(160, 160, 160, 0.6); --btn-primary-bg: var(--primary-color); - --btn-primary-bg-hover: rgb(0, 71, 252); + --btn-primary-bg-hover: rgb(34, 96, 255); --btn-primary-fg: #ffffff; --btn-primary-fg-hover: #ffffff; @@ -59,7 +60,7 @@ --input-bg: rgb(35, 35, 35); --input-fg: rgb(193, 193, 193); - --input-placeholder-fg: rgb(119, 126, 139); + --input-placeholder-fg: rgb(107, 108, 109); --input-active-fg: rgb(193, 193, 193); --input-active-bg: rgba(255, 255, 255, 0.1); @@ -84,12 +85,12 @@ --toast-description-fg: rgb(193, 194, 197); --toast-bg: rgb(16, 16, 16); - --modal-bg: rgb(26, 26, 26); + --modal-bg: var(--main-bg); --badge-bg: rgb(0, 0, 0); --badge-fg: rgb(255, 255, 255); - --paper-bg: rgb(30, 30, 30); + --paper-bg: rgb(20, 20, 20); --placeholder-bg: rgba(53, 53, 53, 0.5); --placeholder-fg: rgba(126, 126, 126); @@ -180,25 +181,4 @@ .current-song { background: rgba(96, 144, 240, 0.3) !important; } - - .mantine-Modal-modal { - background: var(--modal-bg); - border-radius: 10px; - backdrop-filter: blur(8px); - } - - .mantine-Modal-header { - padding-bottom: 1rem; - margin-right: 0.5rem; - border-bottom: 1px solid var(--generic-border-color); - } - - .mantine-Modal-title { - font-weight: bold; - font-size: medium; - } - - .mantine-Modal-body { - padding: 1rem; - } } diff --git a/src/renderer/themes/light.scss b/src/renderer/themes/light.scss index b96f2914..1890b2e9 100644 --- a/src/renderer/themes/light.scss +++ b/src/renderer/themes/light.scss @@ -7,7 +7,7 @@ body[data-theme='defaultLight'] { --titlebar-fg: rgb(25, 25, 25); --titlebar-bg: rgb(240, 241, 242); - --titlebar-controls-bg: rgba(240, 240, 240, 0.2); + --titlebar-controls-bg: rgba(0, 0, 0, 0); --sidebar-bg: rgb(240, 241, 242); --sidebar-fg: rgb(0, 0, 0); @@ -35,7 +35,8 @@ body[data-theme='defaultLight'] { --tooltip-fg: rgb(0, 0, 0); --scrollbar-track-bg: transparent; - --scrollbar-thumb-bg: rgb(140, 140, 140); + --scrollbar-thumb-bg: rgba(140, 140, 140, 0.3); + --scrollbar-thumb-bg: rgba(140, 140, 140, 0.6); --btn-primary-bg: var(--primary-color); --btn-primary-fg: #ffffff; @@ -78,7 +79,7 @@ body[data-theme='defaultLight'] { --modal-bg: rgb(255, 255, 255); - --paper-bg: rgb(240, 240, 240); + --paper-bg: rgb(235, 235, 235); --placeholder-bg: rgba(204, 204, 204, 0.5); --placeholder-fg: rgb(126, 126, 126); diff --git a/src/renderer/utils/constrain-sidebar-width.ts b/src/renderer/utils/constrain-sidebar-width.ts index 1464ca18..92a5602c 100644 --- a/src/renderer/utils/constrain-sidebar-width.ts +++ b/src/renderer/utils/constrain-sidebar-width.ts @@ -1,6 +1,6 @@ export const constrainSidebarWidth = (num: number) => { - if (num < 225) { - return 225; + if (num < 260) { + return 260; } if (num > 400) {