fix(media widget): text size fix and added season + Episode info (#6605)

Co-authored-by: Thomas <thomas@ajnart.fr>
This commit is contained in:
BergashaandThomas authored and GitHub committed 2026-08-18 09:44:12 +02:00
1 parent e842177d9b
commit ebdfeb724b
61 files changed
+254 -182

No files matched your search

@@ -20,7 +20,9 @@ const sessionSchema = z.object({
SeriesName: z.string().nullish(),
Name: z.string().nullish(),
SeasonName: z.string().nullish(),
ParentIndexNumber: z.number().nullish(),
EpisodeTitle: z.string().nullish(),
IndexNumber: z.number().nullish(),
Album: z.string().nullish(),
EpisodeCount: z.number().nullish(),
})
@@ -114,7 +116,9 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat
type: convertJellyfinType(sessionInfo.NowPlayingItem.Type),
name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "",
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
seasonNumber: sessionInfo.NowPlayingItem.ParentIndexNumber ?? null,
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
episodeNumber: sessionInfo.NowPlayingItem.IndexNumber ?? null,
albumName: sessionInfo.NowPlayingItem.Album ?? "",
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
playback: {
@@ -10,7 +10,9 @@ export interface StreamSession {
type: "audio" | "video" | "tv" | "movie";
name: string;
seasonName: string | undefined;
seasonNumber?: number | null;
episodeName?: string | null;
episodeNumber?: number | null;
albumName?: string | null;
episodeCount?: number | null;
playback: {
@@ -64,7 +64,9 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte
type: convertJellyfinType(sessionInfo.NowPlayingItem.Type),
name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "",
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
seasonNumber: sessionInfo.NowPlayingItem.ParentIndexNumber ?? null,
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
episodeNumber: sessionInfo.NowPlayingItem.IndexNumber ?? null,
albumName: sessionInfo.NowPlayingItem.Album ?? "",
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
playback: {
@@ -26,7 +26,9 @@ const sessions: StreamSession[] = [
type: "video",
name: "Breaking Bad",
seasonName: "Season 5",
seasonNumber: 5,
episodeName: "Ozymandias",
episodeNumber: 14,
albumName: null,
episodeCount: null,
playback: { state: null, positionMs: null, durationMs: null },
@@ -58,7 +60,9 @@ const sessions: StreamSession[] = [
type: "video",
name: "Severance",
seasonName: "Season 2",
seasonNumber: 2,
episodeName: "The We We Are",
episodeNumber: 10,
albumName: null,
episodeCount: null,
playback: { state: null, positionMs: null, durationMs: null },
+2 -1
View File
@@ -80,8 +80,9 @@ interface Session {
$: {
grandparentTitle?: string;
parentTitle?: string;
parentIndex?: string;
title?: string;
index?: number;
index?: string;
type: string;
live?: string;
viewOffset?: string;
@@ -115,9 +115,11 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
type: mediaElement.$.live === "1" ? "tv" : PlexIntegration.getCurrentlyPlayingType(mediaElement.$.type),
name: mediaElement.$.grandparentTitle ?? mediaElement.$.title ?? "Unknown",
seasonName: mediaElement.$.parentTitle,
seasonNumber: parseOptionalNumber(mediaElement.$.parentIndex),
episodeName: mediaElement.$.title ?? null,
episodeNumber: parseOptionalNumber(mediaElement.$.index),
albumName: mediaElement.$.type === "track" ? (mediaElement.$.parentTitle ?? null) : null,
episodeCount: mediaElement.$.index ?? null,
episodeCount: parseOptionalNumber(mediaElement.$.index),
playback: {
state: playbackState,
positionMs,
+1
View File
@@ -4130,6 +4130,7 @@
"transcoding": "Transcoding",
"paused": "Paused",
"remaining": "{minutes} min left",
"seasonEpisode": "S{season} E{episode}",
"metadata": {
"title": "Stats for nerds",
"video": {
@@ -314,7 +314,7 @@ export function JsonPathTreePicker({
{hasData && (
<Combobox.Header>
<Group gap="xs" px="xs" pt="xs" pb={4}>
<Badge size="sm" color="teal" variant="light" leftSection={<IconCircleCheck size={12} />}>
<Badge size="sm" color="teal" variant="light" leftSection={<IconCircleCheck size="var(--mantine-font-size-xs)" />}>
{loadedHint}
</Badge>
<Text size="xs" c="dimmed">
@@ -353,7 +353,7 @@ function TreeNodeOption({ node, selected, isSearching }: { node: FlatNode; selec
<Group gap={4} wrap="nowrap" style={{ minWidth: 0 }}>
{node.hasChildren && (
<IconChevronDown
size={14}
size="var(--mantine-font-size-sm)"
style={{
flexShrink: 0,
color: "var(--mantine-color-dimmed)",
@@ -362,7 +362,7 @@ function TreeNodeOption({ node, selected, isSearching }: { node: FlatNode; selec
}}
/>
)}
<TypeIcon size={14} style={{ flexShrink: 0, opacity: 0.5 }} />
<TypeIcon size="var(--mantine-font-size-sm)" style={{ flexShrink: 0, opacity: 0.5 }} />
<Text
size="sm"
fw={selected ? 600 : 400}
@@ -76,7 +76,7 @@ export const WidgetCustomWidgetConfigurationInput = ({
return (
<Stack gap="md">
{issues[0] && (
<Alert color="yellow" variant="light" icon={<IconAlertTriangle size={16} />}>
<Alert color="yellow" variant="light" icon={<IconAlertTriangle size="var(--mantine-font-size-md)" />}>
<Text size="sm">{labels("needsAttention", { message: issues[0].message })}</Text>
</Alert>
)}
@@ -115,7 +115,7 @@ export const WidgetCustomWidgetSelectInput = ({
>
<Group wrap="nowrap" gap="sm">
<Avatar src={def.iconUrl} size={36} radius="md" color="blue">
<IconApi size={18} />
<IconApi size="var(--mantine-font-size-lg)" />
</Avatar>
<Stack gap={1} style={{ minWidth: 0 }}>
<Text size="sm" fw={600} truncate>
@@ -152,7 +152,7 @@ export const WidgetCustomWidgetSelectInput = ({
<Alert
color="yellow"
variant="light"
icon={<IconAlertTriangle size={18} />}
icon={<IconAlertTriangle size="var(--mantine-font-size-lg)" />}
title={labels("migrationRequired")}
>
<Text size="xs">{labels("migrationDescription")}</Text>
@@ -89,7 +89,7 @@ export const WidgetDynamicSelectInput = ({
style={{ marginInlineStart: "auto" }}
color="currentColor"
opacity={0.6}
size={18}
size="var(--mantine-font-size-lg)"
stroke={1.5}
/>
)}
@@ -97,7 +97,7 @@ export const WidgetLocationInput = ({ property, kind, options }: CommonWidgetInp
disabled={!selectionEnabled}
onClick={onSearch}
variant="light"
leftSection={<IconListSearch size={16} />}
leftSection={<IconListSearch size="var(--mantine-font-size-md)" />}
>
{tLocation("search")}
</Button>
@@ -257,7 +257,7 @@ const LocationSelectTableRow = ({ city, onLocationSelect, closeModal }: Location
})}
>
<ActionIcon color="red" variant="subtle" onClick={onSelect}>
<IconClick size={16} />
<IconClick size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
</Table.Td>
@@ -229,19 +229,19 @@ export const WidgetMultiReleasesRepositoriesInput = ({
})
}
variant="light"
leftSection={<IconEdit size={15} />}
leftSection={<IconEdit size="var(--mantine-font-size-sm)" />}
size="xs"
>
{tRepository("edit.label")}
</Button>
<ActionIcon variant="transparent" color="red" onClick={() => onRepositoryRemove(index)}>
<IconTrash size={15} />
<IconTrash size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Group>
{Object.keys(form.errors).filter((key) => key.startsWith(`options.${property}.${index}.`)).length > 0 && (
<Group align="center" justify="center" gap="xs" bg="red.1">
<IconTriangleFilled size={15} color="var(--mantine-color-red-filled)" />
<IconTriangleFilled size="var(--mantine-font-size-sm)" color="var(--mantine-color-red-filled)" />
<Text size="sm" c="red">
{tRepository("invalid")}
</Text>
@@ -371,7 +371,7 @@ const ProviderTokenInput = ({
}}
style={{ flex: 1 }}
size="xs"
leftSection={<IconKey size={14} />}
leftSection={<IconKey size="var(--mantine-font-size-sm)" />}
/>
{editing && value.trim() && (
<Button size="xs" onClick={handleSave} loading={saving}>
@@ -380,7 +380,7 @@ const ProviderTokenInput = ({
)}
{hasToken && !editing && (
<ActionIcon variant="light" color="red" size="sm" onClick={handleDelete} loading={saving}>
<IconTrash size={14} />
<IconTrash size="var(--mantine-font-size-sm)" />
</ActionIcon>
)}
</Group>
@@ -51,20 +51,20 @@ export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInput
label: translateIfNecessary(t, option.label) ?? option.value,
},
)}
leftSection={CurrentIcon && <CurrentIcon size={16} stroke={1.5} />}
leftSection={CurrentIcon && <CurrentIcon size="var(--mantine-font-size-md)" stroke={1.5} />}
renderOption={({ option, checked }) => {
const Icon = getIconFor(options.options, String(option.value));
return (
<Group flex="1" gap="xs">
{Icon && <Icon color="currentColor" opacity={0.6} size={18} stroke={1.5} />}
{Icon && <Icon color="currentColor" opacity={0.6} size="var(--mantine-font-size-lg)" stroke={1.5} />}
{option.label}
{checked && (
<IconCheck
style={{ marginInlineStart: "auto" }}
color="currentColor"
opacity={0.6}
size={18}
size="var(--mantine-font-size-lg)"
stroke={1.5}
/>
)}
@@ -263,7 +263,7 @@ const AnchorNoteWidgetContent = ({
onClick={handleEdit}
disabled={isUpdating}
>
<IconEdit size={16} />
<IconEdit size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
)}
+1 -1
View File
@@ -18,7 +18,7 @@ export default function AssistantWidget(props: WidgetComponentProps<"assistant">
<Center h="100%" p="md">
<Stack align="center" gap="xs" ta="center">
<ThemeIcon variant="light" color="red" radius="xl" size="lg">
<IconRobot size={20} />
<IconRobot size="var(--mantine-font-size-xl)" />
</ThemeIcon>
<Text size="sm" fw={600}>
{t("title")}
@@ -201,7 +201,7 @@ export default function BeszelSystemStatsWidget({
<Button
variant="default"
size="compact-xs"
leftSection={<IconServer size={14} />}
leftSection={<IconServer size="var(--mantine-font-size-sm)" />}
className={classes.beszelStatsSystemToggle}
disabled={isSelectionSavePending}
>
+1 -1
View File
@@ -49,7 +49,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
</Group>
<ActionIcon variant="transparent" color="red" onClick={removeItem}>
<IconX size={20} />
<IconX size="var(--mantine-font-size-xl)" />
</ActionIcon>
</Group>
);
+2 -2
View File
@@ -303,7 +303,7 @@ const CalendarAgenda = ({
disabled={isEditMode}
onClick={() => setMonth(moveCalendarMonth(month, -1))}
>
<IconChevronLeft size={18} aria-hidden />
<IconChevronLeft size="var(--mantine-font-size-lg)" aria-hidden />
</ActionIcon>
</Tooltip>
<Stack gap={0} miw={0}>
@@ -323,7 +323,7 @@ const CalendarAgenda = ({
disabled={isEditMode}
onClick={() => setMonth(moveCalendarMonth(month, 1))}
>
<IconChevronRight size={18} aria-hidden />
<IconChevronRight size="var(--mantine-font-size-lg)" aria-hidden />
</ActionIcon>
</Tooltip>
</Group>
@@ -25,7 +25,7 @@ export function IntegrationErrorIndicator({ results }: IntegrationErrorIndicator
return (
<Tooltip label={label} position="left" withArrow>
<ActionIcon variant="transparent" color="orange" size={28} aria-label={label} style={{ cursor: "help" }}>
<IconAlertTriangle aria-hidden size={14} />
<IconAlertTriangle aria-hidden size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
);
@@ -28,7 +28,7 @@ export function WidgetQueryErrorIndicator({ error, label }: WidgetQueryErrorIndi
tabIndex={0}
style={{ cursor: "help" }}
>
<IconAlertTriangle aria-hidden size={14} />
<IconAlertTriangle aria-hidden size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
);
@@ -22,7 +22,7 @@ export function ApplicationsSection({ applications, baseUrl, isTiny }: Applicati
return (
<Accordion.Item value="applications">
<Accordion.Control icon={isTiny ? null : <IconCloud size={16} />}>
<Accordion.Control icon={isTiny ? null : <IconCloud size="var(--mantine-font-size-md)" />}>
<Group gap="xs">
<Text size="xs">{tCommon("applications")}</Text>
<Badge variant="dot" color={getBadgeColor(runningApps, applications.length)} size="xs">
@@ -106,7 +106,7 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
variant="subtle"
c="dimmed"
>
<IconLink size={12} />
<IconLink size="var(--mantine-font-size-xs)" />
</ActionIcon>
)}
{!isTiny && logsUrl && (
@@ -121,7 +121,7 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
variant="subtle"
c="dimmed"
>
<IconFileText size={12} />
<IconFileText size="var(--mantine-font-size-xs)" />
</ActionIcon>
)}
{!isTiny && (
@@ -43,7 +43,7 @@ export function ServersSection({
return (
<Accordion.Item value="servers">
<Group gap={0} wrap="nowrap">
<Accordion.Control icon={isTiny ? null : <IconServer size={16} />} style={{ flex: 1, minWidth: 0 }}>
<Accordion.Control icon={isTiny ? null : <IconServer size="var(--mantine-font-size-md)" />} style={{ flex: 1, minWidth: 0 }}>
<Group gap="xs">
<Text size="xs">{tCommon("servers")}</Text>
<Badge variant="dot" color={getBadgeColor(onlineServers, servers.length)} size="xs">
@@ -61,7 +61,7 @@ export function ServersSection({
mr="xs"
onClick={onToggleIp}
>
{showIp ? <IconEye size={12} /> : <IconEyeOff size={12} />}
{showIp ? <IconEye size="var(--mantine-font-size-xs)" /> : <IconEyeOff size="var(--mantine-font-size-xs)" />}
</ActionIcon>
</Group>
<Accordion.Panel p={4}>
@@ -25,7 +25,7 @@ export function ServicesSection({ services, baseUrl, isTiny, isAdvanced }: Servi
return (
<Accordion.Item value="services">
<Accordion.Control icon={isTiny ? null : <IconStack2 size={16} />}>
<Accordion.Control icon={isTiny ? null : <IconStack2 size="var(--mantine-font-size-md)" />}>
<Group gap="xs">
<Text size="xs">{tCommon("services")}</Text>
<Badge variant="dot" color={getBadgeColor(runningServices, services.length)} size="xs">
@@ -90,7 +90,7 @@ const TimerModal = ({ opened, close, selectedIntegrationIds, disableDns }: Timer
<Button
variant="light"
color="red"
leftSection={<IconClockPause size={20} />}
leftSection={<IconClockPause size="var(--mantine-font-size-xl)" />}
h="2rem"
w="12rem"
onClick={handleSetTimer}
@@ -184,7 +184,7 @@ export default function DnsHoleControlsWidget({
radius={board.itemRadius}
flex={1}
>
<IconPlayerPlay className="dns-hole-controls-enable-all-icon" size={16} />
<IconPlayerPlay className="dns-hole-controls-enable-all-icon" size="var(--mantine-font-size-md)" />
</Button>
</Tooltip>
@@ -205,7 +205,7 @@ export default function DnsHoleControlsWidget({
radius={board.itemRadius}
flex={1}
>
<IconClockPause className="dns-hole-controls-timer-all-icon" size={16} />
<IconClockPause className="dns-hole-controls-timer-all-icon" size="var(--mantine-font-size-md)" />
</Button>
</Tooltip>
@@ -223,7 +223,7 @@ export default function DnsHoleControlsWidget({
radius={board.itemRadius}
flex={1}
>
<IconPlayerStop className="dns-hole-controls-disable-all-icon" size={16} />
<IconPlayerStop className="dns-hole-controls-disable-all-icon" size="var(--mantine-font-size-md)" />
</Button>
</Tooltip>
</Flex>
@@ -378,7 +378,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
color="green"
variant="light"
>
<IconPlayerPlay size={12} />
<IconPlayerPlay size="var(--mantine-font-size-xs)" />
</ActionIcon>
) : (
<ActionIcon
@@ -390,7 +390,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
color="red"
variant="light"
>
<IconPlayerStop size={12} />
<IconPlayerStop size="var(--mantine-font-size-xs)" />
</ActionIcon>
)}
<ActionIcon
@@ -406,7 +406,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
display={isInteractPermitted ? undefined : "none"}
disabled={!controlEnabled || !isEnabled}
>
<IconClockPause size={12} />
<IconClockPause size="var(--mantine-font-size-xs)" />
</ActionIcon>
</Group>
)}
@@ -436,7 +436,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
<IconCircleFilled
className="dns-hole-controls-item-status-icon"
color={dnsLightStatus(isEnabled)}
size={16}
size="var(--mantine-font-size-md)"
/>
)
}
@@ -474,7 +474,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
open();
}}
>
<IconClockPause className="dns-hole-controls-item-timer-icon" size={20} />
<IconClockPause className="dns-hole-controls-item-timer-icon" size="var(--mantine-font-size-xl)" />
</ActionIcon>
)}
</Flex>
+8 -8
View File
@@ -430,7 +430,7 @@ export default function DockerWidget({
wrap="nowrap"
>
<Group gap={4} wrap="nowrap">
<IconBrandDocker size={20} style={{ flexShrink: 0 }} />
<IconBrandDocker size="var(--mantine-font-size-xl)" style={{ flexShrink: 0 }} />
<Text size="sm" truncate>
{t("table.footer", { count: containers.length.toString() })}
</Text>
@@ -457,7 +457,7 @@ export default function DockerWidget({
onClick={() => refreshInventory.mutate()}
aria-label={t("table.refresh.lastUpdated", { when: relativeTime })}
>
<IconRefresh size={16} />
<IconRefresh size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
</Group>
@@ -491,7 +491,7 @@ function ContainerMenuButton({
aria-label={t("title")}
onClick={(event) => event.stopPropagation()}
>
<IconDots size={16} />
<IconDots size="var(--mantine-font-size-md)" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown w={containerMenuWidth} miw={containerMenuWidth} maw={containerMenuWidth}>
@@ -525,7 +525,7 @@ function ContainerActionItems({
{container.name}
</Menu.Label>
<Menu.Item
leftSection={<IconFileText size={14} />}
leftSection={<IconFileText size="var(--mantine-font-size-sm)" />}
disabled={!handlers.canUse(container, "logs")}
onClick={() => {
handlers.onOpenLogs(container);
@@ -537,7 +537,7 @@ function ContainerActionItems({
<Menu.Divider />
<Menu.Item
color={stateAction === "start" ? "green" : "red"}
leftSection={<StateIcon size={14} />}
leftSection={<StateIcon size="var(--mantine-font-size-sm)" />}
disabled={!handlers.canUse(container, "lifecycle")}
onClick={() => invokeAction(stateAction)}
>
@@ -545,7 +545,7 @@ function ContainerActionItems({
</Menu.Item>
<Menu.Item
color="orange"
leftSection={<IconRotateClockwise size={14} />}
leftSection={<IconRotateClockwise size="var(--mantine-font-size-sm)" />}
disabled={!handlers.canUse(container, "lifecycle")}
onClick={() => invokeAction("restart")}
>
@@ -553,7 +553,7 @@ function ContainerActionItems({
</Menu.Item>
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
leftSection={<IconTrash size="var(--mantine-font-size-sm)" />}
disabled={!handlers.canUse(container, "remove")}
onClick={() => invokeAction("remove")}
>
@@ -561,7 +561,7 @@ function ContainerActionItems({
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconCategoryPlus size={14} />}
leftSection={<IconCategoryPlus size="var(--mantine-font-size-sm)" />}
onClick={() => {
handlers.onAddToHomarr(container);
onClose();
+18 -18
View File
@@ -279,7 +279,7 @@ function SpeedCell({
const { Icon, color } = speedColumnConfig[direction];
return (
<Group gap={4} wrap="nowrap">
<Icon size={12} style={{ flexShrink: 0, opacity: 0.5 }} />
<Icon size="var(--mantine-font-size-xs)" style={{ flexShrink: 0, opacity: 0.5 }} />
<Text size={fontSize} c={color}>
{formatByteRate(speed)}
</Text>
@@ -914,7 +914,7 @@ function ExpandedRow({ item, collapse }: { item: ExtendedDownloadClientItem; col
<DetailPair
label={t("items.downSpeed.detailsTitle")}
value={formatByteRate(item.downSpeed)}
icon={<IconDownload size={10} />}
icon={<IconDownload size="var(--mantine-font-size-xs)" />}
color="blue"
/>
)}
@@ -922,7 +922,7 @@ function ExpandedRow({ item, collapse }: { item: ExtendedDownloadClientItem; col
<DetailPair
label={t("items.upSpeed.detailsTitle")}
value={formatByteRate(item.upSpeed)}
icon={<IconUpload size={10} />}
icon={<IconUpload size="var(--mantine-font-size-xs)" />}
color="green"
/>
)}
@@ -1005,7 +1005,7 @@ function GlobalStatsBar({
<Tooltip label={t("stats.totalSize")} withArrow>
<Group gap={4}>
<IconDatabase size={12} style={{ opacity: 0.6 }} />
<IconDatabase size="var(--mantine-font-size-xs)" style={{ opacity: 0.6 }} />
<Text size="xs">{`${formatBytes(queueStats.completedSize)} / ${formatBytes(queueStats.totalSize)}`}</Text>
</Group>
</Tooltip>
@@ -1013,14 +1013,14 @@ function GlobalStatsBar({
<Group gap="md">
<Group gap={4}>
<IconDownload size={12} style={{ opacity: 0.6 }} />
<IconDownload size="var(--mantine-font-size-xs)" style={{ opacity: 0.6 }} />
<Text size="xs" fw={600} c="blue">
{formatByteRate(totalSpeed)}
</Text>
</Group>
{totalUpSpeed > 0 && (
<Group gap={4}>
<IconUpload size={12} style={{ opacity: 0.6 }} />
<IconUpload size="var(--mantine-font-size-xs)" style={{ opacity: 0.6 }} />
<Text size="xs" fw={600} c="green">
{formatByteRate(totalUpSpeed)}
</Text>
@@ -1093,7 +1093,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos
<Menu.Label>{truncateText(item.name, 40)}</Menu.Label>
<Menu.Item
leftSection={<IconInfoCircle size={14} />}
leftSection={<IconInfoCircle size="var(--mantine-font-size-sm)" />}
onClick={() => {
void navigator.clipboard.writeText(item.name).catch(() => {});
onClose();
@@ -1104,7 +1104,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos
{item.id && (
<Menu.Item
leftSection={<IconCopy size={14} />}
leftSection={<IconCopy size="var(--mantine-font-size-sm)" />}
onClick={() => {
void navigator.clipboard.writeText(item.id).catch(() => {});
onClose();
@@ -1118,7 +1118,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos
<>
<Menu.Divider />
<Menu.Item
leftSection={<PauseResumeIcon size={14} />}
leftSection={<PauseResumeIcon size="var(--mantine-font-size-sm)" />}
onClick={() => {
pauseResumeInvoke?.();
onClose();
@@ -1130,7 +1130,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos
{!showDeleteConfirm && (
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
leftSection={<IconTrash size="var(--mantine-font-size-sm)" />}
onClick={(e) => {
e.stopPropagation();
openDelete();
@@ -1144,7 +1144,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos
<>
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
leftSection={<IconTrash size="var(--mantine-font-size-sm)" />}
onClick={() => {
item.actions?.delete({ fromDisk: false });
onClose();
@@ -1154,7 +1154,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos
</Menu.Item>
<Menu.Item
color="red"
leftSection={<IconTrashX size={14} />}
leftSection={<IconTrashX size="var(--mantine-font-size-sm)" />}
onClick={() => {
item.actions?.delete({ fromDisk: true });
onClose();
@@ -1321,14 +1321,14 @@ function WidgetFooter({
}
}}
>
<FilterIcon size={14} />
<FilterIcon size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
{toggleStats ? (
<Tooltip label={statsTooltip}>
<ActionIcon size="xs" variant={statsIconVariant} aria-label={statsTooltip} onClick={toggleStats}>
<StatsIcon size={14} />
<StatsIcon size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
) : null}
@@ -1356,7 +1356,7 @@ function WidgetFooter({
disabled={integrationsStatuses.paused.length === 0}
onClick={() => resumeQueue({ integrationIds: integrationsStatuses.paused })}
>
<IconPlayerPlay size={14} />
<IconPlayerPlay size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
)}
@@ -1364,12 +1364,12 @@ function WidgetFooter({
{!showStats && (
<Group gap={2}>
<Text size="xs" fw={600} c="blue">
<IconDownload size={10} style={{ verticalAlign: "middle", marginRight: 2 }} />
<IconDownload size="var(--mantine-font-size-xs)" style={{ verticalAlign: "middle", marginRight: 2 }} />
{formatByteRate(totalSpeed)}
</Text>
{totalUpSpeed > 0 && (
<Text size="xs" fw={600} c="green">
<IconUpload size={10} style={{ verticalAlign: "middle", marginRight: 2 }} />
<IconUpload size="var(--mantine-font-size-xs)" style={{ verticalAlign: "middle", marginRight: 2 }} />
{formatByteRate(totalUpSpeed)}
</Text>
)}
@@ -1386,7 +1386,7 @@ function WidgetFooter({
disabled={integrationsStatuses.active.length === 0}
onClick={() => pauseQueue({ integrationIds: integrationsStatuses.active })}
>
<IconPlayerPause size={14} />
<IconPlayerPause size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
)}
+3 -3
View File
@@ -386,7 +386,7 @@ const InterfacesPanel = ({
variant={isAdvanced ? "contained" : "default"}
>
<Accordion.Item value="interfaces">
<Accordion.Control icon={<IconTopologyBus size={16} />}>
<Accordion.Control icon={<IconTopologyBus size="var(--mantine-font-size-md)" />}>
<Group justify="space-between" wrap="nowrap" gap="xs">
<Text size="xs">{label}</Text>
{hasError && (
@@ -410,7 +410,7 @@ const InterfacesPanel = ({
</Text>
<Group gap={4} wrap="nowrap">
<IconArrowBarUp
size={12}
size="var(--mantine-font-size-xs)"
color="light-dark(var(--mantine-color-green-8), var(--mantine-color-green-3))"
/>
<Text size="xs" c="light-dark(var(--mantine-color-green-8), var(--mantine-color-green-3))">
@@ -419,7 +419,7 @@ const InterfacesPanel = ({
</Group>
<Group gap={4} wrap="nowrap">
<IconArrowBarDown
size={12}
size="var(--mantine-font-size-xs)"
color="light-dark(var(--mantine-color-yellow-9), var(--mantine-color-yellow-3))"
/>
<Text size="xs" c="light-dark(var(--mantine-color-yellow-9), var(--mantine-color-yellow-3))">
@@ -26,7 +26,7 @@ export const ResourceAccordionItem = ({
}: PropsWithChildren<ResourceAccordionItemProps>) => {
return (
<Accordion.Item value={value}>
<Accordion.Control icon={isTiny ? null : <Icon size={16} />}>
<Accordion.Control icon={isTiny ? null : <Icon size="var(--mantine-font-size-md)" />}>
<Group style={{ rowGap: "0" }} gap="xs">
<Text size="xs">{title}</Text>
<Badge variant="dot" color={badge.color} size="xs">
@@ -97,20 +97,20 @@ const ComputeResourceDetails = ({ item }: { item: ComputeResource }) => {
const storage = formatBytesPair(item.storage.used, item.storage.total);
return (
<List>
<List.Item icon={<IconCpu size={16} />}>
<List.Item icon={<IconCpu size="var(--mantine-font-size-md)" />}>
{t("cpu")} - {item.cpu.cores}
</List.Item>
<List.Item icon={<IconBrain size={16} />}>
<List.Item icon={<IconBrain size="var(--mantine-font-size-md)" />}>
{t("memory")} - {memory.used} / {memory.available}
</List.Item>
<List.Item icon={<IconDatabase size={16} />}>
<List.Item icon={<IconDatabase size="var(--mantine-font-size-md)" />}>
{t("storage")} - {storage.used} / {storage.available}
</List.Item>
<List.Item icon={<IconClockHour3 size={16} />}>
<List.Item icon={<IconClockHour3 size="var(--mantine-font-size-md)" />}>
{t("uptime")} - {dayjs(dayjs().add(-item.uptime, "seconds")).fromNow(true)}
</List.Item>
{item.haState && (
<List.Item icon={<IconHeartBolt size={16} />}>
<List.Item icon={<IconHeartBolt size="var(--mantine-font-size-md)" />}>
{t("haState")} - {capitalize(item.haState)}
</List.Item>
)}
@@ -152,15 +152,15 @@ const DiskStats = ({ item }: { item: ComputeResource }) => {
return null;
}
return (
<List.Item icon={<IconDatabase size={16} />}>
<List.Item icon={<IconDatabase size="var(--mantine-font-size-md)" />}>
<Group gap="sm">
<Group gap={0}>
<Text>{formatBytes(item.storage.write)}</Text>
<IconArrowNarrowDown size={14} />
<IconArrowNarrowDown size="var(--mantine-font-size-sm)" />
</Group>
<Group gap={0}>
<Text>{formatBytes(item.storage.read)}</Text>
<IconArrowNarrowUp size={14} />
<IconArrowNarrowUp size="var(--mantine-font-size-sm)" />
</Group>
</Group>
</List.Item>
@@ -172,15 +172,15 @@ const NetStats = ({ item }: { item: ComputeResource }) => {
return null;
}
return (
<List.Item icon={<IconNetwork size={16} />}>
<List.Item icon={<IconNetwork size="var(--mantine-font-size-md)" />}>
<Group gap="sm">
<Group gap={0}>
<Text>{formatBytes(item.network.in)}</Text>
<IconArrowNarrowDown size={14} />
<IconArrowNarrowDown size="var(--mantine-font-size-sm)" />
</Group>
<Group gap={0}>
<Text>{formatBytes(item.network.out)}</Text>
<IconArrowNarrowUp size={14} />
<IconArrowNarrowUp size="var(--mantine-font-size-sm)" />
</Group>
</Group>
</List.Item>
@@ -178,7 +178,7 @@ function Carousel({
size={40}
onClick={() => move(-1)}
>
<IconChevronLeft size={18} />
<IconChevronLeft size="var(--mantine-font-size-lg)" />
</ActionIcon>
)}
<ActionIcon
@@ -189,7 +189,7 @@ function Carousel({
size={advanced ? 40 : 32}
onClick={() => setPaused((value) => !value)}
>
{paused ? <IconPlayerPlay size={18} /> : <IconPlayerPause size={18} />}
{paused ? <IconPlayerPlay size="var(--mantine-font-size-lg)" /> : <IconPlayerPause size="var(--mantine-font-size-lg)" />}
</ActionIcon>
{advanced && (
<ActionIcon
@@ -200,7 +200,7 @@ function Carousel({
size={40}
onClick={() => move(1)}
>
<IconChevronRight size={18} />
<IconChevronRight size="var(--mantine-font-size-lg)" />
</ActionIcon>
)}
</Group>
@@ -214,7 +214,7 @@ function Carousel({
</Text>
)}
<Group gap="xs">
<IconCalendar size={16} />
<IconCalendar size="var(--mantine-font-size-md)" />
<Text size="xs">{new Date(currentAsset.fileModifiedAt).toLocaleDateString(locale)}</Text>
</Group>
<Text size="xs" c="dimmed">
@@ -59,7 +59,7 @@ export default function ImmichServerStatsWidget({
<SimpleGrid cols={statsLayout.columns} spacing={statsLayout.dense ? 4 : "sm"}>
{statVisibility.showUsers && (
<StatItem
icon={<IconUsers size={20} />}
icon={<IconUsers size="var(--mantine-font-size-xl)" />}
label={t("widget.immich-serverStats.users")}
value={stats.userCount.toLocaleString(locale)}
dense={statsLayout.dense}
@@ -67,7 +67,7 @@ export default function ImmichServerStatsWidget({
)}
{statVisibility.showPhotos && (
<StatItem
icon={<IconPhoto size={20} />}
icon={<IconPhoto size="var(--mantine-font-size-xl)" />}
label={t("widget.immich-serverStats.photos")}
value={stats.photoCount.toLocaleString(locale)}
dense={statsLayout.dense}
@@ -75,7 +75,7 @@ export default function ImmichServerStatsWidget({
)}
{statVisibility.showVideos && (
<StatItem
icon={<IconVideo size={20} />}
icon={<IconVideo size="var(--mantine-font-size-xl)" />}
label={t("widget.immich-serverStats.videos")}
value={stats.videoCount.toLocaleString(locale)}
dense={statsLayout.dense}
@@ -83,7 +83,7 @@ export default function ImmichServerStatsWidget({
)}
{statVisibility.showStorage && (
<StatItem
icon={<IconDatabase size={20} />}
icon={<IconDatabase size="var(--mantine-font-size-xl)" />}
label={t("widget.immich-serverStats.storage")}
value={formatBytes(stats.totalLibraryUsageInBytes)}
dense={statsLayout.dense}
@@ -133,7 +133,7 @@ export default function IndexerManagerWidget({
}}
aria-label={t("widget.indexerManager.testAll")}
>
<IconTestPipe size={14} />
<IconTestPipe size="var(--mantine-font-size-sm)" />
</ActionIcon>
</Tooltip>
</Group>
@@ -141,7 +141,7 @@ export default function MediaMissingWidget({
{showMissing && (
<Paper withBorder radius="sm" style={{ minHeight: 0, overflow: "hidden" }}>
<Group p="xs" gap="xs">
<IconQuestionMark size={16} />
<IconQuestionMark size="var(--mantine-font-size-md)" />
<Text size="sm" fw={600}>
{tabLabel(t("tab.missing"), missing.length, missingCount)}
</Text>
@@ -152,7 +152,7 @@ export default function MediaMissingWidget({
{showQueued && (
<Paper withBorder radius="sm" style={{ minHeight: 0, overflow: "hidden" }}>
<Group p="xs" gap="xs">
<IconDownload size={16} />
<IconDownload size="var(--mantine-font-size-md)" />
<Text size="sm" fw={600}>
{tabLabel(t("tab.queued"), queued.length, queuedCount)}
</Text>
@@ -181,12 +181,12 @@ export default function MediaMissingWidget({
</Group>
<Tabs.List grow>
{showMissing && (
<Tabs.Tab value="missing" px={isThin ? 6 : undefined} leftSection={<IconQuestionMark size={14} />}>
<Tabs.Tab value="missing" px={isThin ? 6 : undefined} leftSection={<IconQuestionMark size="var(--mantine-font-size-sm)" />}>
{tabLabel(t("tab.missing"), missing.length, missingCount)}
</Tabs.Tab>
)}
{showQueued && (
<Tabs.Tab value="queued" px={isThin ? 6 : undefined} leftSection={<IconDownload size={14} />}>
<Tabs.Tab value="queued" px={isThin ? 6 : undefined} leftSection={<IconDownload size="var(--mantine-font-size-sm)" />}>
{tabLabel(t("tab.queued"), queued.length, queuedCount)}
</Tabs.Tab>
)}
@@ -273,7 +273,7 @@ const InfoDivider = () => (
const Info = ({ icon: Icon, label }: IconAndLabelProps) => {
return (
<Group gap={4} wrap="nowrap" miw={0}>
{Icon && <Icon size={12} color={getMantineColor("gray", 5)} style={{ flexShrink: 0 }} />}
{Icon && <Icon size="var(--mantine-font-size-xs)" color={getMantineColor("gray", 5)} style={{ flexShrink: 0 }} />}
<Text size="xs" c="dimmed" truncate="end">
{label}
</Text>
@@ -94,7 +94,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[]
aria-label={t("action.search.label")}
onClick={() => openMediaRequestSearch({ integrationIds })}
>
<IconSearch size={16} />
<IconSearch size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
);
@@ -281,7 +281,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible
handleDecision("approve");
}}
>
<IconThumbUp size={16} />
<IconThumbUp size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
<Tooltip label={t("pending.decline")}>
@@ -296,7 +296,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible
handleDecision("decline");
}}
>
<IconThumbDown size={16} />
<IconThumbDown size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
</Group>
@@ -132,7 +132,7 @@ export default function MediaServerWidget({
<Tooltip label={t(`titles.stats.${stat.name}`)}>
<Card p={0} radius={board.itemRadius} className={classes.card}>
<Group className="mediaRequests-stats-stat-stack" justify="center" align="center" gap="xs" w="100%">
<stat.icon className="mediaRequests-stats-stat-icon" size={16} />
<stat.icon className="mediaRequests-stats-stat-icon" size="var(--mantine-font-size-md)" />
<Text className="mediaRequests-stats-stat-value" size="md">
{stat.number}
</Text>
@@ -226,7 +226,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[]
aria-label={t("action.search.label")}
onClick={() => openMediaRequestSearch({ integrationIds })}
>
<IconSearch size={16} />
<IconSearch size="var(--mantine-font-size-md)" />
</ActionIcon>
</Tooltip>
);
@@ -23,8 +23,10 @@
.tableViewport :global(th),
.tableViewport :global(td) {
padding-block: 4px;
padding-block: 8px;
padding-inline: 8px;
overflow: hidden;
vertical-align: top;
}
.tableViewport :global(th) {
@@ -41,7 +43,6 @@
.cellContent {
min-width: 0;
overflow: hidden;
}
.userIdentity {
@@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import type { StreamSession } from "@homarr/integrations";
import { getMediaServerColumnVisibility, SessionDetailsPopover } from "./component";
import { getMediaServerColumnVisibility, getSeasonEpisodeParams, SessionDetailsPopover } from "./component";
vi.mock("@homarr/translation/client", () => ({
useScopedI18n: () => (key: string) => key,
@@ -108,3 +108,19 @@ describe("getMediaServerColumnVisibility", () => {
expect(getMediaServerColumnVisibility(0, true)).toEqual({ user: true, status: true });
});
});
describe("getSeasonEpisodeParams", () => {
test("zero-pads season and episode numbers", () => {
expect(getSeasonEpisodeParams(4, 12)).toEqual({ season: "04", episode: "12" });
});
test("does not pad numbers already two digits or longer", () => {
expect(getSeasonEpisodeParams(12, 104)).toEqual({ season: "12", episode: "104" });
});
test("returns null when either number is missing", () => {
expect(getSeasonEpisodeParams(undefined, 12)).toBeNull();
expect(getSeasonEpisodeParams(4, null)).toBeNull();
expect(getSeasonEpisodeParams(null, undefined)).toBeNull();
});
});
+40 -14
View File
@@ -77,6 +77,16 @@ function formatBitrate(bitrateKbps: number | null | undefined): string | null {
return bitrateKbps >= 1000 ? `${(bitrateKbps / 1000).toFixed(1)} Mbps` : `${Math.round(bitrateKbps)} kbps`;
}
export function getSeasonEpisodeParams(
seasonNumber: number | null | undefined,
episodeNumber: number | null | undefined,
): { season: string; episode: string } | null {
if (seasonNumber === null || seasonNumber === undefined || episodeNumber === null || episodeNumber === undefined) {
return null;
}
return { season: String(seasonNumber).padStart(2, "0"), episode: String(episodeNumber).padStart(2, "0") };
}
const getSessionSortValue = (session: StreamSession, column: SortColumn) => {
if (column === "user") return session.user?.username ?? session.sessionName;
if (column === "currentlyPlaying") return session.currentlyPlaying?.name ?? session.sessionName;
@@ -124,7 +134,7 @@ function StreamTableHeader({
<Text component="span" size="xs" fw={600} truncate>
{label}
</Text>
<SortIcon size={13} aria-hidden />
<SortIcon size="var(--mantine-font-size-xs)" aria-hidden />
</UnstyledButton>
) : (
<Text size="xs" fw={600} truncate>
@@ -225,7 +235,7 @@ export default function MediaServerWidget({
onChange={(event) => setSearch(event.currentTarget.value)}
placeholder={tGlobal("search.placeholder")}
aria-label={tGlobal("search.placeholder")}
leftSection={<IconSearch size={14} aria-hidden />}
leftSection={<IconSearch size="var(--mantine-font-size-xs)" aria-hidden />}
/>
<WidgetQueryErrorIndicator error={currentStreamsQuery.error} label={t("name")} />
<IntegrationErrorIndicator results={currentStreams} />
@@ -304,13 +314,13 @@ export default function MediaServerWidget({
>
{columnVisibility.user && (
<Table.Td>
<Group gap="xs" wrap="nowrap" style={{ minWidth: 0 }}>
<Avatar size={28} src={session.user?.profilePictureUrl} style={{ flexShrink: 0 }} />
<div className={classes.userIdentity}>
<Text component="div" size="xs" truncate className={classes.userIdentityLine}>
<Group gap="xs" wrap="nowrap" w="100%">
<Avatar size={28} src={session.user?.profilePictureUrl} />
<Stack gap={2} className={classes.cellContent}>
<Text size="xs" truncate>
{session.user?.username ?? t("items.unknownUser")}
</Text>
<Text component="div" size="10px" c="dimmed" truncate className={classes.userIdentityLine}>
<Text size="xs" c="dimmed" truncate>
{session.sessionName}
</Text>
</div>
@@ -329,22 +339,27 @@ export default function MediaServerWidget({
{columnVisibility.status && (
<Table.Td>
{currentlyPlaying && (
<Stack gap={4} align="flex-start" className={classes.cellContent}>
<Stack gap={4} align="flex-start" w="100%" className={classes.cellContent}>
<Badge size="xs" variant="light" color={playbackStatusColorMap[status]}>
{t(`items.${status}` as never)}
</Badge>
{(location ?? bitrateLabel) && (
<Group gap={4} align="center" justify="space-between" wrap="nowrap" w="100%">
<Group gap={4} align="center" wrap="nowrap">
{location && (location === "lan" ? <IconWifi size={12} /> : <IconWorld size={12} />)}
{location &&
(location === "lan" ? (
<IconWifi size="var(--mantine-font-size-xs)" />
) : (
<IconWorld size="var(--mantine-font-size-xs)" />
))}
{location && (
<Text size="10px" c="dimmed" tt="uppercase">
<Text size="xs" c="dimmed" tt="uppercase">
{location}
</Text>
)}
</Group>
{bitrateLabel && (
<Text size="10px" c="dimmed" style={{ whiteSpace: "nowrap" }}>
<Text size="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
{bitrateLabel}
</Text>
)}
@@ -372,7 +387,7 @@ export default function MediaServerWidget({
}}
>
<Group gap={4} wrap="nowrap">
<IconVideo size={16} style={{ flexShrink: 0 }} />
<IconVideo size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
{(t as unknown as (key: string, params?: { count: number }) => string)("footer.streams", {
count: flatSessions.length,
@@ -424,14 +439,25 @@ function CurrentlyPlaying({ item }: { item: StreamSession }) {
: null;
const remainingMinutes =
positionMs !== null && durationMs !== null ? Math.max(0, Math.round((durationMs - positionMs) / 60_000)) : null;
const seasonEpisodeParams = getSeasonEpisodeParams(currentlyPlaying.seasonNumber, currentlyPlaying.episodeNumber);
const seasonEpisodeLabel = seasonEpisodeParams ? t("items.seasonEpisode", seasonEpisodeParams) : null;
return (
<Stack gap={4} style={{ minWidth: 0 }}>
<Stack gap={6} style={{ minWidth: 0 }}>
<Group gap="xs" align="center" wrap="nowrap" style={{ minWidth: 0 }}>
<Icon size={16} color={isPaused ? "var(--mantine-color-yellow-6)" : undefined} style={{ flexShrink: 0 }} />
<Icon
size="var(--mantine-font-size-xs)"
color={isPaused ? "var(--mantine-color-yellow-6)" : undefined}
style={{ flexShrink: 0 }}
/>
<Text size="xs" lineClamp={1} style={{ minWidth: 0 }}>
{currentlyPlaying.name}
</Text>
{seasonEpisodeLabel && (
<Text size="xs" c="dimmed" style={{ flexShrink: 0 }}>
{seasonEpisodeLabel}
</Text>
)}
{isPaused ? (
<Text size="xs" c="yellow" style={{ flexShrink: 0 }}>
{t("items.paused")}
@@ -153,7 +153,7 @@ export default function MediaTranscodingWidget({
return {
label: (
<Center style={{ gap: 4 }}>
<Icon size={12} />
<Icon size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
{footerLayout.showTabLabels ? (
<Text span size="xs">
{t(`tab.${value}`)}
@@ -259,7 +259,7 @@ export const resolveQueuePagination = (
const AdvancedPanel = ({ title, icon: Icon, children }: { title: string; icon: TablerIcon; children: ReactNode }) => (
<Paper withBorder radius="sm" p="xs" mih={280} style={{ display: "flex", flexDirection: "column" }}>
<Group gap="xs" mb="xs">
<Icon size={16} />
<Icon size="var(--mantine-font-size-md)" style={{ flexShrink: 0 }} />
<Text size="sm" fw={600}>
{title}
</Text>
@@ -27,7 +27,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
<Popover.Target>
<ActionIcon className={actionTargetClasses.root} variant="subtle" size="sm" aria-label={t("title")}>
<Indicator color={textColor(indicatorColor, colorScheme)} size={6} display="flex">
<IconHeartbeat size={16} />
<IconHeartbeat size="var(--mantine-font-size-md)" />
</Indicator>
</ActionIcon>
</Popover.Target>
@@ -38,7 +38,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
>
<Stack gap="sm" align="center">
<Group gap="xs">
<IconHeartbeat size={18} />
<IconHeartbeat size="var(--mantine-font-size-lg)" />
<Text size="sm">{t("title")}</Text>
</Group>
<Divider
@@ -59,11 +59,11 @@ export function QueuePanel(props: QueuePanelProps) {
<Group gap={4} wrap="nowrap">
{item.type === "transcode" ? (
<Tooltip label={t("table.transcode")}>
<IconTransform size={12} />
<IconTransform size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
</Tooltip>
) : (
<Tooltip label={t("table.healthCheck")}>
<IconHeartbeat size={12} />
<IconHeartbeat size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
</Tooltip>
)}
<Text lineClamp={1} size="xs">
@@ -94,7 +94,7 @@ function StatisticItem(props: StatisticItemProps) {
<Tooltip label={props.label}>
<Card p={0} radius={board.itemRadius} miw={48} flex={1}>
<Group justify="center" align="center" gap="xs" w="100%" wrap="nowrap">
<props.icon size={16} style={{ minWidth: 16 }} />
<props.icon size="var(--mantine-font-size-md)" style={{ flexShrink: 0 }} />
<Text size="md">{props.value}</Text>
</Group>
</Card>
@@ -66,11 +66,11 @@ export function WorkersPanel(props: WorkersPanelProps) {
<div>
{worker.jobType === "transcode" ? (
<Tooltip label={t("table.transcode")}>
<IconTransform size={14} />
<IconTransform size="var(--mantine-font-size-sm)" style={{ flexShrink: 0 }} />
</Tooltip>
) : (
<Tooltip label={t("table.healthCheck")}>
<IconHeartbeat size={14} />
<IconHeartbeat size="var(--mantine-font-size-sm)" style={{ flexShrink: 0 }} />
</Tooltip>
)}
</div>
@@ -76,7 +76,7 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
return (
<Stack>
<Alert icon={<IconInfoCircle size={16} />} color="blue" variant="light">
<Alert icon={<IconInfoCircle size="var(--mantine-font-size-md)" />} color="blue" variant="light">
{t("item.edit.app.propagationNotice")}
</Alert>
<AppForm
@@ -111,7 +111,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={section.status}
label={statusLabels[getBinaryStatusKey(section.status)]}
size={16}
size="var(--mantine-font-size-md)"
/>
<Text size="sm" fw={600}>
{getMatrixSectionLabel(section.key, t)}
@@ -210,7 +210,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.lan.status}
label={statusLabels[getBinaryStatusKey(summary.lan.status)]}
size={20}
size="var(--mantine-font-size-xl)"
/>
}
>
@@ -256,7 +256,15 @@ const getMatrixSectionLabel = (key: NetworkControllerMatrixSectionKey, t: Transl
return t(`widget.networkControllerSummary.card.${key}`);
};
const StatusIcon = ({ status, label, size }: { status?: "enabled" | "disabled"; label: string; size: number }) => {
const StatusIcon = ({
status,
label,
size,
}: {
status?: "enabled" | "disabled";
label: string;
size: number | string;
}) => {
const mantineTheme = useMantineTheme();
if (status === "enabled") {
return <IconCircleCheckFilled aria-label={label} size={size} color={mantineTheme.colors.green[6]} />;
+1 -1
View File
@@ -445,7 +445,7 @@ function CompactStatGrid({
style={{ "--stat-bg": severityToMantineBgColor(severity) } as CSSProperties}
>
{showIcon && (
<Icon className={classes.statIcon} size={18} stroke={1.5} color={severityToIconColor(severity)} />
<Icon className={classes.statIcon} size="var(--mantine-font-size-lg)" stroke={1.5} color={severityToIconColor(severity)} />
)}
<span className={classes.compactStatValue}>
<Text component="span" fw={700} size="inherit" c={severityToMantineColor(severity)}>
+9 -9
View File
@@ -303,20 +303,20 @@ export default function ReleasesWidget({ options, itemId, width, displayMode }:
{hasError ? (
<IconTriangleFilled
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-error"
size={10}
size="var(--mantine-font-size-xs)"
color="var(--mantine-color-red-filled)"
/>
) : repository.viewed ? (
<IconCheck
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-confirmed"
size={10}
size="var(--mantine-font-size-xs)"
color="green"
/>
) : (
(repository.isNewRelease || repository.isStaleRelease) && (
<IconCircleFilled
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-marker"
size={10}
size="var(--mantine-font-size-xs)"
color={
repository.isNewRelease
? "var(--mantine-color-primaryColor-filled)"
@@ -390,7 +390,7 @@ const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: Detai
"releases-repository-details-icon",
"releases-repository-details-icon-preRelease",
)}
size={13}
size="var(--mantine-font-size-sm)"
color={
repository.isPreRelease ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"
}
@@ -411,7 +411,7 @@ const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: Detai
"releases-repository-details-icon",
"releases-repository-details-icon-archived",
)}
size={13}
size="var(--mantine-font-size-sm)"
color={
repository.isArchived ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"
}
@@ -432,7 +432,7 @@ const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: Detai
"releases-repository-details-icon",
"releases-repository-details-icon-forked",
)}
size={13}
size="var(--mantine-font-size-sm)"
color={repository.isFork ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"}
/>
</Tooltip>
@@ -459,7 +459,7 @@ const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: Detai
"releases-repository-details-stats-icon",
"releases-repository-details-stats-stars-icon",
)}
size={12}
size="var(--mantine-font-size-xs)"
color={!repository.starsCount ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/>
<Text
@@ -501,7 +501,7 @@ const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: Detai
"releases-repository-details-stats-icon",
"releases-repository-details-stats-forks-icon",
)}
size={12}
size="var(--mantine-font-size-xs)"
color={!repository.forksCount ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/>
<Text
@@ -543,7 +543,7 @@ const DetailsDisplay = ({ repository, isExpanded, toggleExpandedDisplay }: Detai
"releases-repository-details-stats-icon",
"releases-repository-details-stats-issues-icon",
)}
size={12}
size="var(--mantine-font-size-xs)"
color={!repository.openIssues ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/>
<Text
+2 -2
View File
@@ -73,7 +73,7 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC
return (
<Flex direction="column" h="100%" p="xs">
{warning && (
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size={16} />} p="xs">
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size="var(--mantine-font-size-md)" />} p="xs">
<output>{warning}</output>
</Alert>
)}
@@ -103,7 +103,7 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC
return (
<ScrollArea className="scroll-area-w100" w="100%" h="100%" p={isTiny ? 4 : "xs"}>
{warning && (
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size={16} />} p="xs" mb="xs">
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size="var(--mantine-font-size-md)" />} p="xs" mb="xs">
<output>{warning}</output>
</Alert>
)}
+7 -2
View File
@@ -78,14 +78,19 @@ const TimetableWidgetInner = ({ station, baseUrl, itemId, displayMode, width, he
)}
{compactStaleWarning && displayMode === "compact" && (
<Group gap={2} wrap="nowrap">
<IconAlertTriangle aria-hidden size={12} color="var(--mantine-color-orange-light-color)" />
<IconAlertTriangle
aria-hidden
size="var(--mantine-font-size-xs)"
color="var(--mantine-color-orange-light-color)"
style={{ flexShrink: 0 }}
/>
<Text component="output" size="xs" c="var(--mantine-color-text)" style={{ whiteSpace: "nowrap" }}>
{compactStaleWarning}
</Text>
</Group>
)}
{staleWarning && displayMode === "advanced" && (
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size={16} />} p="xs">
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size="var(--mantine-font-size-md)" />} p="xs">
<output>{staleWarning}</output>
</Alert>
)}
@@ -18,14 +18,14 @@ export function StatsBar({
return (
<SimpleGrid cols={cols} spacing="xs">
<StatCard icon={<IconVideo size={16} />} label={t("stats.activeStreams")} value={stats.activeStreams} />
<StatCard icon={<IconUsers size={16} />} label={t("stats.totalUsers")} value={stats.totalUsers} />
<StatCard icon={<IconVideo size="var(--mantine-font-size-md)" />} label={t("stats.activeStreams")} value={stats.activeStreams} />
<StatCard icon={<IconUsers size="var(--mantine-font-size-md)" />} label={t("stats.totalUsers")} value={stats.totalUsers} />
<StatCard
icon={<IconDevices size={16} />}
icon={<IconDevices size="var(--mantine-font-size-md)" />}
label={t("stats.transcodes")}
value={`${summary.transcodes}/${summary.total}`}
/>
<StatCard icon={<IconNetwork size={16} />} label={t("stats.bandwidth")} value={summary.totalBitrate} />
<StatCard icon={<IconNetwork size="var(--mantine-font-size-md)" />} label={t("stats.bandwidth")} value={summary.totalBitrate} />
</SimpleGrid>
);
}
@@ -88,9 +88,9 @@ function StreamCard({
<Group justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
{stream.state === "playing" ? (
<IconPlayerPlay size={14} color="var(--mantine-color-green-6)" />
<IconPlayerPlay size="var(--mantine-font-size-sm)" color="var(--mantine-color-green-6)" />
) : (
<IconPlayerPause size={14} color="var(--mantine-color-yellow-6)" />
<IconPlayerPause size="var(--mantine-font-size-sm)" color="var(--mantine-color-yellow-6)" />
)}
<Text size="sm" fw={600} lineClamp={1}>
{stream.username}
@@ -34,7 +34,7 @@ export function ViolationsList({
<Group justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
<IconAlertTriangle
size={14}
size="var(--mantine-font-size-sm)"
color={
violation.severity === "high"
? "var(--mantine-color-red-6)"
+4 -4
View File
@@ -308,7 +308,7 @@ function ProtocolCard({ protocol, data }: { protocol: ProtocolKey; data: Traefik
{rows.map(({ key, icon: Icon, summary }) => (
<div key={key} className={classes.protocolRow}>
<Group gap={4} wrap="nowrap" className={classes.protocolLabel}>
<Icon size={14} />
<Icon size="var(--mantine-font-size-sm)" />
<Text>{t(`resource.${key}`)}</Text>
</Group>
<StatusCount summary={summary} />
@@ -354,7 +354,7 @@ function HealthBadge({ errors, warnings }: { errors: number; warnings: number })
if (errors > 0) {
return (
<Badge color="red" variant="light" leftSection={<IconAlertTriangle size={12} />} radius="sm">
<Badge color="red" variant="light" leftSection={<IconAlertTriangle size="var(--mantine-font-size-xs)" />} radius="sm">
{t("status.errors", { count: errors })}
</Badge>
);
@@ -362,14 +362,14 @@ function HealthBadge({ errors, warnings }: { errors: number; warnings: number })
if (warnings > 0) {
return (
<Badge color="yellow" variant="light" leftSection={<IconAlertTriangle size={12} />} radius="sm">
<Badge color="yellow" variant="light" leftSection={<IconAlertTriangle size="var(--mantine-font-size-xs)" />} radius="sm">
{t("status.warnings", { count: warnings })}
</Badge>
);
}
return (
<Badge color="green" variant="light" leftSection={<IconCircleCheck size={12} />} radius="sm">
<Badge color="green" variant="light" leftSection={<IconCircleCheck size="var(--mantine-font-size-xs)" />} radius="sm">
{t("status.healthy")}
</Badge>
);
+3 -3
View File
@@ -159,7 +159,7 @@ function VpnInfoColumn({
</Text>
{!dense && (city || country) && (
<Group gap={4} justify="flex-start" wrap="nowrap">
<IconMapPin aria-hidden size={10} />
<IconMapPin aria-hidden size="var(--mantine-font-size-xs)" />
<Text fs="italic" fw={500} size="sm" lh={1.2} lineClamp={1}>
{[city, country].filter(Boolean).join(", ")}
</Text>
@@ -176,7 +176,7 @@ function VpnProviderDetails({ provider, protocol }: { provider: string; protocol
<Text fz="xs" lh={1.2} tt="capitalize" lineClamp={1}>
{provider}
</Text>
<IconArrowsExchange stroke={1} size={12} style={{ flexShrink: 0 }} />
<IconArrowsExchange stroke={1} size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
<Text fz="xs" lh={1.2} tt="capitalize" lineClamp={1}>
{protocol}
</Text>
@@ -196,7 +196,7 @@ function DnsStatusBadge({ status }: { status: string }) {
color={getStatusColor(status)}
size="xs"
radius="xl"
leftSection={<StatusIcon aria-hidden size={9} />}
leftSection={<StatusIcon aria-hidden size="var(--mantine-font-size-xs)" />}
>
DNS
<VisuallyHidden> {statusLabel}</VisuallyHidden>
+14 -14
View File
@@ -116,11 +116,11 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => {
<Group gap="lg" wrap="wrap">
<Group gap={5} wrap="nowrap">
<IconMapPin size={16} aria-hidden />
<IconMapPin size="var(--mantine-font-size-md)" aria-hidden />
<Text size="sm">{options.location.name}</Text>
</Group>
<Group gap={5} wrap="nowrap">
<IconWind size={16} aria-hidden />
<IconWind size="var(--mantine-font-size-md)" aria-hidden />
<Text size="sm">
{t("currentWindSpeed", {
currentWindSpeed: getPreferredWindSpeed(weather.current.windspeed, options.useImperialSpeed),
@@ -196,7 +196,7 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => {
</Table.Td>
<Table.Td>
<Group gap={4} wrap="nowrap">
<IconArrowUpRight size={15} aria-hidden />
<IconArrowUpRight size="var(--mantine-font-size-sm)" aria-hidden />
<Text size="sm">
{getPreferredUnit(
dayWeather.maxTemp,
@@ -207,7 +207,7 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => {
<Text size="sm" c="dimmed">
/
</Text>
<IconArrowDownRight size={15} aria-hidden />
<IconArrowDownRight size="var(--mantine-font-size-sm)" aria-hidden />
<Text size="sm">
{getPreferredUnit(
dayWeather.minTemp,
@@ -219,13 +219,13 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => {
</Table.Td>
<Table.Td>
<Group gap={5} wrap="nowrap">
<IconDroplets size={15} aria-hidden />
<IconDroplets size="var(--mantine-font-size-sm)" aria-hidden />
<Text size="sm">{dayWeather.humidity === undefined ? "?" : `${dayWeather.humidity}%`}</Text>
</Group>
</Table.Td>
<Table.Td>
<Group gap={5} wrap="nowrap">
<IconWind size={15} aria-hidden />
<IconWind size="var(--mantine-font-size-sm)" aria-hidden />
<Text size="sm">
{getPreferredWindSpeed(dayWeather.maxWindSpeed, options.useImperialSpeed)} /{" "}
{getPreferredWindSpeed(dayWeather.maxWindGusts, options.useImperialSpeed)}
@@ -235,14 +235,14 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => {
<Table.Td>
<Group gap="xs" wrap="nowrap">
<Group gap={3} wrap="nowrap">
<IconSunrise size={15} aria-hidden />
<IconSunrise size="var(--mantine-font-size-sm)" aria-hidden />
<Text size="sm">{getPreferredTime(dayWeather.sunrise, locale)}</Text>
</Group>
<Text size="sm" c="dimmed">
/
</Text>
<Group gap={3} wrap="nowrap">
<IconSunset size={15} aria-hidden />
<IconSunset size="var(--mantine-font-size-sm)" aria-hidden />
<Text size="sm">{getPreferredTime(dayWeather.sunset, locale)}</Text>
</Group>
</Group>
@@ -285,7 +285,7 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) =>
<Stack gap="xs" align="center">
{options.showCurrentWindSpeed && (
<Group className="weather-current-wind-speed-group" wrap="nowrap" gap="xs">
<IconWind size={16} />
<IconWind size="var(--mantine-font-size-md)" />
<Text fz={16}>
{t("currentWindSpeed", {
currentWindSpeed: (options.useImperialSpeed
@@ -301,13 +301,13 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) =>
)}
{weather.daily[0]?.humidity !== undefined && (
<Group className="weather-humidity-group" wrap="nowrap" gap="xs">
<IconDroplets size={16} />
<IconDroplets size="var(--mantine-font-size-md)" />
<Text fz={16}>{t("dailyForecast.humidity", { humidity: weather.daily[0].humidity })}</Text>
</Group>
)}
<Group className="weather-max-min-temp-group" wrap="nowrap" gap="sm">
<Group gap="xs" wrap="nowrap">
<IconArrowUpRight size={16} />
<IconArrowUpRight size="var(--mantine-font-size-md)" />
<Text fz={16}>
{getPreferredUnit(
weather.daily[0]?.maxTemp,
@@ -317,7 +317,7 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) =>
</Text>
</Group>
<Group gap="xs" wrap="nowrap">
<IconArrowDownRight size={16} />
<IconArrowDownRight size="var(--mantine-font-size-md)" />
<Text fz={16}>
{getPreferredUnit(
weather.daily[0]?.minTemp,
@@ -331,7 +331,7 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) =>
{options.showCity && (
<>
<Group className="weather-city-group" wrap="nowrap" gap="xs">
<IconMapPin size={16} />
<IconMapPin size="var(--mantine-font-size-md)" />
<Text fz={16} style={{ whiteSpace: "nowrap" }}>
{options.location.name}
</Text>
@@ -350,7 +350,7 @@ const WeeklyForecast = ({ options, weather, isEditMode, maxDays }: CompactWeathe
<Group className="weather-forecast-city-temp-group" wrap="nowrap" gap="md">
{options.showCity && (
<Group gap="xs" wrap="nowrap">
<IconMapPin size={16} />
<IconMapPin size="var(--mantine-font-size-md)" />
<Text fz={16} style={{ whiteSpace: "nowrap" }}>
{options.location.name}
</Text>
+7 -7
View File
@@ -95,15 +95,15 @@ export const WeatherDescription = ({
<Text fz="24px">{formatWeatherDate(time, locale, dateFormat)}</Text>
<Text fz="16px">{t(`kind.${name}`)}</Text>
<List>
<List.Item icon={<IconTemperaturePlus size={15} />}>{`${tCommon("information.max")}: ${maxTemp}`}</List.Item>
<List.Item icon={<IconTemperatureMinus size={15} />}>{`${tCommon("information.min")}: ${minTemp}`}</List.Item>
<List.Item icon={<IconSun size={15} />}>{`${t("dailyForecast.sunrise")}: ${sunrise}`}</List.Item>
<List.Item icon={<IconMoon size={15} />}>{`${t("dailyForecast.sunset")}: ${sunset}`}</List.Item>
<List.Item icon={<IconTemperaturePlus size="var(--mantine-font-size-sm)" />}>{`${tCommon("information.max")}: ${maxTemp}`}</List.Item>
<List.Item icon={<IconTemperatureMinus size="var(--mantine-font-size-sm)" />}>{`${tCommon("information.min")}: ${minTemp}`}</List.Item>
<List.Item icon={<IconSun size="var(--mantine-font-size-sm)" />}>{`${t("dailyForecast.sunrise")}: ${sunrise}`}</List.Item>
<List.Item icon={<IconMoon size="var(--mantine-font-size-sm)" />}>{`${t("dailyForecast.sunset")}: ${sunset}`}</List.Item>
{humidity !== undefined && (
<List.Item icon={<IconDroplets size={15} />}>{t("dailyForecast.humidity", { humidity })}</List.Item>
<List.Item icon={<IconDroplets size="var(--mantine-font-size-sm)" />}>{t("dailyForecast.humidity", { humidity })}</List.Item>
)}
{maxWindSpeed !== undefined && (
<List.Item icon={<IconWind size={15} />}>
<List.Item icon={<IconWind size="var(--mantine-font-size-sm)" />}>
{t("dailyForecast.maxWindSpeed", {
maxWindSpeed: (useImperialSpeed ? metricToImperial(maxWindSpeed) : maxWindSpeed).toFixed(1),
unit: useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"),
@@ -111,7 +111,7 @@ export const WeatherDescription = ({
</List.Item>
)}
{maxWindGusts !== undefined && (
<List.Item icon={<IconWind size={15} />}>
<List.Item icon={<IconWind size="var(--mantine-font-size-sm)" />}>
{t("dailyForecast.maxWindGusts", {
maxWindGusts: (useImperialSpeed ? metricToImperial(maxWindGusts) : maxWindGusts).toFixed(1),
unit: useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"),
@@ -88,7 +88,7 @@ export const WidgetIntegrationSelect = ({
return (
<Combobox.Option value={item.id} key={item.id} active={multiSelectValues.includes(item.id)}>
<Group gap="sm" align="center">
{multiSelectValues.includes(item.id) ? <CheckIcon size={12} /> : null}
{multiSelectValues.includes(item.id) ? <CheckIcon size="var(--mantine-font-size-xs)" /> : null}
<Group gap={7} align="center">
<Avatar src={getIconUrl(item.kind)} size="sm" />
<Stack gap={0}>