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 { persister } from '../queryClient' import { formatFileSize } from '../utils/formatFileSize' import './Settings.css' export const Settings = ({ onLogout }: { onLogout: () => void }) => { 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(() => { 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'} {queueCount > 0 ? ( <> {' '} ({queueCount} track{queueCount === 1 ? '' : 's'} in queue) ) : ( '' )}{' '} /{' '} {formatFileSize(storageStats.trackCount === 0 ? 0 : storageStats?.indexedDB || 0)} {' '} Used
{(storageStats.trackCount > 0 || queueCount > 0 || !audioStorage.isInitialized()) && ( )}
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
Crossfade
{playback.crossfadeDuration}s
playback.setCrossfadeDuration(Number(e.target.value))} />
12s
Smoothly transition between tracks by gradually fading out the current song while simultaneously fading in the next, creating a seamless and immersive listening experience
Lyrics
Timestamps
Show timestamps with the synchronized lyrics
Alignment
Center lyrics for a different look, overriding the default left alignment
Interface
Filter state
Remember selected filters across sessions for a consistent experience
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

) }