[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.

refactor(ui): Remove non-mobile sidebar and ad header links

* Remove non-mobile sidebar to free up some more screen real estate
* Add quick Links menu to header that hides on mobile breakpoint

FoxxMD (Jul 8, 2026, 4:49 PM UTC) f431f4d5 be872d5c

+78 -31
+5 -5
src/client/AppNext.tsx
··· 13 13 import { Container, Box, Center, Splitter, useSplitter } from '@chakra-ui/react'; 14 14 import { MsSseEvent } from '../core/Api'; 15 15 import { SSEProvider } from "@flamefrontend/sse-runtime-react"; 16 - import { AppHeader, RightHeaderFloatingLogs, RightHeaderSwitchLogs } from './components/AppHeader'; 16 + import { AppHeader } from './components/AppHeader'; 17 17 import { NAV_LINKS, SideNavItems } from './components/SideNav'; 18 - import { LogsFetchable } from './components/LogsNext'; 19 - import { SplitLayout } from './components/layouts/SplitLayout'; 20 18 import { ComponentDetailedRoutable } from './components/msComponent/MSComponentDetailed'; 21 19 import { MSErrorBoundary } from './components/ErrorBoundary'; 22 20 ··· 60 58 //const [logsEnabled, setLogsEnabled] = useState(true); 61 59 const location = useLocation(); 62 60 return (<> 63 - <Box px="4" py="2" mb="4" pb="4" position="sticky" top="0" zIndex="1" bg="bg" borderBottomWidth="1px"><AppHeader fetchable><RightHeaderFloatingLogs streamable/></AppHeader></Box> 61 + <Box px="4" py="2" mb="4" pb="4" position="sticky" top="0" zIndex="1" bg="bg" borderBottomWidth="1px"> 62 + <AppHeader fetchable/> 63 + </Box> 64 64 <Container display="flex"> 65 - <Box hideBelow="md" display="flex" flexDir="column" pr="2" gap="6" flexShrink="1"><SideNavItems items={NAV_LINKS} currentUrl={location.pathname}/></Box> 65 + <Box hideBelow="md" display="flex" flexDir="column" pr="2" gap="6" flexShrink="1"></Box> 66 66 <Outlet/> 67 67 </Container> 68 68 </>);
+8 -4
src/client/components/AppHeader.tsx
··· 17 17 import { ErrorLike } from "../../core/Atomic"; 18 18 import { ErrorAlert } from "./ErrorAlert"; 19 19 import { MSErrorBoundary } from "./ErrorBoundary"; 20 + import { ExternaLinksMenu } from "./ExternaLinksMenu"; 20 21 21 22 export const AppTitle = (props: { fetchable?: boolean } = {}) => { 22 23 const { ··· 121 122 export const RightHeaderFloatingLogs = (props: {streamable?: boolean}) => { 122 123 const [width, height] = useWindowSize(); 123 124 124 - return <HStack gap="2"> 125 - <Box marginRight="2"><SSEStatus live={props.streamable}/></Box> 125 + return ( 126 126 <FloatingPanel.Root 127 127 defaultPosition={{x: width * 0.03, y: height * 0.65}} 128 128 defaultSize={{ width: width * 0.95, height: height * 0.3 }} ··· 160 160 </FloatingPanel.Positioner> 161 161 </Portal> 162 162 </FloatingPanel.Root> 163 - </HStack> 163 + ); 164 164 } 165 165 166 166 export const AppHeader = (props: PropsWithChildren<{ fetchable?: boolean }>) => { 167 167 return ( 168 168 <Flex justify="space-between"> 169 169 <AppTitle fetchable={props.fetchable} /> 170 - <Flex justify="flex-start" alignItems="flex-end">{props.children}</Flex> 170 + <Flex justify="flex-start" gap="1" alignItems="flex-end"> 171 + <ExternaLinksMenu hideBelow="sm"/> 172 + <Box marginRight="2"><SSEStatus live={props.fetchable}/></Box> 173 + <RightHeaderFloatingLogs streamable={props.fetchable}/> 174 + </Flex> 171 175 </Flex> 172 176 ) 173 177 }
+30
src/client/components/ExternaLinksMenu.tsx
··· 1 + import { Button, Menu, Portal, Box, ChakraComponent } from "@chakra-ui/react" 2 + import { ExternalLinkIcon } from "./icons/ChakraIcons" 3 + import { EXTERNAL_LINKS } from "./SideNav" 4 + import { ComponentProps } from "react" 5 + 6 + 7 + export const ExternaLinksMenu = (props: ComponentProps<typeof Box> = {}) => ( 8 + <Box {...props}> 9 + <Menu.Root> 10 + <Menu.Trigger asChild> 11 + <Button size="sm" variant="ghost"> 12 + Links 13 + </Button> 14 + </Menu.Trigger> 15 + <Portal> 16 + <Menu.Positioner> 17 + <Menu.Content> 18 + {EXTERNAL_LINKS.items.map((link) => ( 19 + <Menu.Item key={link.url} asChild value={link.title} cursor="pointer"> 20 + <a href={link.url} target="_blank" rel="noreferrer"> 21 + {link.title} <ExternalLinkIcon /> 22 + </a> 23 + </Menu.Item> 24 + ))} 25 + </Menu.Content> 26 + </Menu.Positioner> 27 + </Portal> 28 + </Menu.Root> 29 + </Box> 30 + )
+4 -7
src/client/components/MobileMenu.tsx
··· 12 12 const MobileMenuButton = MenuButton; 13 13 14 14 export const MobileSidebarNav = (props: { hideFrom?: BreakpointName | false } = {}) => { 15 - const { 16 - hideFrom = 'md' 17 - } = props; 18 15 const [isOpen, setIsOpen] = useState(false); 19 16 20 17 let location = useLocation(); ··· 22 19 const closeMenu = () => setIsOpen(false) 23 20 24 21 const menuButtonProps: ComponentProps<typeof MobileMenuButton> = { 25 - variant: 'ghost' 22 + variant: 'ghost', 23 + iconProps: { 24 + size: 'lg' 25 + } 26 26 }; 27 - if (hideFrom !== false) { 28 - menuButtonProps.hideFrom = hideFrom; 29 - } 30 27 31 28 useEffect(() => { 32 29 setIsOpen(false);
+16 -14
src/client/components/SideNav.tsx
··· 104 104 /> 105 105 ) 106 106 107 - export const NAV_LINKS: SideNavProps[] = [ 108 - { 109 - title: 'Main', 110 - id: 'Main', 111 - items: [ 112 - { 113 - title: 'Dashboard', 114 - url: '/next/' 115 - } 116 - ] 117 - }, 118 - { 107 + export const EXTERNAL_LINKS: SideNavProps = { 119 108 title: 'Links', 120 109 id: 'Links', 121 110 items: [ ··· 135 124 external: true 136 125 } 137 126 ] 138 - } 139 - ] 127 + } 128 + 129 + export const NAV_LINKS: SideNavProps[] = [ 130 + { 131 + title: 'Main', 132 + id: 'Main', 133 + items: [ 134 + { 135 + title: 'Dashboard', 136 + url: '/next/' 137 + } 138 + ] 139 + }, 140 + EXTERNAL_LINKS 141 + ];
+15 -1
src/client/components/icons/ChakraIcons.tsx
··· 48 48 </IconButton> 49 49 ); 50 50 } 51 + export const makeChakraIconButton = (IconBase: IconType) => (props: PropsWithChildren<ComponentProps<typeof IconButton>> & { iconProps?: ComponentProps<typeof Icon>, loading?: boolean }) => { 52 + const { 53 + iconProps = {}, 54 + children, 55 + loading = false, 56 + size = 'xs', 57 + ...rest 58 + } = props; 59 + return ( 60 + <IconButton variant="surface" disabled={loading} size={size} {...rest}> 61 + {loading ? <Spinner/> : <Icon {...iconProps}><IconBase/></Icon>}{children} 62 + </IconButton> 63 + ); 64 + } 51 65 export const makeChakraIcon = (IconComponent: IconType) => (props: ComponentProps<typeof Icon> & { iconProps?: IconBaseProps }) => { 52 66 const { 53 67 iconProps, ··· 95 109 ); 96 110 97 111 export const MenuIcon = LuAlignJustify; 98 - export const MenuButton = makeIconButton(MenuIcon); 112 + export const MenuButton = makeChakraIconButton(MenuIcon); 99 113 100 114 export const XIcon = LuX; 101 115 export const XButton = makeIconButton(XIcon);