[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): Make timeline items more compact

#500

FoxxMD (Jul 14, 2026, 3:23 PM UTC) 41ea9e0a a9d91c2c

+54 -27
+18 -18
src/client/components/ActivityTimeline.tsx
··· 1 - import type { Collapsible} from '@chakra-ui/react'; 1 + import type { Collapsible, IconProps} from '@chakra-ui/react'; 2 2 import { Card, Icon, SkeletonCircle, SkeletonText, Span, Tabs, Timeline, type HTMLChakraProps } from '@chakra-ui/react'; 3 3 import type { Dayjs } from "dayjs"; 4 4 import dayjs from "dayjs"; ··· 12 12 import { sortByNewestDate } from "../../core/PlayUtils"; 13 13 import { capitalizeWords } from "../../core/StringUtils"; 14 14 import { shortTodayAwareFormat } from "../../core/TimeUtils"; 15 - import { activityTransformHasIssue, timelineTextFormatting } from "../utils/ComponentUtils"; 15 + import { activityTransformHasIssue, timelineTextFormatting, timelineIconProps } from "../utils/ComponentUtils"; 16 16 import { ChakraCodeBlockShort } from "./CodeBlock"; 17 17 import { ErrorAlert } from "./ErrorAlert"; 18 18 import { MSErrorBoundary } from "./ErrorBoundary"; ··· 42 42 <Timeline.Connector> 43 43 <Timeline.Separator /> 44 44 <Timeline.Indicator> 45 - <Icon fontSize="lg"> 45 + <Icon {...timelineIconProps}> 46 46 <SkeletonCircle /> 47 47 </Icon> 48 48 </Timeline.Indicator> ··· 62 62 <Timeline.Connector> 63 63 <Timeline.Separator /> 64 64 <Timeline.Indicator> 65 - <TimelineIndicatorIconQueued fontSize="lg" /> 65 + <TimelineIndicatorIconQueued {...timelineIconProps} /> 66 66 </Timeline.Indicator> 67 67 </Timeline.Connector> 68 - <Timeline.Content gap="4"> 68 + <Timeline.Content> 69 69 <Timeline.Title> 70 70 <ItemSummaryText>{props.dead ? 'Dead ' : ''}Queued <Muted>at</Muted> {shortTodayAwareFormat(dayjs(props.datetime))}</ItemSummaryText> 71 71 </Timeline.Title> ··· 98 98 <Timeline.Connector> 99 99 <Timeline.Separator /> 100 100 <Timeline.Indicator> 101 - <Icon fontSize="lg"> 101 + <Icon {...timelineIconProps}> 102 102 <IoMdCodeDownload /> 103 103 </Icon> 104 104 </Timeline.Indicator> ··· 157 157 <Timeline.Connector> 158 158 <Timeline.Separator /> 159 159 <Timeline.Indicator> 160 - <Icon fontSize="lg"> 160 + <Icon {...timelineIconProps}> 161 161 <BiWrench /> 162 162 </Icon> 163 163 </Timeline.Indicator> 164 164 </Timeline.Connector> 165 - <Timeline.Content gap="4"> 165 + <Timeline.Content> 166 166 <Timeline.Title> 167 167 <MSCollapsible 168 168 triggerProps={timelineCollapsibleProps} ··· 186 186 <Timeline.Connector> 187 187 <Timeline.Separator /> 188 188 <Timeline.Indicator> 189 - <Icon fontSize="lg"> 189 + <Icon {...timelineIconProps}> 190 190 <BiWrench /> 191 191 </Icon> 192 192 </Timeline.Indicator> 193 193 </Timeline.Connector> 194 - <Timeline.Content gap="4"> 194 + <Timeline.Content> 195 195 <Timeline.Title > 196 196 <ItemSummaryText>Play <Muted>was</Muted> not transformed <Muted>because no</Muted> Transform Rules <Muted> were used/configured.</Muted></ItemSummaryText> 197 197 </Timeline.Title> ··· 210 210 <Timeline.Connector> 211 211 <Timeline.Separator /> 212 212 <Timeline.Indicator> 213 - <Icon fontSize="lg" color={`${match.match ? 'orange' : 'green'}.focusRing`}> 213 + <Icon {...timelineIconProps} color={`${match.match ? 'orange' : 'green'}.focusRing`}> 214 214 <HiMiniMagnifyingGlass /> 215 215 </Icon> 216 216 </Timeline.Indicator> 217 217 </Timeline.Connector> 218 - <Timeline.Content gap="4"> 218 + <Timeline.Content> 219 219 <Timeline.Title> 220 220 <MSCollapsible 221 221 triggerProps={timelineCollapsibleProps} ··· 268 268 <Timeline.Separator /> 269 269 <Timeline.Indicator> 270 270 {error !== undefined ? <TimelineErrorIcon /> : ( 271 - <Icon fontSize="lg" {...scrobbleIconProps}> 271 + <Icon {...timelineIconProps} {...scrobbleIconProps}> 272 272 <TbDatabaseEdit /> 273 273 </Icon> 274 274 )} 275 275 </Timeline.Indicator> 276 276 </Timeline.Connector> 277 - <Timeline.Content gap="4"> 277 + <Timeline.Content > 278 278 <Timeline.Title> 279 279 <MSCollapsible 280 280 triggerProps={timelineCollapsibleProps} ··· 305 305 <Timeline.Connector> 306 306 <Timeline.Separator /> 307 307 <Timeline.Indicator> 308 - <TimelineIndicatorIconQueued fontSize="lg"/> 308 + <TimelineIndicatorIconQueued {...timelineIconProps}/> 309 309 </Timeline.Indicator> 310 310 </Timeline.Connector> 311 311 <Timeline.Content gap="4"> ··· 323 323 <Timeline.Connector> 324 324 <Timeline.Separator /> 325 325 <Timeline.Indicator> 326 - <CheckIcon fontSize="lg"/> 326 + <CheckIcon {...timelineIconProps}/> 327 327 </Timeline.Indicator> 328 328 </Timeline.Connector> 329 329 <Timeline.Content gap="4"> ··· 356 356 <Timeline.Connector> 357 357 <Timeline.Separator /> 358 358 <Timeline.Indicator> 359 - <CheckIcon fontSize="lg"/> 359 + <CheckIcon {...timelineIconProps}/> 360 360 </Timeline.Indicator> 361 361 </Timeline.Connector> 362 - <Timeline.Content gap="4"> 362 + <Timeline.Content > 363 363 <Timeline.Title> 364 364 {titleContent} 365 365 </Timeline.Title>
+10 -2
src/client/components/MSCollapsible.tsx
··· 20 20 21 21 export const timelineCollapsibleProps = { 22 22 indicatorProps: { paddingBottom: '4px' }, 23 - triggerProps: { paddingBlockStart: "0.3em" } 23 + triggerProps: { paddingBlockStart: "0.3em" }, 24 + triggerPropsClosed: { 25 + //paddingBlockStart: "0.3em", 26 + paddingBlock: 'initial' 27 + }, 28 + triggerPropsOpen: { 29 + paddingBlock: 'initial', 30 + paddingBlockEnd: "var(--chakra-spacing-3)" 31 + } 24 32 } 25 33 26 34 export type MSCollapsibleProps = PropsWithChildren<ComponentProps<typeof Collapsible.Root>> & MSCollapsibleInternalProps; ··· 104 112 iProps.display = 'none'; 105 113 } 106 114 107 - const tProps = { ...(timeline ? timelineCollapsibleProps.triggerProps : {}), ...triggerProps } 115 + const tProps = { ...(timeline ? (open? timelineCollapsibleProps.triggerPropsOpen : timelineCollapsibleProps.triggerPropsClosed) : {}), ...triggerProps } 108 116 109 117 return ( 110 118 <Collapsible.Root open={open} onOpenChange={(val) => setOpen(val.open)} flexGrow="1" disabled={isDisabled} {...rest}>
+16 -1
src/client/components/Provider.tsx
··· 1 1 "use client" 2 2 3 - import { ChakraProvider, createSystem, defaultConfig, defineConfig } from "@chakra-ui/react" 3 + import { ChakraProvider, createSystem, defaultConfig, defineConfig, defineSlotRecipe } from "@chakra-ui/react" 4 + import { timelineAnatomy } from "@chakra-ui/react/anatomy"; 4 5 import { 5 6 QueryClient, 6 7 QueryClientProvider ··· 10 11 ColorModeProvider, 11 12 type ColorModeProviderProps, 12 13 } from "./Color-Mode" 14 + 15 + const timelineSlotRecipe = defineSlotRecipe({ 16 + slots: timelineAnatomy.keys(), 17 + base: { 18 + item: { 19 + "--timeline-content-gap": "spacing.5" // was spacing.6 20 + } 21 + } 22 + }); 13 23 14 24 const queryClient = new QueryClient() 15 25 ··· 37 47 color: "gray.500" 38 48 } 39 49 }, 50 + theme: { 51 + slotRecipes: { 52 + timeline: timelineSlotRecipe 53 + } 54 + } 40 55 }) 41 56 42 57 export const system = createSystem(defaultConfig, customConfig)
+5 -5
src/client/components/TransformSteps.tsx
··· 1 - import { Heading, Icon, Span, Stack, Text, Timeline } from '@chakra-ui/react'; 1 + import { Heading, Icon, Span, Stack, Text, Timeline} from '@chakra-ui/react'; 2 2 import React, { Fragment, useMemo } from "react"; 3 3 import { BsExclamationTriangle, BsSkipForward, BsStoplights } from "react-icons/bs"; 4 4 import { MdMusicNote } from "react-icons/md"; 5 5 import type {JsonPlayObject, LifecycleStep} from "../../core/Atomic"; 6 6 import { patchObject } from "../../core/DataUtils"; 7 - import { timelineTextFormatting } from "../utils/ComponentUtils"; 7 + import { timelineIconProps, timelineTextFormatting } from "../utils/ComponentUtils"; 8 8 import { ChakraCodeBlockShort } from "./CodeBlock"; 9 9 import { ErrorAlert } from "./ErrorAlert"; 10 10 import { MSCollapsible, type MSCollapsibleExternalProps } from "./MSCollapsible"; ··· 84 84 const [diffs, finalPlay] = useMemo(() => diffElements(original, steps), [steps, original]); 85 85 86 86 return ( 87 - <Timeline.Root variant="subtle" css={{ "--timeline-separator-display": 'block' }}> 87 + <Timeline.Root variant="subtle" size="lg" css={{ "--timeline-separator-display": 'block' }}> 88 88 {steps.map((x, index) => { 89 89 const { 90 90 patch, ··· 137 137 <Timeline.Connector> 138 138 <Timeline.Separator /> 139 139 <Timeline.Indicator> 140 - <Icon fontSize="lg" {...iconProps}> 140 + <Icon {...timelineIconProps} {...iconProps}> 141 141 {timelineIcon} 142 142 </Icon> 143 143 </Timeline.Indicator> ··· 175 175 <Timeline.Connector> 176 176 <Timeline.Separator /> 177 177 <Timeline.Indicator> 178 - <Icon fontSize="lg"> 178 + <Icon {...timelineIconProps}> 179 179 <MdMusicNote /> 180 180 </Icon> 181 181 </Timeline.Indicator>
+5 -1
src/client/utils/ComponentUtils.tsx
··· 1 - import type { Card} from '@chakra-ui/react'; 1 + import type { Card, IconProps} from '@chakra-ui/react'; 2 2 import type {HTMLChakraProps} from '@chakra-ui/react'; 3 3 import type {PlayApiCommonDetailed} from '../../core/Api'; 4 4 import { type LifecycleStep, QUEUE_STATUS_COMPLETED, QUEUE_STATUS_FAILED } from '../../core/Atomic'; ··· 11 11 export const timelineTextFormatting: HTMLChakraProps<"span"> = { 12 12 //textAlign: "left", 13 13 textWrap: "balance" 14 + } 15 + 16 + export const timelineIconProps: IconProps = { 17 + fontSize: 'md' 14 18 } 15 19 16 20 export const activityTransformHasIssue = (steps: LifecycleStep[]): 'warn' | 'error' | undefined => {