Merge pull request #6933 from thornbill/item-count-widget

Add item count widget to dashboard
This commit is contained in:
Bill Thornton
2025-06-10 16:37:21 -04:00
committed by GitHub
10 changed files with 235 additions and 15 deletions
@@ -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 (
<Grid
container
spacing={2}
sx={{
alignItems: 'stretch',
marginTop: 2
}}
>
<Grid size={{ xs: 12, sm: 6, lg: 4 }}>
<MetricCard
Icon={Movie}
metrics={[{
label: globalize.translate('Movies'),
value: counts?.MovieCount
}]}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 4 }}>
<MetricCard
Icon={Tv}
metrics={[{
label: globalize.translate('Series'),
value: counts?.SeriesCount
}, {
label: globalize.translate('Episodes'),
value: counts?.EpisodeCount
}]}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 4 }}>
<MetricCard
Icon={MusicNote}
metrics={[{
label: globalize.translate('Albums'),
value: counts?.AlbumCount
}, {
label: globalize.translate('Songs'),
value: counts?.SongCount
}]}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 4 }}>
<MetricCard
Icon={MusicVideo}
metrics={[{
label: globalize.translate('MusicVideos'),
value: counts?.MusicVideoCount
}]}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 4 }}>
<MetricCard
Icon={Book}
metrics={[{
label: globalize.translate('Books'),
value: counts?.BookCount
}]}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 4 }}>
<MetricCard
Icon={VideoLibrary}
metrics={[{
label: globalize.translate('Collections'),
value: counts?.BoxSetCount
}]}
/>
</Grid>
</Grid>
);
};
export default ItemCountsWidget;
@@ -20,13 +20,13 @@
</div>
<div class="dashboardActionsContainer">
<button is="emby-button" type="button" class="raised btnRefresh">
<button is="emby-button" type="button" class="raised btnRefresh button-submit">
<span>${ButtonScanAllLibraries}</span>
</button>
<button is="emby-button" type="button" id="btnRestartServer" class="raised">
<button is="emby-button" type="button" id="btnRestartServer" class="raised button-delete">
<span>${Restart}</span>
</button>
<button is="emby-button" type="button" id="btnShutdown" class="raised">
<button is="emby-button" type="button" id="btnShutdown" class="raised button-delete">
<span>${ButtonShutdown}</span>
</button>
</div>
@@ -36,6 +36,8 @@
<div id="divRunningTasks" class="paperList" style="padding: 1em;">
</div>
</div>
<div id="itemCounts"></div>
</div>
<div class="dashboardSection">
+8 -3
View File
@@ -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);
@@ -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));
};
@@ -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<MetricCardProps> = ({
metrics,
Icon
}) => {
const { dateTimeLocale } = useLocale();
return (
<Card
sx={{
display: 'flex',
alignItems: 'center',
height: '100%'
}}
>
<Stack
direction='row'
sx={{
width: '100%',
padding: 2,
justifyContent: 'space-between',
alignItems: 'center'
}}
>
{metrics.map(({ label, value }) => (
<Box key={label}>
<Typography
variant='body2'
color='text.secondary'
>
{label}
</Typography>
<Typography
variant='h5'
component='div'
>
{typeof value !== 'undefined' ? (
toDecimalString(value, dateTimeLocale)
) : (
<Skeleton />
)}
</Typography>
</Box>
))}
<Icon fontSize='large' />
</Stack>
</Card>
);
};
export default MetricCard;