149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
import { Badge, Box, Group, Paper, Progress, Stack, Text, Tooltip } from "@mantine/core";
|
|
import { IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react";
|
|
|
|
import { formatDuration } from "@homarr/common";
|
|
import type { TracearrStream } from "@homarr/integrations/types";
|
|
import { useI18n } from "@homarr/translation/client";
|
|
|
|
import type { SourcedTracearrItem } from "./source";
|
|
|
|
type SourcedTracearrStream = SourcedTracearrItem<TracearrStream>;
|
|
|
|
export function StreamsList({
|
|
streams,
|
|
width,
|
|
showSource,
|
|
}: {
|
|
streams: SourcedTracearrStream[];
|
|
width: number;
|
|
showSource: boolean;
|
|
}) {
|
|
const t = useI18n("widget.tracearr");
|
|
|
|
if (streams.length === 0) {
|
|
return (
|
|
<Text c="dimmed" ta="center" size="sm">
|
|
{t("streams.empty")}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack gap="xs">
|
|
{streams.map((stream) => (
|
|
<StreamCard key={stream.key} stream={stream} compact={width < 300} showSource={showSource} />
|
|
))}
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
function StreamCard({
|
|
stream,
|
|
compact,
|
|
showSource,
|
|
}: {
|
|
stream: SourcedTracearrStream;
|
|
compact: boolean;
|
|
showSource: boolean;
|
|
}) {
|
|
const t = useI18n("widget.tracearr");
|
|
const progressPercent =
|
|
stream.durationMs && stream.durationMs > 0 ? (stream.progressMs / stream.durationMs) * 100 : 0;
|
|
|
|
const mediaLabel =
|
|
stream.mediaType === "episode" && stream.showTitle
|
|
? `${stream.showTitle} - S${stream.seasonNumber ?? 0}E${stream.episodeNumber ?? 0} - ${stream.mediaTitle}`
|
|
: stream.mediaTitle;
|
|
|
|
const videoDecisionLabel =
|
|
stream.videoDecision === "directplay"
|
|
? t("streams.videoDecision.directPlay")
|
|
: stream.videoDecision === "transcode"
|
|
? t("streams.videoDecision.transcode")
|
|
: stream.videoDecision === "copy"
|
|
? t("streams.videoDecision.directStream")
|
|
: null;
|
|
|
|
return (
|
|
<Paper p="xs" radius="lg" style={{ position: "relative", overflow: "hidden" }}>
|
|
{stream.posterUrl && (
|
|
<Box
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
width: "50%",
|
|
backgroundImage: `url(${stream.posterUrl})`,
|
|
backgroundSize: "cover",
|
|
backgroundPosition: "center right",
|
|
opacity: 0.25,
|
|
maskImage: "linear-gradient(to left, black 20%, transparent 100%)",
|
|
WebkitMaskImage: "linear-gradient(to left, black 20%, transparent 100%)",
|
|
pointerEvents: "none",
|
|
}}
|
|
/>
|
|
)}
|
|
<Stack gap={4} style={{ position: "relative" }}>
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
|
|
{stream.state === "playing" ? (
|
|
<IconPlayerPlay size="var(--mantine-font-size-sm)" color="var(--mantine-color-green-6)" />
|
|
) : (
|
|
<IconPlayerPause size="var(--mantine-font-size-sm)" color="var(--mantine-color-yellow-6)" />
|
|
)}
|
|
<Text size="sm" fw={600} lineClamp={1}>
|
|
{stream.username}
|
|
</Text>
|
|
</Group>
|
|
<Group gap={4} wrap={compact ? "wrap" : "nowrap"}>
|
|
{stream.resolution && (
|
|
<Badge size="xs" variant="light">
|
|
{stream.resolution}
|
|
</Badge>
|
|
)}
|
|
{videoDecisionLabel && (
|
|
<Badge
|
|
size="xs"
|
|
variant="light"
|
|
color={
|
|
stream.videoDecision === "directplay"
|
|
? "green"
|
|
: stream.videoDecision === "transcode"
|
|
? "orange"
|
|
: "blue"
|
|
}
|
|
>
|
|
{videoDecisionLabel}
|
|
</Badge>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
|
|
<Tooltip label={mediaLabel}>
|
|
<Text size="xs" c="dimmed" lineClamp={1}>
|
|
{mediaLabel}
|
|
</Text>
|
|
</Tooltip>
|
|
|
|
{stream.durationMs && stream.durationMs > 0 && (
|
|
<Group gap="xs" align="center">
|
|
<Progress value={progressPercent} size="xs" style={{ flex: 1 }} radius="xl" />
|
|
<Text size="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
|
{formatDuration(stream.progressMs)} / {formatDuration(stream.durationMs)}
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
|
|
{(stream.device || showSource) && (
|
|
<Group gap="xs" wrap="nowrap">
|
|
<Text size="xs" c="dimmed" lineClamp={1}>
|
|
{[stream.player ?? stream.device, showSource ? stream.integrationName : null].filter(Boolean).join(" · ")}
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|