diff --git a/packages/integrations/src/emby/emby-integration.ts b/packages/integrations/src/emby/emby-integration.ts index 23e5bc2a9..8fec4f4b1 100644 --- a/packages/integrations/src/emby/emby-integration.ts +++ b/packages/integrations/src/emby/emby-integration.ts @@ -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: { diff --git a/packages/integrations/src/interfaces/media-server/media-server-types.ts b/packages/integrations/src/interfaces/media-server/media-server-types.ts index c9a287155..a112c95ff 100644 --- a/packages/integrations/src/interfaces/media-server/media-server-types.ts +++ b/packages/integrations/src/interfaces/media-server/media-server-types.ts @@ -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: { diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index a7be2d409..7450e6226 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -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: { diff --git a/packages/integrations/src/mock/data/media-server.ts b/packages/integrations/src/mock/data/media-server.ts index 3acae2016..278bc379b 100644 --- a/packages/integrations/src/mock/data/media-server.ts +++ b/packages/integrations/src/mock/data/media-server.ts @@ -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 }, diff --git a/packages/integrations/src/plex/interface.ts b/packages/integrations/src/plex/interface.ts index 5c7c01e6e..21c517361 100644 --- a/packages/integrations/src/plex/interface.ts +++ b/packages/integrations/src/plex/interface.ts @@ -80,8 +80,9 @@ interface Session { $: { grandparentTitle?: string; parentTitle?: string; + parentIndex?: string; title?: string; - index?: number; + index?: string; type: string; live?: string; viewOffset?: string; diff --git a/packages/integrations/src/plex/plex-integration.ts b/packages/integrations/src/plex/plex-integration.ts index cd4e9d6e9..ec46a466c 100644 --- a/packages/integrations/src/plex/plex-integration.ts +++ b/packages/integrations/src/plex/plex-integration.ts @@ -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, diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index bfcaa0a40..91e4178c5 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -4130,6 +4130,7 @@ "transcoding": "Transcoding", "paused": "Paused", "remaining": "{minutes} min left", + "seasonEpisode": "S{season} E{episode}", "metadata": { "title": "Stats for nerds", "video": { diff --git a/packages/widgets/src/_inputs/json-path-tree-picker.tsx b/packages/widgets/src/_inputs/json-path-tree-picker.tsx index 621b30cc4..d449c1b0a 100644 --- a/packages/widgets/src/_inputs/json-path-tree-picker.tsx +++ b/packages/widgets/src/_inputs/json-path-tree-picker.tsx @@ -314,7 +314,7 @@ export function JsonPathTreePicker({ {hasData && ( - }> + }> {loadedHint} @@ -353,7 +353,7 @@ function TreeNodeOption({ node, selected, isSearching }: { node: FlatNode; selec {node.hasChildren && ( )} - + {issues[0] && ( - }> + }> {labels("needsAttention", { message: issues[0].message })} )} diff --git a/packages/widgets/src/_inputs/widget-custom-widget-select-input.tsx b/packages/widgets/src/_inputs/widget-custom-widget-select-input.tsx index 24276feea..ad06d8c59 100644 --- a/packages/widgets/src/_inputs/widget-custom-widget-select-input.tsx +++ b/packages/widgets/src/_inputs/widget-custom-widget-select-input.tsx @@ -115,7 +115,7 @@ export const WidgetCustomWidgetSelectInput = ({ > - + @@ -152,7 +152,7 @@ export const WidgetCustomWidgetSelectInput = ({ } + icon={} title={labels("migrationRequired")} > {labels("migrationDescription")} diff --git a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx index 27ea719cc..4baa16864 100644 --- a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx +++ b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx @@ -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} /> )} diff --git a/packages/widgets/src/_inputs/widget-location-input.tsx b/packages/widgets/src/_inputs/widget-location-input.tsx index 91c7d0dd1..aba83394c 100644 --- a/packages/widgets/src/_inputs/widget-location-input.tsx +++ b/packages/widgets/src/_inputs/widget-location-input.tsx @@ -97,7 +97,7 @@ export const WidgetLocationInput = ({ property, kind, options }: CommonWidgetInp disabled={!selectionEnabled} onClick={onSearch} variant="light" - leftSection={} + leftSection={} > {tLocation("search")} @@ -257,7 +257,7 @@ const LocationSelectTableRow = ({ city, onLocationSelect, closeModal }: Location })} > - + diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index d78d11660..15ff18f3a 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -229,19 +229,19 @@ export const WidgetMultiReleasesRepositoriesInput = ({ }) } variant="light" - leftSection={} + leftSection={} size="xs" > {tRepository("edit.label")} onRepositoryRemove(index)}> - + {Object.keys(form.errors).filter((key) => key.startsWith(`options.${property}.${index}.`)).length > 0 && ( - + {tRepository("invalid")} @@ -371,7 +371,7 @@ const ProviderTokenInput = ({ }} style={{ flex: 1 }} size="xs" - leftSection={} + leftSection={} /> {editing && value.trim() && ( @@ -205,7 +205,7 @@ export default function DnsHoleControlsWidget({ radius={board.itemRadius} flex={1} > - + @@ -223,7 +223,7 @@ export default function DnsHoleControlsWidget({ radius={board.itemRadius} flex={1} > - + @@ -378,7 +378,7 @@ const ControlsCard: React.FC = ({ color="green" variant="light" > - + ) : ( = ({ color="red" variant="light" > - + )} = ({ display={isInteractPermitted ? undefined : "none"} disabled={!controlEnabled || !isEnabled} > - + )} @@ -436,7 +436,7 @@ const ControlsCard: React.FC = ({ ) } @@ -474,7 +474,7 @@ const ControlsCard: React.FC = ({ open(); }} > - + )} diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index 0b0ca9239..45322531d 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -430,7 +430,7 @@ export default function DockerWidget({ wrap="nowrap" > - + {t("table.footer", { count: containers.length.toString() })} @@ -457,7 +457,7 @@ export default function DockerWidget({ onClick={() => refreshInventory.mutate()} aria-label={t("table.refresh.lastUpdated", { when: relativeTime })} > - + @@ -491,7 +491,7 @@ function ContainerMenuButton({ aria-label={t("title")} onClick={(event) => event.stopPropagation()} > - + @@ -525,7 +525,7 @@ function ContainerActionItems({ {container.name} } + leftSection={} disabled={!handlers.canUse(container, "logs")} onClick={() => { handlers.onOpenLogs(container); @@ -537,7 +537,7 @@ function ContainerActionItems({ } + leftSection={} disabled={!handlers.canUse(container, "lifecycle")} onClick={() => invokeAction(stateAction)} > @@ -545,7 +545,7 @@ function ContainerActionItems({ } + leftSection={} disabled={!handlers.canUse(container, "lifecycle")} onClick={() => invokeAction("restart")} > @@ -553,7 +553,7 @@ function ContainerActionItems({ } + leftSection={} disabled={!handlers.canUse(container, "remove")} onClick={() => invokeAction("remove")} > @@ -561,7 +561,7 @@ function ContainerActionItems({ } + leftSection={} onClick={() => { handlers.onAddToHomarr(container); onClose(); diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 04f982681..a50685460 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -279,7 +279,7 @@ function SpeedCell({ const { Icon, color } = speedColumnConfig[direction]; return ( - + {formatByteRate(speed)} @@ -914,7 +914,7 @@ function ExpandedRow({ item, collapse }: { item: ExtendedDownloadClientItem; col } + icon={} color="blue" /> )} @@ -922,7 +922,7 @@ function ExpandedRow({ item, collapse }: { item: ExtendedDownloadClientItem; col } + icon={} color="green" /> )} @@ -1005,7 +1005,7 @@ function GlobalStatsBar({ - + {`${formatBytes(queueStats.completedSize)} / ${formatBytes(queueStats.totalSize)}`} @@ -1013,14 +1013,14 @@ function GlobalStatsBar({ - + {formatByteRate(totalSpeed)} {totalUpSpeed > 0 && ( - + {formatByteRate(totalUpSpeed)} @@ -1093,7 +1093,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos {truncateText(item.name, 40)} } + leftSection={} onClick={() => { void navigator.clipboard.writeText(item.name).catch(() => {}); onClose(); @@ -1104,7 +1104,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos {item.id && ( } + leftSection={} onClick={() => { void navigator.clipboard.writeText(item.id).catch(() => {}); onClose(); @@ -1118,7 +1118,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos <> } + leftSection={} onClick={() => { pauseResumeInvoke?.(); onClose(); @@ -1130,7 +1130,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos {!showDeleteConfirm && ( } + leftSection={} onClick={(e) => { e.stopPropagation(); openDelete(); @@ -1144,7 +1144,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos <> } + leftSection={} onClick={() => { item.actions?.delete({ fromDisk: false }); onClose(); @@ -1154,7 +1154,7 @@ function RowContextMenu({ state, onClose, t }: { state: ContextMenuState; onClos } + leftSection={} onClick={() => { item.actions?.delete({ fromDisk: true }); onClose(); @@ -1321,14 +1321,14 @@ function WidgetFooter({ } }} > - + {toggleStats ? ( - + ) : null} @@ -1356,7 +1356,7 @@ function WidgetFooter({ disabled={integrationsStatuses.paused.length === 0} onClick={() => resumeQueue({ integrationIds: integrationsStatuses.paused })} > - + )} @@ -1364,12 +1364,12 @@ function WidgetFooter({ {!showStats && ( - + {formatByteRate(totalSpeed)} {totalUpSpeed > 0 && ( - + {formatByteRate(totalUpSpeed)} )} @@ -1386,7 +1386,7 @@ function WidgetFooter({ disabled={integrationsStatuses.active.length === 0} onClick={() => pauseQueue({ integrationIds: integrationsStatuses.active })} > - + )} diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 62a553c4f..b848ca53f 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -386,7 +386,7 @@ const InterfacesPanel = ({ variant={isAdvanced ? "contained" : "default"} > - }> + }> {label} {hasError && ( @@ -410,7 +410,7 @@ const InterfacesPanel = ({ @@ -419,7 +419,7 @@ const InterfacesPanel = ({ diff --git a/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx b/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx index 5c6aa9dc4..0d3ab0b7e 100644 --- a/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx +++ b/packages/widgets/src/health-monitoring/cluster/resource-accordion-item.tsx @@ -26,7 +26,7 @@ export const ResourceAccordionItem = ({ }: PropsWithChildren) => { return ( - }> + }> {title} diff --git a/packages/widgets/src/health-monitoring/cluster/resource-popover.tsx b/packages/widgets/src/health-monitoring/cluster/resource-popover.tsx index 606596342..456af9a57 100644 --- a/packages/widgets/src/health-monitoring/cluster/resource-popover.tsx +++ b/packages/widgets/src/health-monitoring/cluster/resource-popover.tsx @@ -97,20 +97,20 @@ const ComputeResourceDetails = ({ item }: { item: ComputeResource }) => { const storage = formatBytesPair(item.storage.used, item.storage.total); return ( - }> + }> {t("cpu")} - {item.cpu.cores} - }> + }> {t("memory")} - {memory.used} / {memory.available} - }> + }> {t("storage")} - {storage.used} / {storage.available} - }> + }> {t("uptime")} - {dayjs(dayjs().add(-item.uptime, "seconds")).fromNow(true)} {item.haState && ( - }> + }> {t("haState")} - {capitalize(item.haState)} )} @@ -152,15 +152,15 @@ const DiskStats = ({ item }: { item: ComputeResource }) => { return null; } return ( - }> + }> {formatBytes(item.storage.write)} - + {formatBytes(item.storage.read)} - + @@ -172,15 +172,15 @@ const NetStats = ({ item }: { item: ComputeResource }) => { return null; } return ( - }> + }> {formatBytes(item.network.in)} - + {formatBytes(item.network.out)} - + diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index b105603f3..a7f77d073 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -178,7 +178,7 @@ function Carousel({ size={40} onClick={() => move(-1)} > - + )} setPaused((value) => !value)} > - {paused ? : } + {paused ? : } {advanced && ( move(1)} > - + )} @@ -214,7 +214,7 @@ function Carousel({ )} - + {new Date(currentAsset.fileModifiedAt).toLocaleDateString(locale)} diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx index 70663d037..caefd652b 100644 --- a/packages/widgets/src/immich/server-stats/component.tsx +++ b/packages/widgets/src/immich/server-stats/component.tsx @@ -59,7 +59,7 @@ export default function ImmichServerStatsWidget({ {statVisibility.showUsers && ( } + icon={} label={t("widget.immich-serverStats.users")} value={stats.userCount.toLocaleString(locale)} dense={statsLayout.dense} @@ -67,7 +67,7 @@ export default function ImmichServerStatsWidget({ )} {statVisibility.showPhotos && ( } + icon={} label={t("widget.immich-serverStats.photos")} value={stats.photoCount.toLocaleString(locale)} dense={statsLayout.dense} @@ -75,7 +75,7 @@ export default function ImmichServerStatsWidget({ )} {statVisibility.showVideos && ( } + icon={} label={t("widget.immich-serverStats.videos")} value={stats.videoCount.toLocaleString(locale)} dense={statsLayout.dense} @@ -83,7 +83,7 @@ export default function ImmichServerStatsWidget({ )} {statVisibility.showStorage && ( } + icon={} label={t("widget.immich-serverStats.storage")} value={formatBytes(stats.totalLibraryUsageInBytes)} dense={statsLayout.dense} diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index 377597341..a78e3f188 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -133,7 +133,7 @@ export default function IndexerManagerWidget({ }} aria-label={t("widget.indexerManager.testAll")} > - + diff --git a/packages/widgets/src/media-missing/component.tsx b/packages/widgets/src/media-missing/component.tsx index 3d4ec2503..7be733ea3 100644 --- a/packages/widgets/src/media-missing/component.tsx +++ b/packages/widgets/src/media-missing/component.tsx @@ -141,7 +141,7 @@ export default function MediaMissingWidget({ {showMissing && ( - + {tabLabel(t("tab.missing"), missing.length, missingCount)} @@ -152,7 +152,7 @@ export default function MediaMissingWidget({ {showQueued && ( - + {tabLabel(t("tab.queued"), queued.length, queuedCount)} @@ -181,12 +181,12 @@ export default function MediaMissingWidget({ {showMissing && ( - }> + }> {tabLabel(t("tab.missing"), missing.length, missingCount)} )} {showQueued && ( - }> + }> {tabLabel(t("tab.queued"), queued.length, queuedCount)} )} diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index f574a48bb..0d639a0c6 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -273,7 +273,7 @@ const InfoDivider = () => ( const Info = ({ icon: Icon, label }: IconAndLabelProps) => { return ( - {Icon && } + {Icon && } {label} diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 92beaf0d0..733b0fbeb 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -94,7 +94,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] aria-label={t("action.search.label")} onClick={() => openMediaRequestSearch({ integrationIds })} > - + ); @@ -281,7 +281,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible handleDecision("approve"); }} > - + @@ -296,7 +296,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible handleDecision("decline"); }} > - + diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index ea96b099f..02d2c93e4 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -132,7 +132,7 @@ export default function MediaServerWidget({ - + {stat.number} @@ -226,7 +226,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] aria-label={t("action.search.label")} onClick={() => openMediaRequestSearch({ integrationIds })} > - + ); diff --git a/packages/widgets/src/media-server/component.module.css b/packages/widgets/src/media-server/component.module.css index 37df5cc3f..b5ca6ce77 100644 --- a/packages/widgets/src/media-server/component.module.css +++ b/packages/widgets/src/media-server/component.module.css @@ -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 { diff --git a/packages/widgets/src/media-server/component.spec.tsx b/packages/widgets/src/media-server/component.spec.tsx index 2c79d29cf..0f23b232c 100644 --- a/packages/widgets/src/media-server/component.spec.tsx +++ b/packages/widgets/src/media-server/component.spec.tsx @@ -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(); + }); +}); diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index ad6f9cd4d..e69b9564b 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -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({ {label} - + ) : ( @@ -225,7 +235,7 @@ export default function MediaServerWidget({ onChange={(event) => setSearch(event.currentTarget.value)} placeholder={tGlobal("search.placeholder")} aria-label={tGlobal("search.placeholder")} - leftSection={} + leftSection={} /> @@ -304,13 +314,13 @@ export default function MediaServerWidget({ > {columnVisibility.user && ( - - -
- + + + + {session.user?.username ?? t("items.unknownUser")} - + {session.sessionName}
@@ -329,22 +339,27 @@ export default function MediaServerWidget({ {columnVisibility.status && ( {currentlyPlaying && ( - + {t(`items.${status}` as never)} {(location ?? bitrateLabel) && ( - {location && (location === "lan" ? : )} + {location && + (location === "lan" ? ( + + ) : ( + + ))} {location && ( - + {location} )} {bitrateLabel && ( - + {bitrateLabel} )} @@ -372,7 +387,7 @@ export default function MediaServerWidget({ }} > - + {(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 ( - + - + {currentlyPlaying.name} + {seasonEpisodeLabel && ( + + {seasonEpisodeLabel} + + )} {isPaused ? ( {t("items.paused")} diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index a651bbf73..f93dbd53c 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -153,7 +153,7 @@ export default function MediaTranscodingWidget({ return { label: (
- + {footerLayout.showTabLabels ? ( {t(`tab.${value}`)} @@ -259,7 +259,7 @@ export const resolveQueuePagination = ( const AdvancedPanel = ({ title, icon: Icon, children }: { title: string; icon: TablerIcon; children: ReactNode }) => ( - + {title} diff --git a/packages/widgets/src/media-transcoding/health-check-status.tsx b/packages/widgets/src/media-transcoding/health-check-status.tsx index 17c575899..5d0bf87d1 100644 --- a/packages/widgets/src/media-transcoding/health-check-status.tsx +++ b/packages/widgets/src/media-transcoding/health-check-status.tsx @@ -27,7 +27,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) { - + @@ -38,7 +38,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) { > - + {t("title")} {item.type === "transcode" ? ( - + ) : ( - + )} diff --git a/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx b/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx index e31f51de1..f1aa49623 100644 --- a/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx +++ b/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx @@ -94,7 +94,7 @@ function StatisticItem(props: StatisticItemProps) { - + {props.value} diff --git a/packages/widgets/src/media-transcoding/panels/workers.panel.tsx b/packages/widgets/src/media-transcoding/panels/workers.panel.tsx index 3dbf968b7..447a7c652 100644 --- a/packages/widgets/src/media-transcoding/panels/workers.panel.tsx +++ b/packages/widgets/src/media-transcoding/panels/workers.panel.tsx @@ -66,11 +66,11 @@ export function WorkersPanel(props: WorkersPanelProps) {
{worker.jobType === "transcode" ? ( - + ) : ( - + )}
diff --git a/packages/widgets/src/modals/embedded-app-edit-form.tsx b/packages/widgets/src/modals/embedded-app-edit-form.tsx index 81b336869..d0a1d86f7 100644 --- a/packages/widgets/src/modals/embedded-app-edit-form.tsx +++ b/packages/widgets/src/modals/embedded-app-edit-form.tsx @@ -76,7 +76,7 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro return ( - } color="blue" variant="light"> + } color="blue" variant="light"> {t("item.edit.app.propagationNotice")} {getMatrixSectionLabel(section.key, t)} @@ -210,7 +210,7 @@ export default function NetworkControllerSummaryWidget({ } > @@ -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 ; diff --git a/packages/widgets/src/patchmon/component.tsx b/packages/widgets/src/patchmon/component.tsx index 401bd1803..6cd8c2927 100644 --- a/packages/widgets/src/patchmon/component.tsx +++ b/packages/widgets/src/patchmon/component.tsx @@ -445,7 +445,7 @@ function CompactStatGrid({ style={{ "--stat-bg": severityToMantineBgColor(severity) } as CSSProperties} > {showIcon && ( - + )} diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx index 9b041e5e1..d1bcb7245 100644 --- a/packages/widgets/src/releases/component.tsx +++ b/packages/widgets/src/releases/component.tsx @@ -303,20 +303,20 @@ export default function ReleasesWidget({ options, itemId, width, displayMode }: {hasError ? ( ) : repository.viewed ? ( ) : ( (repository.isNewRelease || repository.isStaleRelease) && (
@@ -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)"} /> {warning && ( - } p="xs"> + } p="xs"> {warning} )} @@ -103,7 +103,7 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC return ( {warning && ( - } p="xs" mb="xs"> + } p="xs" mb="xs"> {warning} )} diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index 29602c9a8..b34534e96 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -78,14 +78,19 @@ const TimetableWidgetInner = ({ station, baseUrl, itemId, displayMode, width, he )} {compactStaleWarning && displayMode === "compact" && ( - + {compactStaleWarning} )} {staleWarning && displayMode === "advanced" && ( - } p="xs"> + } p="xs"> {staleWarning} )} diff --git a/packages/widgets/src/tracearr/stats-section.tsx b/packages/widgets/src/tracearr/stats-section.tsx index 60b4ef125..2098556f4 100644 --- a/packages/widgets/src/tracearr/stats-section.tsx +++ b/packages/widgets/src/tracearr/stats-section.tsx @@ -18,14 +18,14 @@ export function StatsBar({ return ( - } label={t("stats.activeStreams")} value={stats.activeStreams} /> - } label={t("stats.totalUsers")} value={stats.totalUsers} /> + } label={t("stats.activeStreams")} value={stats.activeStreams} /> + } label={t("stats.totalUsers")} value={stats.totalUsers} /> } + icon={} label={t("stats.transcodes")} value={`${summary.transcodes}/${summary.total}`} /> - } label={t("stats.bandwidth")} value={summary.totalBitrate} /> + } label={t("stats.bandwidth")} value={summary.totalBitrate} /> ); } diff --git a/packages/widgets/src/tracearr/streams-section.tsx b/packages/widgets/src/tracearr/streams-section.tsx index 119584832..bc3157c84 100644 --- a/packages/widgets/src/tracearr/streams-section.tsx +++ b/packages/widgets/src/tracearr/streams-section.tsx @@ -88,9 +88,9 @@ function StreamCard({ {stream.state === "playing" ? ( - + ) : ( - + )} {stream.username} diff --git a/packages/widgets/src/tracearr/violations-section.tsx b/packages/widgets/src/tracearr/violations-section.tsx index 9d10240ac..285c0cc50 100644 --- a/packages/widgets/src/tracearr/violations-section.tsx +++ b/packages/widgets/src/tracearr/violations-section.tsx @@ -34,7 +34,7 @@ export function ViolationsList({ (
- + {t(`resource.${key}`)} @@ -354,7 +354,7 @@ function HealthBadge({ errors, warnings }: { errors: number; warnings: number }) if (errors > 0) { return ( - } radius="sm"> + } radius="sm"> {t("status.errors", { count: errors })} ); @@ -362,14 +362,14 @@ function HealthBadge({ errors, warnings }: { errors: number; warnings: number }) if (warnings > 0) { return ( - } radius="sm"> + } radius="sm"> {t("status.warnings", { count: warnings })} ); } return ( - } radius="sm"> + } radius="sm"> {t("status.healthy")} ); diff --git a/packages/widgets/src/vpn/vpn-card.tsx b/packages/widgets/src/vpn/vpn-card.tsx index aeaa36c0c..439f1e7df 100644 --- a/packages/widgets/src/vpn/vpn-card.tsx +++ b/packages/widgets/src/vpn/vpn-card.tsx @@ -159,7 +159,7 @@ function VpnInfoColumn({ {!dense && (city || country) && ( - + {[city, country].filter(Boolean).join(", ")} @@ -176,7 +176,7 @@ function VpnProviderDetails({ provider, protocol }: { provider: string; protocol {provider} - + {protocol} @@ -196,7 +196,7 @@ function DnsStatusBadge({ status }: { status: string }) { color={getStatusColor(status)} size="xs" radius="xl" - leftSection={} + leftSection={} > DNS {statusLabel} diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx index f58e97653..357318fa6 100644 --- a/packages/widgets/src/weather/component.tsx +++ b/packages/widgets/src/weather/component.tsx @@ -116,11 +116,11 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => { - + {options.location.name} - + {t("currentWindSpeed", { currentWindSpeed: getPreferredWindSpeed(weather.current.windspeed, options.useImperialSpeed), @@ -196,7 +196,7 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => { - + {getPreferredUnit( dayWeather.maxTemp, @@ -207,7 +207,7 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => { / - + {getPreferredUnit( dayWeather.minTemp, @@ -219,13 +219,13 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => { - + {dayWeather.humidity === undefined ? "?" : `${dayWeather.humidity}%`} - + {getPreferredWindSpeed(dayWeather.maxWindSpeed, options.useImperialSpeed)} /{" "} {getPreferredWindSpeed(dayWeather.maxWindGusts, options.useImperialSpeed)} @@ -235,14 +235,14 @@ const AdvancedWeather = ({ options, weather }: WeatherProps) => { - + {getPreferredTime(dayWeather.sunrise, locale)} / - + {getPreferredTime(dayWeather.sunset, locale)} @@ -285,7 +285,7 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) => {options.showCurrentWindSpeed && ( - + {t("currentWindSpeed", { currentWindSpeed: (options.useImperialSpeed @@ -301,13 +301,13 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) => )} {weather.daily[0]?.humidity !== undefined && ( - + {t("dailyForecast.humidity", { humidity: weather.daily[0].humidity })} )} - + {getPreferredUnit( weather.daily[0]?.maxTemp, @@ -317,7 +317,7 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) => - + {getPreferredUnit( weather.daily[0]?.minTemp, @@ -331,7 +331,7 @@ const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) => {options.showCity && ( <> - + {options.location.name} @@ -350,7 +350,7 @@ const WeeklyForecast = ({ options, weather, isEditMode, maxDays }: CompactWeathe {options.showCity && ( - + {options.location.name} diff --git a/packages/widgets/src/weather/icon.tsx b/packages/widgets/src/weather/icon.tsx index d2c5a28b7..50268b1c1 100644 --- a/packages/widgets/src/weather/icon.tsx +++ b/packages/widgets/src/weather/icon.tsx @@ -95,15 +95,15 @@ export const WeatherDescription = ({ {formatWeatherDate(time, locale, dateFormat)} {t(`kind.${name}`)} - }>{`${tCommon("information.max")}: ${maxTemp}`} - }>{`${tCommon("information.min")}: ${minTemp}`} - }>{`${t("dailyForecast.sunrise")}: ${sunrise}`} - }>{`${t("dailyForecast.sunset")}: ${sunset}`} + }>{`${tCommon("information.max")}: ${maxTemp}`} + }>{`${tCommon("information.min")}: ${minTemp}`} + }>{`${t("dailyForecast.sunrise")}: ${sunrise}`} + }>{`${t("dailyForecast.sunset")}: ${sunset}`} {humidity !== undefined && ( - }>{t("dailyForecast.humidity", { humidity })} + }>{t("dailyForecast.humidity", { humidity })} )} {maxWindSpeed !== undefined && ( - }> + }> {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 = ({ )} {maxWindGusts !== undefined && ( - }> + }> {t("dailyForecast.maxWindGusts", { maxWindGusts: (useImperialSpeed ? metricToImperial(maxWindGusts) : maxWindGusts).toFixed(1), unit: useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"), diff --git a/packages/widgets/src/widget-integration-select.tsx b/packages/widgets/src/widget-integration-select.tsx index c89be9c3e..ed82c4099 100644 --- a/packages/widgets/src/widget-integration-select.tsx +++ b/packages/widgets/src/widget-integration-select.tsx @@ -88,7 +88,7 @@ export const WidgetIntegrationSelect = ({ return ( - {multiSelectValues.includes(item.id) ? : null} + {multiSelectValues.includes(item.id) ? : null}