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 '';
}