[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): Persist log window state #500

Store opened state, and position/size, to local storage so logs stay where user wants them across refreshes/initial load.

Resets window to default position if any part of log window is outside viewport bounds on open.

FoxxMD (Jul 12, 2026, 4:19 PM UTC) fab2e493 fbcd1e1b

+61 -3
+61 -3
src/client/components/LogsNext.tsx
··· 7 7 import * as AnsiImport from "ansi-to-react"; 8 8 import { FixedSizeList } from "fixed-size-list"; 9 9 import ky from 'ky'; 10 - import React, { useCallback, useEffect, useRef, useState } from 'react'; 10 + import React, { useCallback, useEffect, useRef, useState, type ComponentProps } from 'react'; 11 11 import type {LogLevelStandalone, LogOutputConfig} from '../../core/Atomic'; 12 12 import { tanQueries } from '../queries'; 13 13 import { ChakraClipDynamic } from './ChakraClipboard'; ··· 15 15 import { LuGripHorizontal, LuMinus } from 'react-icons/lu'; 16 16 import { Ripple } from './icons/AnimatedIcons'; 17 17 import { MSErrorBoundary } from './ErrorBoundary'; 18 + import { useLocalStorage } from 'usehooks-ts' 18 19 19 20 // @ts-expect-error Ansi export is built incorrectly 20 21 const Ansi = AnsiImport.default.default as typeof AnsiImport.default; ··· 148 149 149 150 } 150 151 152 + interface Rectangle { 153 + x: number; // left edge 154 + y: number; // top edge 155 + width: number; 156 + height: number; 157 + } 158 + 159 + const isAnyCornerOutside = (rectangleA: Rectangle, rectangleB: Rectangle): boolean => { 160 + const corners = [ 161 + { x: rectangleA.x, y: rectangleA.y }, // top-left 162 + { x: rectangleA.x + rectangleA.width, y: rectangleA.y }, // top-right 163 + { x: rectangleA.x, y: rectangleA.y + rectangleA.height }, // bottom-left 164 + { x: rectangleA.x + rectangleA.width, y: rectangleA.y + rectangleA.height }, // bottom-right 165 + ]; 166 + 167 + const bLeft = rectangleB.x; 168 + const bRight = rectangleB.x + rectangleB.width; 169 + const bTop = rectangleB.y; 170 + const bBottom = rectangleB.y + rectangleB.height; 171 + 172 + return corners.some( 173 + (corner) => 174 + corner.x < bLeft || 175 + corner.x > bRight || 176 + corner.y < bTop || 177 + corner.y > bBottom 178 + ); 179 + }; 180 + 181 + const defaultPosition = (width: number, height: number): ComponentProps<typeof FloatingPanel['Root']>['position'] => ({x: width * 0.03, y: height * 0.65}); 182 + const defaultSize = (width: number, height: number): ComponentProps<typeof FloatingPanel['Root']>['size'] => ({ width: width * 0.95, height: height * 0.3 }); 183 + 151 184 export const FloatingLogs = (props: {streamable?: boolean}) => { 152 185 const [width, height] = useWindowSize(); 153 186 187 + // remember logs state across refreshes 188 + // keep logs open if it was last opened, in the last position/size *if still within viewport bounds* 189 + const [logsOpen, setLogsOpen] = useLocalStorage('logsOpen', false); 190 + const [logsPosition, setLogsPosition] = useLocalStorage<ComponentProps<typeof FloatingPanel['Root']>['position']>('logsPosition', defaultPosition(width, height)); 191 + const [logsSize, setLogsSize] = useLocalStorage<ComponentProps<typeof FloatingPanel['Root']>['size']>('logsSize', defaultSize(width, height)); 192 + 193 + // when logs are opened 194 + // check if any part of the logs window is outside the bounds of the viewport 195 + // and if it is then reset size/position to default so that user can't accidentally make logs irretrievable 196 + useEffect(() => { 197 + if(logsOpen) { 198 + const logRectangle: Rectangle = {x: logsPosition.x, y: logsPosition.y, width: logsSize.width, height: logsSize.height}; 199 + const windowRectangle: Rectangle = {x: 0, y: 0, width, height}; 200 + if(isAnyCornerOutside(logRectangle, windowRectangle)) { 201 + console.log('resetting logs height/size due to boundary conflict'); 202 + setLogsPosition(defaultPosition(width, height)); 203 + setLogsSize(defaultSize(width, height)); 204 + } 205 + } 206 + },[logsOpen]); 207 + 154 208 return ( 155 209 <FloatingPanel.Root 156 - defaultPosition={{x: width * 0.03, y: height * 0.65}} 157 - defaultSize={{ width: width * 0.95, height: height * 0.3 }} 210 + open={logsOpen} 211 + onOpenChange={(details) => setLogsOpen(details.open)} 212 + position={logsPosition} 213 + onPositionChange={(details) => setLogsPosition(details.position)} 214 + size={logsSize} 215 + onSizeChange={(details) => setLogsSize(details.size)} 158 216 persistRect 159 217 closeOnEscape 160 218 lazyMount