diff --git a/src/apps/dashboard/components/widgets/ItemCountsWidget.tsx b/src/apps/dashboard/components/widgets/ItemCountsWidget.tsx new file mode 100644 index 0000000000..12a58a60ef --- /dev/null +++ b/src/apps/dashboard/components/widgets/ItemCountsWidget.tsx @@ -0,0 +1,95 @@ +import Book from '@mui/icons-material/Book'; +import Movie from '@mui/icons-material/Movie'; +import MusicNote from '@mui/icons-material/MusicNote'; +import MusicVideo from '@mui/icons-material/MusicVideo'; +import Tv from '@mui/icons-material/Tv'; +import VideoLibrary from '@mui/icons-material/VideoLibrary'; +import Grid from '@mui/material/Grid2'; +import React from 'react'; + +import { useItemCounts } from 'apps/dashboard/features/metrics/api/useItemCounts'; +import MetricCard from 'apps/dashboard/features/metrics/components/MetricCard'; +import globalize from 'lib/globalize'; + +const ItemCountsWidget = () => { + const { data: counts } = useItemCounts(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ItemCountsWidget; diff --git a/src/apps/dashboard/controllers/dashboard.html b/src/apps/dashboard/controllers/dashboard.html index 55906b6882..e0656d1cc8 100644 --- a/src/apps/dashboard/controllers/dashboard.html +++ b/src/apps/dashboard/controllers/dashboard.html @@ -20,13 +20,13 @@
- - -
@@ -36,6 +36,8 @@
+ +
diff --git a/src/apps/dashboard/controllers/dashboard.js b/src/apps/dashboard/controllers/dashboard.js index 8e91a8f33a..c9dd75b97d 100644 --- a/src/apps/dashboard/controllers/dashboard.js +++ b/src/apps/dashboard/controllers/dashboard.js @@ -33,6 +33,7 @@ import 'elements/emby-itemscontainer/emby-itemscontainer'; import 'components/listview/listview.scss'; import 'styles/flexstyles.scss'; import './dashboard.scss'; +import ItemCountsWidget from '../components/widgets/ItemCountsWidget'; function showPlaybackInfo(btn, session) { let title; @@ -748,7 +749,7 @@ const DashboardPage = { export default function (view) { const serverId = ApiClient.serverId(); - let unmountPathsWidget; + let unmountWidgetFns = []; function onRestartRequired(evt, apiClient) { console.debug('onRestartRequired not implemented', evt, apiClient); @@ -824,7 +825,8 @@ export default function (view) { button: page.querySelector('.btnRefresh') }); - unmountPathsWidget = renderComponent(ServerPathWidget, {}, page.querySelector('#serverPaths')); + unmountWidgetFns.push(renderComponent(ItemCountsWidget, {}, page.querySelector('#itemCounts'))); + unmountWidgetFns.push(renderComponent(ServerPathWidget, {}, page.querySelector('#serverPaths'))); page.querySelector('#btnRestartServer').addEventListener('click', DashboardPage.restart); page.querySelector('#btnShutdown').addEventListener('click', DashboardPage.shutdown); @@ -852,7 +854,10 @@ export default function (view) { button: page.querySelector('.btnRefresh') }); - if (unmountPathsWidget) unmountPathsWidget(); + unmountWidgetFns.forEach(unmount => { + unmount(); + }); + unmountWidgetFns = []; page.querySelector('#btnRestartServer').removeEventListener('click', DashboardPage.restart); page.querySelector('#btnShutdown').removeEventListener('click', DashboardPage.shutdown); diff --git a/src/apps/dashboard/features/metrics/api/useItemCounts.ts b/src/apps/dashboard/features/metrics/api/useItemCounts.ts new file mode 100644 index 0000000000..ee2ef772db --- /dev/null +++ b/src/apps/dashboard/features/metrics/api/useItemCounts.ts @@ -0,0 +1,33 @@ +import type { Api } from '@jellyfin/sdk'; +import type { LibraryApiGetItemCountsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api'; +import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'; +import { queryOptions, useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { useApi } from 'hooks/useApi'; + +const fetchItemCounts = async ( + api: Api, + params?: LibraryApiGetItemCountsRequest, + options?: AxiosRequestConfig +) => { + const response = await getLibraryApi(api) + .getItemCounts(params, options); + return response.data; +}; + +const getItemCountsQuery = ( + api?: Api, + params?: LibraryApiGetItemCountsRequest +) => queryOptions({ + queryKey: [ 'ItemCounts', params ], + queryFn: ({ signal }) => fetchItemCounts(api!, params, { signal }), + enabled: !!api +}); + +export const useItemCounts = ( + params?: LibraryApiGetItemCountsRequest +) => { + const { api } = useApi(); + return useQuery(getItemCountsQuery(api, params)); +}; diff --git a/src/apps/dashboard/features/metrics/components/MetricCard.tsx b/src/apps/dashboard/features/metrics/components/MetricCard.tsx new file mode 100644 index 0000000000..5ab87bc167 --- /dev/null +++ b/src/apps/dashboard/features/metrics/components/MetricCard.tsx @@ -0,0 +1,71 @@ +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Typography from '@mui/material/Typography'; +import React, { type FC } from 'react'; + +import { useLocale } from 'hooks/useLocale'; +import { toDecimalString } from 'utils/number'; + +interface Metric { + label: string + value?: number +} + +interface MetricCardProps { + metrics: Metric[] + Icon: typeof SvgIcon +} + +const MetricCard: FC = ({ + metrics, + Icon +}) => { + const { dateTimeLocale } = useLocale(); + + return ( + + + {metrics.map(({ label, value }) => ( + + + {label} + + + {typeof value !== 'undefined' ? ( + toDecimalString(value, dateTimeLocale) + ) : ( + + )} + + + ))} + + + + ); +}; + +export default MetricCard; diff --git a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx index 79f6a8cf35..edd834a9e0 100644 --- a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx +++ b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx @@ -9,7 +9,7 @@ import CircularProgress, { } from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; -import { toPercent } from 'utils/number'; +import { toPercentString } from 'utils/number'; import { getCurrentDateTimeLocale } from 'lib/globalize'; import type { ItemDto } from 'types/base/models/item-dto'; @@ -36,7 +36,7 @@ function CircularProgressWithLabel( component='div' color='text.secondary' > - {toPercent(props.value / 100, getCurrentDateTimeLocale())} + {toPercentString(props.value / 100, getCurrentDateTimeLocale())} diff --git a/src/elements/emby-progressring/emby-progressring.js b/src/elements/emby-progressring/emby-progressring.js index fee77e5ec7..ce4b061bd9 100644 --- a/src/elements/emby-progressring/emby-progressring.js +++ b/src/elements/emby-progressring/emby-progressring.js @@ -2,7 +2,7 @@ import './emby-progressring.scss'; import 'webcomponents.js/webcomponents-lite'; import template from './emby-progressring.template.html'; import { getCurrentDateTimeLocale } from '../../lib/globalize'; -import { toPercent } from '../../utils/number.ts'; +import { toPercentString } from '../../utils/number.ts'; const EmbyProgressRing = Object.create(HTMLDivElement.prototype); @@ -71,7 +71,7 @@ EmbyProgressRing.setProgress = function (progress) { this.querySelector('.animate-75-100-b').style.transform = 'rotate(' + angle + 'deg)'; } - this.querySelector('.progressring-text').innerHTML = toPercent(progress / 100, getCurrentDateTimeLocale()); + this.querySelector('.progressring-text').innerHTML = toPercentString(progress / 100, getCurrentDateTimeLocale()); }; EmbyProgressRing.attachedCallback = function () { diff --git a/src/hooks/useLocale.tsx b/src/hooks/useLocale.tsx index a7b8bc625c..63bbb5818f 100644 --- a/src/hooks/useLocale.tsx +++ b/src/hooks/useLocale.tsx @@ -11,11 +11,11 @@ export function useLocale() { const { dateTimeLocale: dateTimeSetting, language } = useUserSettings(); const [ dateFnsLocale, setDateFnsLocale ] = useState(enUS); - const locale = useMemo(() => ( + const locale: string = useMemo(() => ( normalizeLocaleName(language || getDefaultLanguage()) ), [ language ]); - const dateTimeLocale = useMemo(() => ( + const dateTimeLocale: string = useMemo(() => ( dateTimeSetting ? normalizeLocaleName(dateTimeSetting) : locale ), [ dateTimeSetting, locale ]); diff --git a/src/themes/dark/theme.scss b/src/themes/dark/theme.scss index f055a4cfc4..b646875929 100644 --- a/src/themes/dark/theme.scss +++ b/src/themes/dark/theme.scss @@ -91,8 +91,8 @@ html { } .button-delete { - background: rgb(247, 0, 0); - color: rgba(255, 255, 255, 0.87); + background: #cb272a; + color: #fff; } .checkboxLabel { diff --git a/src/utils/number.ts b/src/utils/number.ts index af7e5f7307..b68879523c 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -13,13 +13,27 @@ export function randomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } +/** + * Gets the value of a number formatted as a string. + * @param {number} value The value as a number. + * @param {string} locale The locale to use for formatting (i.e. en-us). + * @returns {string} The value formatted as a string. + */ +export function toDecimalString(value: number, locale: string): string { + if (toLocaleStringSupportsOptions()) { + return value.toLocaleString(locale); + } + + return value.toString(); +} + /** * Gets the value of a number formatted as a perentage. * @param {number} value The value as a number. * @param {string} locale The locale to use for formatting (i.e. en-us). * @returns {string} The value formatted as a percentage. */ -export function toPercent(value: number | null | undefined, locale: string): string { +export function toPercentString(value: number | null | undefined, locale: string): string { if (value == null) { return ''; }