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:
@@ -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,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user