fix(media-server): improve responsive styles (#2548)

* fix(media-server): improve responsive styles

* fix(media-server): translate table headers
This commit is contained in:
Meier Lukas
2025-03-09 14:20:04 +01:00
committed by GitHub
parent d9720ac596
commit 82528dbd96
2 changed files with 88 additions and 65 deletions

View File

@@ -1665,6 +1665,7 @@
"description": "Show the current streams on your media servers", "description": "Show the current streams on your media servers",
"option": {}, "option": {},
"items": { "items": {
"currentlyPlaying": "Currently playing",
"user": "User", "user": "User",
"name": "Name", "name": "Name",
"id": "Id" "id": "Id"

View File

@@ -1,8 +1,9 @@
"use client"; "use client";
import type { ReactNode } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import { Avatar, Box, Flex, Group, Stack, Text, Title } from "@mantine/core"; import { Avatar, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { IconDeviceAudioTape, IconDeviceTv, IconMovie, IconVideo } from "@tabler/icons-react"; import { IconDeviceTv, IconHeadphones, IconMovie, IconVideo } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table"; import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table"; import { MantineReactTable } from "mantine-react-table";
@@ -11,6 +12,7 @@ import { getIconUrl, integrationDefs } from "@homarr/definitions";
import type { StreamSession } from "@homarr/integrations"; import type { StreamSession } from "@homarr/integrations";
import { createModal, useModalAction } from "@homarr/modals"; import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks"; import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
@@ -28,59 +30,51 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
); );
const utils = clientApi.useUtils(); const utils = clientApi.useUtils();
const t = useScopedI18n("widget.mediaServer");
const columns = useMemo<MRT_ColumnDef<StreamSession>[]>( const columns = useMemo<MRT_ColumnDef<StreamSession>[]>(
() => [ () => [
{ {
accessorKey: "sessionName", accessorKey: "sessionName",
header: "Name", header: t("items.name"),
mantineTableHeadCellProps: {
style: {
width: "30%",
},
},
Cell: ({ row }) => ( Cell: ({ row }) => (
<Text size="md" style={{ overflow: "hidden", textOverflow: "ellipsis" }}> <Text size="xs" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{row.original.sessionName} {row.original.sessionName}
</Text> </Text>
), ),
}, },
{ {
accessorKey: "user.username", accessorKey: "user.username",
header: "User", header: t("items.user"),
mantineTableHeadCellProps: {
style: {
width: "25%",
},
},
Cell: ({ row }) => ( Cell: ({ row }) => (
<Group gap={"sm"}> <Group gap="xs">
<Avatar src={row.original.user.profilePictureUrl} size={30} /> <Avatar size={20} src={row.original.user.profilePictureUrl} />
<Text size="md">{row.original.user.username}</Text> <Text size="xs">{row.original.user.username}</Text>
</Group> </Group>
), ),
}, },
{ {
accessorKey: "currentlyPlaying", // currentlyPlaying.name can be undefined which results in a warning. This is why we use currentlyPlaying instead of currentlyPlaying.name accessorKey: "currentlyPlaying", // currentlyPlaying.name can be undefined which results in a warning. This is why we use currentlyPlaying instead of currentlyPlaying.name
header: "Currently playing", header: t("items.currentlyPlaying"),
mantineTableHeadCellProps: {
style: {
width: "45%",
},
},
Cell: ({ row }) => {
if (row.original.currentlyPlaying) {
return (
<Box>
<Text lineClamp={1}>{row.original.currentlyPlaying.name}</Text>
</Box>
);
}
return null; Cell: ({ row }) => {
if (!row.original.currentlyPlaying) return null;
const Icon = mediaTypeIconMap[row.original.currentlyPlaying.type];
return (
<Group gap="xs" align="center">
<Icon size={16} />
<Text size="xs" lineClamp={1}>
{row.original.currentlyPlaying.name}
</Text>
</Group>
);
}, },
}, },
], ],
[], [t],
); );
clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription( clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription(
@@ -137,8 +131,18 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
enableDensityToggle: false, enableDensityToggle: false,
enableFilters: false, enableFilters: false,
enableHiding: false, enableHiding: false,
enableColumnPinning: true,
initialState: { initialState: {
density: "xs", density: "xs",
columnPinning: {
right: ["currentlyPlaying"],
},
},
mantineTableHeadProps: {
fz: "xs",
},
mantineTableHeadCellProps: {
py: 4,
}, },
mantinePaperProps: { mantinePaperProps: {
flex: 1, flex: 1,
@@ -158,20 +162,16 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
}, },
mantineTableBodyCellProps: ({ row }) => ({ mantineTableBodyCellProps: ({ row }) => ({
onClick: () => { onClick: () => {
openModal({ openModal(
item: row.original, {
title: item: row.original,
row.original.currentlyPlaying?.type === "movie" ? ( },
<IconMovie size={36} /> {
) : row.original.currentlyPlaying?.type === "tv" ? ( title: row.original.sessionName,
<IconDeviceTv size={36} /> },
) : row.original.currentlyPlaying?.type === "video" ? ( );
<IconVideo size={36} />
) : (
<IconDeviceAudioTape size={36} />
),
});
}, },
py: 4,
}), }),
}); });
@@ -210,42 +210,64 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
); );
} }
const itemInfoModal = createModal<{ item: StreamSession; title: React.ReactNode }>(({ innerProps }) => { const itemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
const t = useScopedI18n("widget.mediaServer.items"); const t = useScopedI18n("widget.mediaServer.items");
const Icon = innerProps.item.currentlyPlaying ? mediaTypeIconMap[innerProps.item.currentlyPlaying.type] : null;
return ( return (
<Stack align="center"> <Stack align="center">
<Flex direction="column" gap="xs" align="center"> <Flex direction="column" gap="xs" align="center">
<Title>{innerProps.title}</Title> {Icon && innerProps.item.currentlyPlaying !== null && (
<Title>{innerProps.item.currentlyPlaying?.name}</Title> <Group gap="sm" align="center">
<Group display="flex"> <Icon size={24} />
<Title order={3}>{innerProps.item.currentlyPlaying?.episodeName}</Title> <Title order={2}>{innerProps.item.currentlyPlaying.name}</Title>
{innerProps.item.currentlyPlaying?.seasonName && ( </Group>
<> )}
{" - "} {innerProps.item.currentlyPlaying?.episodeName && (
<Title order={3}>{innerProps.item.currentlyPlaying.seasonName}</Title> <Group>
</> <Title order={4}>{innerProps.item.currentlyPlaying.episodeName}</Title>
)} {innerProps.item.currentlyPlaying.seasonName && (
</Group> <>
{" - "}
<Title order={4}>{innerProps.item.currentlyPlaying.seasonName}</Title>
</>
)}
</Group>
)}
</Flex> </Flex>
<NormalizedLine itemKey={t("user")} value={innerProps.item.user.username} /> <NormalizedLine
<NormalizedLine itemKey={t("name")} value={innerProps.item.sessionName} /> itemKey={t("user")}
<NormalizedLine itemKey={t("id")} value={innerProps.item.sessionId} /> value={
<Group gap="sm" align="center">
<Avatar size="sm" src={innerProps.item.user.profilePictureUrl} />{" "}
<Text>{innerProps.item.user.username}</Text>
</Group>
}
/>
<NormalizedLine itemKey={t("name")} value={<Text>{innerProps.item.sessionName}</Text>} />
<NormalizedLine itemKey={t("id")} value={<Text>{innerProps.item.sessionId}</Text>} />
</Stack> </Stack>
); );
}).withOptions({ }).withOptions({
defaultTitle() { defaultTitle() {
return ""; return "";
}, },
size: "auto", size: "lg",
centered: true, centered: true,
}); });
const NormalizedLine = ({ itemKey, value }: { itemKey: string; value: string }) => { const NormalizedLine = ({ itemKey, value }: { itemKey: string; value: ReactNode }) => {
return ( return (
<Group w="100%" align="top" justify="space-between"> <Group w="100%" align="top" justify="space-between">
<Text>{itemKey}:</Text> <Text>{itemKey}:</Text>
<Text>{value}</Text> {value}
</Group> </Group>
); );
}; };
const mediaTypeIconMap = {
movie: IconMovie,
tv: IconDeviceTv,
video: IconVideo,
audio: IconHeadphones,
} satisfies Record<Exclude<StreamSession["currentlyPlaying"], null>["type"], TablerIcon>;