fix(downloads): improve responsive styles (#2552)

This commit is contained in:
Meier Lukas
2025-03-10 20:29:50 +01:00
committed by GitHub
parent 483ce9c28d
commit d714e53cfa
2 changed files with 71 additions and 157 deletions

View File

@@ -24,7 +24,6 @@ import {
Tooltip,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import type { IconProps } from "@tabler/icons-react";
import {
IconAlertTriangle,
IconCirclesRelation,
@@ -75,16 +74,6 @@ const columnsRatios: Record<keyof ExtendedDownloadClientItem, number> = {
upSpeed: 3,
};
const actionIconIconStyle: IconProps["style"] = {
height: "var(--ai-icon-size)",
width: "var(--ai-icon-size)",
};
const standardIconStyle: IconProps["style"] = {
height: "var(--icon-size)",
width: "var(--icon-size)",
};
export default function DownloadClientsWidget({
isEditMode,
integrationIds,
@@ -307,72 +296,41 @@ export default function DownloadClientsWidget({
upSpeed: options.columns.includes("upSpeed") && integrationTypes.includes("torrent"),
} satisfies Record<keyof ExtendedDownloadClientItem, boolean>;
//Set a relative width using ratio table
const totalWidth = options.columns.reduce(
(count: number, column) => (columnVisibility[column] ? count + columnsRatios[column] : count),
0,
);
//Default styling behavior for stopping interaction when editing. (Applied everywhere except the table header)
const editStyle: MantineStyleProp = {
pointerEvents: isEditMode ? "none" : undefined,
};
//General style sizing as vars that should apply or be applied to all elements
const baseStyle: MantineStyleProp = {
"--total-width": totalWidth,
"--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
"--button-fz": "var(--text-fz)",
"--icon-size": "calc(var(--ratio-width) * 2 / 3)", //Normal icon size
"--ai-icon-size": "calc(var(--ratio-width) * 0.5)", //Icon inside action icons size
"--button-size": "calc(var(--ratio-width) * 0.75)", //Action Icon, button and avatar size
"--image-size": "var(--button-size)",
"--mrt-base-background-color": "transparent",
};
//Base element in common with all columns
const columnsDefBase = useCallback(
({
key,
showHeader,
align,
}: {
key: keyof ExtendedDownloadClientItem;
showHeader: boolean;
align?: "center" | "left" | "right" | "justify" | "char";
}): MRT_ColumnDef<ExtendedDownloadClientItem> => {
const style: MantineStyleProp = {
minWidth: 0,
width: "var(--column-width)",
height: "var(--ratio-width)",
padding: "var(--space-size)",
transition: "unset",
"--key-width": columnsRatios[key],
"--column-width": "calc((var(--key-width)/var(--total-width) * 100cqw))",
};
return {
id: key,
accessorKey: key,
header: key,
size: columnsRatios[key],
mantineTableBodyCellProps: { style, align },
mantineTableHeadCellProps: {
style,
align: isEditMode ? "center" : align,
},
Header: () => (showHeader && !isEditMode ? <Text fw={700}>{t(`items.${key}.columnTitle`)}</Text> : ""),
Header: () =>
showHeader ? (
<Text fz="xs" fw={700}>
{t(`items.${key}.columnTitle`)}
</Text>
) : null,
};
},
[isEditMode, t],
[t],
);
//Make columns and cell elements, Memoized to data with deps on data and EditMode
const columns = useMemo<MRT_ColumnDef<ExtendedDownloadClientItem>[]>(
() => [
{
...columnsDefBase({ key: "actions", showHeader: false, align: "center" }),
...columnsDefBase({ key: "actions", showHeader: false }),
enableSorting: false,
Cell: ({ cell, row }) => {
const actions = cell.getValue<ExtendedDownloadClientItem["actions"]>();
@@ -380,19 +338,15 @@ export default function DownloadClientsWidget({
const [opened, { open, close }] = useDisclosure(false);
return actions ? (
<Group wrap="nowrap" gap="var(--space-size)">
<Group wrap="nowrap" gap="xs">
<Tooltip label={t(`actions.item.${pausedAction}`)}>
<ActionIcon variant="light" radius={999} onClick={actions[pausedAction]} size="var(--button-size)">
{pausedAction === "resume" ? (
<IconPlayerPlay style={actionIconIconStyle} />
) : (
<IconPlayerPause style={actionIconIconStyle} />
)}
<ActionIcon size="xs" variant="light" radius="100%" onClick={actions[pausedAction]}>
{pausedAction === "resume" ? <IconPlayerPlay /> : <IconPlayerPause />}
</ActionIcon>
</Tooltip>
<Tooltip label={t("actions.item.delete.title")}>
<ActionIcon color="red" radius={999} onClick={open} size="var(--button-size)">
<IconTrash style={actionIconIconStyle} />
<ActionIcon size="xs" color="red" radius="100%" onClick={open}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Modal opened={opened} onClose={close} title={t("actions.item.delete.modalTitle")} size="auto" centered>
@@ -423,68 +377,68 @@ export default function DownloadClientsWidget({
</Modal>
</Group>
) : (
<ActionIcon radius={999} disabled size="var(--button-size)">
<IconX style={actionIconIconStyle} />
<ActionIcon size="xs" radius="100%" disabled>
<IconX />
</ActionIcon>
);
},
},
{
...columnsDefBase({ key: "added", showHeader: true, align: "center" }),
...columnsDefBase({ key: "added", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const added = cell.getValue<ExtendedDownloadClientItem["added"]>();
return <Text>{added !== undefined ? dayjs(added).fromNow() : "unknown"}</Text>;
return <Text size="xs">{added !== undefined ? dayjs(added).fromNow() : "unknown"}</Text>;
},
},
{
...columnsDefBase({ key: "category", showHeader: false, align: "center" }),
...columnsDefBase({ key: "category", showHeader: false }),
sortUndefined: "last",
Cell: ({ cell }) => {
const category = cell.getValue<ExtendedDownloadClientItem["category"]>();
return (
category !== undefined && (
<Tooltip label={category}>
<IconInfoCircle style={standardIconStyle} />
<IconInfoCircle size={16} />
</Tooltip>
)
);
},
},
{
...columnsDefBase({ key: "downSpeed", showHeader: true, align: "right" }),
...columnsDefBase({ key: "downSpeed", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const downSpeed = cell.getValue<ExtendedDownloadClientItem["downSpeed"]>();
return downSpeed && <Text>{humanFileSize(downSpeed, "/s")}</Text>;
return downSpeed ? <Text size="xs">{humanFileSize(downSpeed, "/s")}</Text> : null;
},
},
{
...columnsDefBase({ key: "id", showHeader: false, align: "center" }),
...columnsDefBase({ key: "id", showHeader: false }),
enableSorting: false,
Cell: ({ cell }) => {
const id = cell.getValue<ExtendedDownloadClientItem["id"]>();
return (
<Tooltip label={id}>
<IconCirclesRelation style={standardIconStyle} />
<IconCirclesRelation size={16} />
</Tooltip>
);
},
},
{
...columnsDefBase({ key: "index", showHeader: true, align: "center" }),
...columnsDefBase({ key: "index", showHeader: true }),
Cell: ({ cell }) => {
const index = cell.getValue<ExtendedDownloadClientItem["index"]>();
return <Text>{index}</Text>;
return <Text size="xs">{index}</Text>;
},
},
{
...columnsDefBase({ key: "integration", showHeader: false, align: "center" }),
...columnsDefBase({ key: "integration", showHeader: false }),
Cell: ({ cell }) => {
const integration = cell.getValue<ExtendedDownloadClientItem["integration"]>();
return (
<Tooltip label={integration.name}>
<Avatar size="var(--image-size)" radius={0} src={getIconUrl(integration.kind)} />
<Avatar size="xs" radius={0} src={getIconUrl(integration.kind)} />
</Tooltip>
);
},
@@ -494,62 +448,61 @@ export default function DownloadClientsWidget({
Cell: ({ cell }) => {
const name = cell.getValue<ExtendedDownloadClientItem["name"]>();
return (
<Text lineClamp={1} style={{ wordBreak: "break-all" }}>
<Text size="xs" lineClamp={1} style={{ wordBreak: "break-all" }}>
{name}
</Text>
);
},
},
{
...columnsDefBase({ key: "progress", showHeader: true, align: "center" }),
...columnsDefBase({ key: "progress", showHeader: true }),
Cell: ({ cell, row }) => {
const progress = cell.getValue<ExtendedDownloadClientItem["progress"]>();
return (
<Stack w="100%" align="center" gap="var(--space-size)">
<Text lh="var(--text-fz)">
<Group align="center" gap="xs" wrap="nowrap" w="100%">
<Text size="xs">
{new Intl.NumberFormat("en", { style: "percent", notation: "compact", unitDisplay: "narrow" }).format(
progress,
)}
</Text>
<Progress
h="calc(var(--ratio-width)*0.25)"
w="100%"
value={progress * 100}
color={row.original.state === "paused" ? "yellow" : progress === 1 ? "green" : "blue"}
radius={999}
radius="lg"
/>
</Stack>
</Group>
);
},
},
{
...columnsDefBase({ key: "ratio", showHeader: true, align: "center" }),
...columnsDefBase({ key: "ratio", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const ratio = cell.getValue<ExtendedDownloadClientItem["ratio"]>();
return ratio !== undefined && <Text>{ratio.toFixed(ratio >= 100 ? 0 : ratio >= 10 ? 1 : 2)}</Text>;
return ratio !== undefined && <Text size="xs">{ratio.toFixed(ratio >= 100 ? 0 : ratio >= 10 ? 1 : 2)}</Text>;
},
},
{
...columnsDefBase({ key: "received", showHeader: true, align: "right" }),
...columnsDefBase({ key: "received", showHeader: true }),
Cell: ({ cell }) => {
const received = cell.getValue<ExtendedDownloadClientItem["received"]>();
return <Text>{humanFileSize(received)}</Text>;
return <Text size="xs">{humanFileSize(received)}</Text>;
},
},
{
...columnsDefBase({ key: "sent", showHeader: true, align: "right" }),
...columnsDefBase({ key: "sent", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const sent = cell.getValue<ExtendedDownloadClientItem["sent"]>();
return sent && <Text>{humanFileSize(sent)}</Text>;
return sent && <Text size="xs">{humanFileSize(sent)}</Text>;
},
},
{
...columnsDefBase({ key: "size", showHeader: true, align: "right" }),
...columnsDefBase({ key: "size", showHeader: true }),
Cell: ({ cell }) => {
const size = cell.getValue<ExtendedDownloadClientItem["size"]>();
return <Text>{humanFileSize(size)}</Text>;
return <Text size="xs">{humanFileSize(size)}</Text>;
},
},
{
@@ -557,25 +510,25 @@ export default function DownloadClientsWidget({
enableSorting: false,
Cell: ({ cell }) => {
const state = cell.getValue<ExtendedDownloadClientItem["state"]>();
return <Text>{t(`states.${state}`)}</Text>;
return <Text size="xs">{t(`states.${state}`)}</Text>;
},
},
{
...columnsDefBase({ key: "time", showHeader: true, align: "center" }),
...columnsDefBase({ key: "time", showHeader: true }),
Cell: ({ cell }) => {
const time = cell.getValue<ExtendedDownloadClientItem["time"]>();
return time === 0 ? <IconInfinity style={standardIconStyle} /> : <Text>{dayjs().add(time).fromNow()}</Text>;
return time === 0 ? <IconInfinity size={16} /> : <Text size="xs">{dayjs().add(time).fromNow()}</Text>;
},
},
{
...columnsDefBase({ key: "type", showHeader: true }),
Cell: ({ cell }) => {
const type = cell.getValue<ExtendedDownloadClientItem["type"]>();
return <Text>{type}</Text>;
return <Text size="xs">{type}</Text>;
},
},
{
...columnsDefBase({ key: "upSpeed", showHeader: true, align: "right" }),
...columnsDefBase({ key: "upSpeed", showHeader: true }),
sortUndefined: "last",
Cell: ({ cell }) => {
const upSpeed = cell.getValue<ExtendedDownloadClientItem["upSpeed"]>();
@@ -604,17 +557,17 @@ export default function DownloadClientsWidget({
mantineTableContainerProps: { style: { height: "100%" } },
mantineTableProps: {
className: "downloads-widget-table",
style: {
"--sortButtonSize": "var(--button-size)",
"--dragButtonSize": "var(--button-size)",
},
},
mantineTableBodyProps: { style: editStyle },
mantineTableHeadCellProps: {
p: 4,
},
mantineTableBodyCellProps: ({ cell, row }) => ({
onClick: () => {
setClickedIndex(row.index);
if (cell.column.id !== "actions") open();
},
p: 4,
}),
onColumnOrderChange: (order) => {
//Order has a tendency to add the disabled column at the end of the the real ordered array
@@ -666,26 +619,25 @@ export default function DownloadClientsWidget({
if (options.columns.length === 0)
return (
<Center h="100%">
<Text fz="7.5cqw">{t("errors.noColumns")}</Text>
<Text>{t("errors.noColumns")}</Text>
</Center>
);
//The actual widget
return (
<Stack gap={0} h="100%" display="flex" style={baseStyle}>
<Stack gap={0} h="100%" display="flex">
<MantineReactTable table={table} />
<Group
h={40}
px="var(--space-size)"
p={4}
justify={integrationTypes.includes("torrent") ? "space-between" : "end"}
style={{
borderTop: "0.0625rem solid var(--border-color)",
}}
>
{integrationTypes.includes("torrent") && (
<Group pt="var(--space-size)">
<Text>{`${t("globalRatio")}:`}</Text>
<Text>{(globalTraffic.up / globalTraffic.down).toFixed(2)}</Text>
<Group>
<Text size="xs" fw="bold">{`${t("globalRatio")}:`}</Text>
<Text size="xs">{(globalTraffic.up / globalTraffic.down).toFixed(2)}</Text>
</Group>
)}
<ClientsControl
@@ -806,12 +758,12 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
<Group gap={5}>
<Popover withinPortal={false} offset={0}>
<Popover.Target>
<ActionIcon size={30} radius={999} variant="light">
<IconFilter style={actionIconIconStyle} />
<ActionIcon size="xs" radius="lg" variant="light">
<IconFilter />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<Stack gap="md" align="center" pb="var(--space-size)">
<Stack gap="md" align="center">
<Text fw="700">{t("items.integration.columnTitle")}</Text>
<Chip.Group
multiple
@@ -839,7 +791,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
</Stack>
</Popover.Dropdown>
</Popover>
<AvatarGroup mx="calc(var(--space-size)*2)" spacing="calc(var(--space-size)*2)">
<AvatarGroup>
{clients.map((client) => (
<ClientAvatar key={client.integration.id} client={client} />
))}
@@ -847,37 +799,37 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
{someInteract && (
<Tooltip label={t("actions.clients.resume")}>
<ActionIcon
size={30}
radius={999}
size="xs"
radius="lg"
disabled={integrationsStatuses.paused.length === 0}
variant="light"
onClick={() => mutateResumeQueue({ integrationIds: integrationsStatuses.paused })}
>
<IconPlayerPlay style={actionIconIconStyle} />
<IconPlayerPlay />
</ActionIcon>
</Tooltip>
)}
<Button
h={20}
size="xs"
variant="light"
radius={999}
h="var(--button-size)"
px="calc(var(--space-size)*2)"
radius="lg"
fw="500"
onClick={open}
styles={{ label: { height: "fit-content", paddingBottom: "calc(var(--space-size)*0.75)" } }}
styles={{ label: { height: "fit-content" } }}
>
{totalSpeed}
</Button>
{someInteract && (
<Tooltip label={t("actions.clients.pause")}>
<ActionIcon
size={30}
radius={999}
size="xs"
radius="xl"
disabled={integrationsStatuses.active.length === 0}
variant="light"
onClick={() => mutatePauseQueue({ integrationIds: integrationsStatuses.active })}
>
<IconPlayerPause style={actionIconIconStyle} />
<IconPlayerPause />
</ActionIcon>
</Tooltip>
)}
@@ -967,9 +919,8 @@ const ClientAvatar = ({ client }: ClientAvatarProps) => {
key={client.integration.id}
src={getIconUrl(client.integration.kind)}
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
size={30}
size="sm"
p={5}
bd={`calc(var(--space-size)*0.5) solid ${client.status ? "transparent" : "var(--mantine-color-red-filled)"}`}
/>
);
};

View File

@@ -1,40 +1,3 @@
.downloads-widget-table {
/*Set Header static and overflow body instead*/
display: flex;
height: 100%;
flex-direction: column;
.mantine-Table-tbody {
overflow-y: auto;
flex: 1;
scrollbar-width: 0;
}
/*Hide scrollbar until I can apply an overlay scrollbar instead*/
.mantine-Table-tbody::-webkit-scrollbar {
width: 0;
}
/*Properly size header*/
.mrt-table-head-cell-labels {
min-height: var(--ratioWidth);
gap: 0;
padding: 0;
}
/*Properly size controls*/
.mrt-grab-handle-button {
margin: unset;
width: var(--dragButtonSize);
min-width: var(--dragButtonSize);
height: var(--dragButtonSize);
min-height: var(--dragButtonSize);
}
.mrt-table-head-sort-button {
margin: unset;
width: var(--sortButtonSize);
min-width: var(--sortButtonSize);
height: var(--sortButtonSize);
min-height: var(--sortButtonSize);
}
}
/*Make background of component different on hover, depending on base var*/
.hoverable-component {
&:hover {