import { CheckCircleFillIcon } from '@primer/octicons-react' import { useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { useAudioStorageContext } from '../context/AudioStorageContext/AudioStorageContext' import { useDownloadContext } from '../context/DownloadContext/DownloadContext' import { useJellyfinContext } from '../context/JellyfinContext/JellyfinContext' import { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext' import { useThemeContext } from '../context/ThemeContext/ThemeContext' import { usePageTitle } from '../context/PageTitleContext/PageTitleContext' import { persister } from '../queryClient' import { formatFileSize } from '../utils/formatFileSize' import './Settings.css' export const Settings = ({ onLogout }: { onLogout: () => void }) => { const { setPageTitle } = usePageTitle() const navigate = useNavigate() const api = useJellyfinContext() const audioStorage = useAudioStorageContext() const { theme, toggleTheme } = useThemeContext() const [lastLogin, setLastLogin] = useState(null) const [clientIp, setClientIp] = useState(null) const [latency, setLatency] = useState(null) const { sessionPlayCount, resetSessionCount, bitrate, setBitrate } = usePlaybackContext() const playback = usePlaybackContext() const queryClient = useQueryClient() const { storageStats, refreshStorageStats, queueCount, clearQueue } = useDownloadContext() const [clearing, setClearing] = useState(false) useEffect(() => { setPageTitle('Settings') }, [setPageTitle]) useEffect(() => { const fetchData = async () => { try { const [user, clientIp, latencyMs] = await Promise.all([ api.fetchUserInfo(), api.fetchClientIp(), api.measureLatency(), ]) if (user.LastLoginDate) { const date = new Date(user.LastLoginDate) const formatted = date .toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', year: 'numeric', hour12: true, }) .replace(/,/, '') setLastLogin(formatted) } else { setLastLogin(null) } setClientIp(clientIp) setLatency(latencyMs) } catch (error) { console.error('Error fetching data:', error) } } fetchData() }, [api]) const handleLogout = () => { playback.audioRef.current.pause() playback.crossfadeRef.current.pause() resetSessionCount() onLogout() navigate('/login') } const handleClearAll = useCallback(async () => { if (!confirm('Are you sure you want to clear all downloads? This cannot be undone.')) { return } try { setClearing(true) await audioStorage.clearAllDownloads() queryClient.clear() await persister.removeClient() clearQueue() await refreshStorageStats() } catch (error) { console.error('Failed to clear downloads:', error) } finally { setClearing(false) } }, [audioStorage, clearQueue, queryClient, refreshStorageStats]) return (
Appearance
toggleTheme('light')} >
Light
toggleTheme('dark')} >
Dark
toggleTheme('system')} >
System
Audio Quality
Streaming & Offline Sync
Adjusting audio quality enables server-side transcoding, converting to a compatible format with a lower bitrate for smoother streaming or efficient offline syncing
setBitrate(0)}>
Source
Direct playback of the original audio source without modifications
setBitrate(320000)} >
High 320 kbps
Superior sound quality, perfect for immersive listening with moderate data usage
setBitrate(256000)} >
Medium 256 kbps
Crisp audio with a balanced blend of quality and data efficiency
setBitrate(192000)} >
Low 192 kbps
Solid quality tailored for streaming with reduced bandwidth
setBitrate(128000)} >
Minimal 128 kbps
Essential audio quality optimized for the lowest data consumption
Offline Sync
Synced Music - {storageStats.trackCount} Track{storageStats.trackCount === 1 ? '' : 's'} / {formatFileSize(storageStats.trackCount === 0 ? 0 : storageStats?.indexedDB || 0)} Used
Cache your music library for seamless offline playback, with new tracks auto-syncing to saved playlists, albums, or artists. Browse music library, available once tracks are synced
{(storageStats.trackCount > 0 || queueCount > 0) && (
)}
Crossfade
{playback.crossfadeDuration}s
Smoothly transition between tracks by gradually fading out the current song while simultaneously fading in the next, creating a seamless and immersive listening experience
playback.setCrossfadeDuration(Number(e.target.value))} disabled={!playback.isCrossfadeActive} />
Lyrics
Timestamps
Show timestamps with the synchronized lyrics
Alignment
Center lyrics for a different look, overriding the default left alignment
Interface
Playback & Navigation
Customize your music experience with personalized playback preferences and interface behaviors
Filter state
Remember selected filters across sessions for a consistent experience
Queue protection
Warn before overwriting the active queue if it includes manually added tracks
Shuffle mode
Enable shuffle by default when playing albums or playlists
Repeat mode
Set default repeat behavior for playback
Performance
Optimization & Caching
Fine-tune performance settings to optimize your listening experience based on your device and network
Preload next track
Buffer the next song in advance for seamless playback transitions
Background sync
Continue downloading tracks when the app is in the background
About
Jelly Music App - Version {__VERSION__}
An open source music player for Jellyfin
Carefully crafted with great attention to detail, aiming to reduce noise and distractions with a minimalistic & lightweight interface: "the quieter you become, the more you are able to hear"
Source code is freely available on{' '} GitHub {' '} and is licensed under the MIT license
Session

Currently connected to{' '} {api.auth.serverUrl} {' '} {latency !== null && ( with {latency}ms latency )}

Last login: {lastLogin} {clientIp ? ` from ${clientIp}` : ''}

Played{' '} {sessionPlayCount !== null && ( {sessionPlayCount} {sessionPlayCount === 1 ? 'track' : 'tracks'} )}{' '} since login

) }