fix(media-server): redesign widget (#1775)

* fix: redisign media server widget

* fix: reviewed changes

* fix: reviewed chenges

* fix: add icon title

* fix: text resize
This commit is contained in:
Yossi Hillali
2024-12-29 19:24:38 +02:00
committed by GitHub
parent 75aefcb0d5
commit 23c7d0bbf0
3 changed files with 166 additions and 23 deletions

View File

@@ -21,6 +21,7 @@ export const mediaServerRouter = createTRPCRouter({
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return { return {
integrationId: integration.id, integrationId: integration.id,
integrationKind: integration.kind,
sessions: data, sessions: data,
}; };
}), }),

View File

@@ -1457,7 +1457,12 @@
"mediaServer": { "mediaServer": {
"name": "Current media server streams", "name": "Current media server streams",
"description": "Show the current streams on your media servers", "description": "Show the current streams on your media servers",
"option": {} "option": {},
"items": {
"user": "User",
"name": "Name",
"id": "Id"
}
}, },
"downloads": { "downloads": {
"name": "Download Client", "name": "Download Client",

View File

@@ -1,12 +1,17 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { Avatar, Box, Group, Text } from "@mantine/core"; import type { MantineStyleProp } from "@mantine/core";
import { Avatar, Box, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { IconDeviceAudioTape, IconDeviceTv, 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";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
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 { useScopedI18n } from "@homarr/translation/client";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks"; import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
@@ -29,26 +34,54 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
{ {
accessorKey: "sessionName", accessorKey: "sessionName",
header: "Name", header: "Name",
mantineTableHeadCellProps: {
style: {
fontSize: "7cqmin",
padding: "2cqmin",
width: "30%",
},
},
Cell: ({ row }) => (
<Text size="7cqmin" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{row.original.sessionName}
</Text>
),
}, },
{ {
accessorKey: "user.username", accessorKey: "user.username",
header: "User", header: "User",
mantineTableHeadCellProps: {
style: {
fontSize: "7cqmin",
padding: "2cqmin",
width: "25%",
},
},
Cell: ({ row }) => ( Cell: ({ row }) => (
<Group gap={"xs"}> <Group gap={"2cqmin"}>
<Avatar src={row.original.user.profilePictureUrl} size={"sm"} /> <Avatar src={row.original.user.profilePictureUrl} size={"10cqmin"} />
<Text>{row.original.user.username}</Text> <Text size="7cqmin">{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: "Currently playing",
mantineTableHeadCellProps: {
style: {
fontSize: "7cqmin",
padding: "2cqmin",
width: "45%",
},
},
Cell: ({ row }) => { Cell: ({ row }) => {
if (row.original.currentlyPlaying) { if (row.original.currentlyPlaying) {
return ( return (
<div> <Box>
<span>{row.original.currentlyPlaying.name}</span> <Text size="7cqmin" style={{ whiteSpace: "normal" }}>
</div> {row.original.currentlyPlaying.name}
</Text>
</Box>
); );
} }
@@ -83,49 +116,153 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
// Only render the flat list of sessions when the currentStreams change // Only render the flat list of sessions when the currentStreams change
// Otherwise it will always create a new array reference and cause the table to re-render // Otherwise it will always create a new array reference and cause the table to re-render
const flatSessions = useMemo(() => currentStreams.flatMap((pair) => pair.sessions), [currentStreams]); const flatSessions = useMemo(
() =>
currentStreams.flatMap((pair) =>
pair.sessions.map((session) => ({
...session,
integrationKind: pair.integrationKind,
integrationName: integrationDefs[pair.integrationKind].name,
integrationIcon: getIconUrl(pair.integrationKind),
})),
),
[currentStreams],
);
const baseStyle: MantineStyleProp = {
"--total-width": "calc(100cqw / var(--total-width))",
"--ratio-width": "calc(100cqw / var(--total-width))",
"--space-size": "calc(var(--ratio-width) * 0.1)", //Standard gap and spacing value
"--text-fz": "calc(var(--ratio-width) * 0.45)", //General Font Size
"--icon-size": "calc(var(--ratio-width) * 2 / 3)", //Normal icon size
"--mrt-base-background-color": "transparent",
};
const { openModal } = useModalAction(itemInfoModal);
const table = useTranslatedMantineReactTable({ const table = useTranslatedMantineReactTable({
columns, columns,
data: flatSessions, data: flatSessions,
enableRowSelection: false, enablePagination: false,
enableTopToolbar: false,
enableBottomToolbar: false,
enableSorting: false,
enableColumnActions: false,
enableStickyHeader: false,
enableColumnOrdering: false, enableColumnOrdering: false,
enableRowSelection: false,
enableFullScreenToggle: false, enableFullScreenToggle: false,
enableGlobalFilter: false, enableGlobalFilter: false,
enableDensityToggle: false, enableDensityToggle: false,
enableFilters: false, enableFilters: false,
enablePagination: true,
enableSorting: true,
enableHiding: false, enableHiding: false,
enableTopToolbar: false,
enableColumnActions: false,
enableStickyHeader: true,
initialState: { initialState: {
density: "xs", density: "xs",
}, },
mantinePaperProps: { mantinePaperProps: {
display: "flex", flex: 1,
h: "100%",
withBorder: false, withBorder: false,
style: { shadow: undefined,
flexDirection: "column",
},
}, },
mantineTableProps: { mantineTableProps: {
className: "media-server-widget-table",
style: { style: {
tableLayout: "fixed", tableLayout: "fixed",
}, },
}, },
mantineTableContainerProps: { mantineTableContainerProps: {
style: { style: {
flexGrow: 5, height: "100%",
}, },
}, },
mantineTableBodyCellProps: ({ row }) => ({
onClick: () => {
openModal({
item: row.original,
title:
row.original.currentlyPlaying?.type === "movie" ? (
<IconMovie size={36} />
) : row.original.currentlyPlaying?.type === "tv" ? (
<IconDeviceTv size={36} />
) : row.original.currentlyPlaying?.type === "video" ? (
<IconVideo size={36} />
) : (
<IconDeviceAudioTape size={36} />
),
});
},
}),
});
const uniqueIntegrations = Array.from(new Set(flatSessions.map((session) => session.integrationKind))).map((kind) => {
const session = flatSessions.find((session) => session.integrationKind === kind);
return {
integrationKind: kind,
integrationIcon: session?.integrationIcon,
integrationName: session?.integrationName,
};
}); });
return ( return (
<Box h="100%"> <Stack gap={0} h="100%" display="flex" style={baseStyle}>
<MantineReactTable table={table} /> <MantineReactTable table={table} />
</Box> <Group
gap="1cqmin"
h="var(--ratio-width)"
px="var(--space-size)"
pr="5cqmin"
justify="flex-end"
style={{
borderTop: "0.0625rem solid var(--border-color)",
}}
>
{uniqueIntegrations.map((integration) => (
<Group key={integration.integrationKind} gap="1cqmin" align="center">
<Avatar className="media-server-icon" src={integration.integrationIcon} size="xs" />
<Text className="media-server-name" size="sm">
{integration.integrationName}
</Text>
</Group>
))}
</Group>
</Stack>
); );
} }
const itemInfoModal = createModal<{ item: StreamSession; title: React.ReactNode }>(({ innerProps }) => {
const t = useScopedI18n("widget.mediaServer.items");
return (
<Stack align="center">
<Flex direction="column" gap="xs" align="center">
<Title>{innerProps.title}</Title>
<Title>{innerProps.item.currentlyPlaying?.name}</Title>
<Group display="flex">
<Title order={3}>{innerProps.item.currentlyPlaying?.episodeName}</Title>
{innerProps.item.currentlyPlaying?.seasonName && (
<>
{" - "}
<Title order={3}>{innerProps.item.currentlyPlaying.seasonName}</Title>
</>
)}
</Group>
</Flex>
<NormalizedLine itemKey={t("user")} value={innerProps.item.user.username} />
<NormalizedLine itemKey={t("name")} value={innerProps.item.sessionName} />
<NormalizedLine itemKey={t("id")} value={innerProps.item.sessionId} />
</Stack>
);
}).withOptions({
defaultTitle() {
return "";
},
size: "auto",
centered: true,
});
const NormalizedLine = ({ itemKey, value }: { itemKey: string; value: string }) => {
return (
<Group w="100%" align="top" justify="space-between">
<Text>{itemKey}:</Text>
<Text>{value}</Text>
</Group>
);
};