diff --git a/src/components/settings/LoudnessLufsButtonGroup.tsx b/src/components/settings/LoudnessLufsButtonGroup.tsx new file mode 100644 index 00000000..ea918ba1 --- /dev/null +++ b/src/components/settings/LoudnessLufsButtonGroup.tsx @@ -0,0 +1,24 @@ +import type { LoudnessLufsPreset } from '../../store/authStoreTypes'; + +const LOUDNESS_LUFS_BUTTON_ORDER: LoudnessLufsPreset[] = [-10, -12, -14, -16]; + +export function LoudnessLufsButtonGroup(props: { + value: LoudnessLufsPreset; + onSelect: (v: LoudnessLufsPreset) => void; +}) { + return ( +
+ {LOUDNESS_LUFS_BUTTON_ORDER.map(v => ( + + ))} +
+ ); +} diff --git a/src/components/settings/settingsTabs.ts b/src/components/settings/settingsTabs.ts new file mode 100644 index 00000000..dc89f132 --- /dev/null +++ b/src/components/settings/settingsTabs.ts @@ -0,0 +1,87 @@ +export type Tab = + | 'library' + | 'servers' + | 'audio' + | 'lyrics' + | 'appearance' + | 'personalisation' + | 'integrations' + | 'input' + | 'storage' + | 'system' + | 'users'; + +// Legacy Tab-IDs die via Route-State oder persisted State noch aufschlagen koennen +// auf die neue Struktur mappen. Gibt es keinen Match, faellt die Settings-Page +// einfach auf 'library' zurueck. +const LEGACY_TAB_ALIAS: Record = { + general: 'library', + server: 'servers', +}; + +export function resolveTab(input: string | undefined | null): Tab { + if (!input) return 'servers'; + const aliased = LEGACY_TAB_ALIAS[input]; + if (aliased) return aliased; + const known: Tab[] = ['library', 'servers', 'audio', 'lyrics', 'appearance', 'personalisation', 'integrations', 'input', 'storage', 'system', 'users']; + return (known as string[]).includes(input) ? (input as Tab) : 'servers'; +} + +// Statischer Suchindex ueber alle Sub-Sections aller Tabs. Mitpflegen, wenn eine +// neue SettingsSubSection hinzukommt — sonst taucht sie nicht in der Suche auf. +export type SearchIndexEntry = { tab: Tab; titleKey: string; keywords?: string }; + +export const SETTINGS_INDEX: SearchIndexEntry[] = [ + { tab: 'audio', titleKey: 'settings.audioOutputDevice', keywords: 'output device speakers headphones alsa wasapi coreaudio' }, + { tab: 'audio', titleKey: 'settings.hiResTitle', keywords: 'hi-res hires resampling bit depth sample rate dsd 24bit' }, + { tab: 'audio', titleKey: 'settings.eqTitle', keywords: 'equalizer eq bass treble autoeq filter pre-gain' }, + { tab: 'audio', titleKey: 'settings.playbackTitle', keywords: 'playback crossfade gapless replaygain replay gain volume' }, + { tab: 'lyrics', titleKey: 'settings.lyricsSourcesTitle', keywords: 'lyrics sources providers lrclib netease server youlyplus karaoke standard static' }, + { tab: 'lyrics', titleKey: 'settings.sidebarLyricsStyle', keywords: 'lyrics scroll style classic apple music' }, + { tab: 'integrations', titleKey: 'settings.lfmTitle', keywords: 'last.fm lastfm scrobble' }, + { tab: 'integrations', titleKey: 'settings.discordRichPresence', keywords: 'discord rich presence rpc' }, + { tab: 'integrations', titleKey: 'settings.enableBandsintown', keywords: 'bandsintown concerts tours events' }, + { tab: 'integrations', titleKey: 'settings.nowPlayingEnabled', keywords: 'now playing share dropdown presence' }, + { tab: 'personalisation',titleKey: 'settings.sidebarTitle', keywords: 'sidebar nav navigation items reorder customize' }, + { tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' }, + { tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'home page customize sections' }, + { tab: 'personalisation',titleKey: 'settings.queueToolbarTitle', keywords: 'queue toolbar buttons reorder customize shuffle save load' }, + { tab: 'library', titleKey: 'settings.randomMixTitle', keywords: 'random mix blacklist genre keywords filter audiobook' }, + { tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' }, + { tab: 'storage', titleKey: 'settings.offlineDirTitle', keywords: 'offline library download directory folder cache' }, + { tab: 'storage', titleKey: 'settings.nextTrackBufferingTitle', keywords: 'next track buffering preload hot cache streaming' }, + { tab: 'storage', titleKey: 'settings.downloadsTitle', keywords: 'downloads zip export archive folder' }, + { tab: 'appearance', titleKey: 'settings.theme', keywords: 'theme color palette dark light' }, + { tab: 'appearance', titleKey: 'settings.themeSchedulerTitle', keywords: 'theme scheduler auto time dark mode sunset' }, + { tab: 'appearance', titleKey: 'settings.visualOptionsTitle', keywords: 'visual options animations effects titlebar mini player' }, + { tab: 'appearance', titleKey: 'settings.uiScaleTitle', keywords: 'ui scale zoom dpi size' }, + { tab: 'appearance', titleKey: 'settings.font', keywords: 'font typography typeface' }, + { tab: 'appearance', titleKey: 'settings.fsPlayerSection', keywords: 'fullscreen player mesh blob' }, + { tab: 'appearance', titleKey: 'settings.seekbarStyle', keywords: 'seekbar progress bar waveform reduced animations performance gpu fps low-end framerate cap' }, + { tab: 'input', titleKey: 'settings.inputKeybindingsTitle', keywords: 'keybindings shortcuts hotkeys keyboard' }, + { tab: 'input', titleKey: 'settings.globalShortcutsTitle', keywords: 'global shortcuts hotkeys system-wide media keys' }, + { tab: 'system', titleKey: 'settings.language', keywords: 'language locale translation i18n' }, + { tab: 'system', titleKey: 'settings.behavior', keywords: 'behavior tray minimize close start smooth scroll linux' }, + { tab: 'system', titleKey: 'settings.backupTitle', keywords: 'backup export import settings restore' }, + { tab: 'system', titleKey: 'settings.loggingTitle', keywords: 'log logs diagnostic debug verbose' }, + { tab: 'system', titleKey: 'settings.aboutTitle', keywords: 'about version update changelog release notes' }, + { tab: 'system', titleKey: 'settings.aboutContributorsLabel', keywords: 'contributors credits maintainers' }, + { tab: 'system', titleKey: 'licenses.title', keywords: 'licenses license open source attribution copyright third party dependencies oss' }, +]; + +// Substring-first, Fuzzy-Fallback (alle Query-Zeichen in Reihenfolge im +// Haystack). Rueckgabe 0 = kein Match. Hoeher = besser. +export function matchScore(haystack: string, needle: string): number { + if (!needle) return 0; + const h = haystack.toLowerCase(); + const n = needle.toLowerCase(); + const idx = h.indexOf(n); + if (idx >= 0) return 1000 - Math.min(999, idx); + let hi = 0; + for (const ch of n) { + const j = h.indexOf(ch, hi); + if (j < 0) return 0; + hi = j + 1; + } + return Math.max(1, 100 - Math.min(99, hi - n.length)); +} diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts new file mode 100644 index 00000000..cab5d97a --- /dev/null +++ b/src/config/settingsCredits.ts @@ -0,0 +1,279 @@ +// Credits list rendered on the Settings → System tab. Update via PR when adding a contributor. +export const CONTRIBUTORS = [ + { + github: 'jiezhuo', + since: '1.21', + contributions: [ + 'Chinese (Simplified) translation', + ], + }, + { + github: 'nullobject', + since: '1.22.0', + contributions: [ + 'Seek debounce & race condition fix (PR #7)', + 'Waveform seekbar stability on position update (PR #8)', + ], + }, + { + github: 'trbn1', + since: '1.22.0', + contributions: [ + 'Replay Gain metadata propagation (PR #9)', + 'songToTrack() — unified track construction across all sources', + ], + }, + { + github: 'nisarg-78', + since: '1.29.0', + contributions: [ + 'Click-to-seek in synced lyrics (PR #38)', + 'Volume scroll wheel on volume slider (PR #38)', + 'Lyrics line visual states: active / completed / upcoming (PR #38)', + 'Queue auto-scroll to keep upcoming tracks in view; fixed re-renders from currentTime in QueuePanel (PR #115)', + ], + }, + { + github: 'JulianNymark', + since: '1.29.0', + contributions: [ + 'OGG/Vorbis container support via symphonia-format-ogg (PR #42)', + 'Themed toast notifications for audio playback errors (PR #43)', + 'Human-readable audio error messages (PR #44)', + ], + }, + { + github: 'zz5zz', + since: '1.32.0', + contributions: [ + 'Norwegian (Bokmål) translation (PR #101)', + ], + }, + { + github: 'cucadmuh', + since: '1.33.0', + contributions: [ + 'Russian translation & i18n locale split (PR #106)', + 'Russian locale refinements using phrasing from ru2 (PR #113)', + 'Gapless manual skip: honor user-initiated play over pre-chained track (PR #119)', + 'Hot playback cache — queue prefetch (PR #123)', + 'Per-server music folder filter and sidebar library picker (PR #124, PR #125)', + 'Richer star ratings, skip threshold, and library filtering (PR #130)', + 'Statistics: scope album and song totals to selected music library (PR #138)', + 'AudioMuse-AI discovery integration for Navidrome (PR #147)', + 'Hot playback cache — eviction budgeting, grace period, and live Audio settings readout (PR #153)', + 'Folder Browser: keyboard navigation, context menus, now-playing path emphasis, and adaptive column layout (PR #158)', + 'Infinite queue: artist-driven candidates via Top Songs + Similar Songs with random fallback (PR #163)', + 'Folder Browser: per-column filter with keyboard flow and Shift+Enter queue append (PR #165)', + 'Keybindings: modifier + key chords for in-app shortcuts; fixed seek ±10s units (PR #167)', + 'Statistics: genre insights scoped to music library, cached Subsonic fetches, localized duration formatting (PR #144)', + 'Audio output device picker: clearer ALSA labels, duplicate disambiguation, system-default mark, live refresh (PR #173)', + 'Folder Browser: arrow navigation blocked when modifier keys are held (PR #174)', + 'Linux audio output device picker: stable watcher (disable false enumeration-miss resets), canonicalize ALSA name drift, ghost entry for unlisted device (PR #176)', + 'Opus audio playback via symphonia-adapter-libopus with bundled libopus (PR #183)', + 'CLI player controls with D-Bus forwarding, shell completions for bash/zsh, library/audio-device/instant-mix CLI commands, active server switcher in header (PR #187)', + 'Streaming playback stability: stream-first start, seek recovery, crossfade/gapless backup preload, hot-cache promotion (PR #200)', + 'ReplayGain values in Queue tech strip (PR #196)', + 'Playback source badge (offline / cache / stream) in Queue tech strip (PR #201)', + 'WebKitGTK wheel scroll mode: smooth (kinetic) default with optional linear toggle (PR #207)', + 'ArtistCardLocal i18n: use plural-aware artists.albumCount key instead of hardcoded German', + 'NixOS / flake install guide with Cachix setup (PR #209)', + 'Subsonic: align Rust HTTP UA with main WebView UA (PR #235)', + 'UI: overlay scrollbars, resizer hit-test, and Linux mini-wheel (PR #255)', + 'Server invites: magic-string paste, Navidrome admin share, add-user validation (PR #258)', + 'Library deep links (psysonic2 scheme) — paste track/album/artist/queue (PR #261)', + 'Linux: stop Wayland GTK drag proxy and PsyDnD ghost (PR #268)', + 'Sidebar: long-press drag to reorder and hide nav items (PR #269)', + 'Lucky Mix — instant queue from listening history, ratings, and AudioMuse similar tracks (PR #278)', + 'UI perf: fix spike when medulla-perch lines up with hair-fan gestures (PR #283)', + 'Navidrome smart playlists workflow in the Playlists page (PR #289)', + 'Loudness normalization — LUFS / EBU R128 analysis cache + persistent waveform store (PR #315)', + 'Queue resize handle: no longer blocked by main scroll hit-test (PR #324)', + 'LUFS loop stabilization, serialized CPU seed work, mixed mean/peak waveform bins (PR #326)', + 'Queue undo/redo — hotkeys, playback-aware restore, scroll restoration (PR #331)', + 'Lucky Mix + mix rating filter (Navidrome / OpenSubsonic) (PR #332)', + 'Loudness: target sync, effective pre-analysis trim, queue/settings copy (PR #333)', + 'CLI logs mode with tail/follow and shell completions (PR #337)', + 'Player: reserve preview row space in delay modal (PR #344)', + 'Infinite queue: respect rating filter in top-up (PR #357)', + 'NixOS install guide refresh for channels and CI (PR #380)', + 'UI refinements — sidebar discovery indicators, adaptive header, interaction polish (PR #397)', + 'Queue: drag rows outside to remove with trash ghost (PR #420)', + 'Tauri: modularize audio and lib command layers (PR #422)', + 'Shortcuts: action registry, dynamic CLI help, 9 new input targets + F1 help binding (PR #435)', + 'Environment upgrade & hot-cache playback — replay via RAM/disk on same-track and queue-end resume, playback-source icon stays correct after resume/undo/gapless, sidebar new-releases 500-id cap merge, Windows tray double-click fix, lazy-loaded routes, rodio 0.22 migration (PR #463)', + 'Audio: post-sleep stream recovery (Windows + Linux) with poll-gap-armed stall watchdog; preview seekbar freeze + anti-jump on preview end; remove card hover lift and per-card GPU compositing hints (PR #476)', + 'Analysis queue control: prune stale http-backfill / cpu-seed jobs when tracks leave the playback queue, cap loudness backfill warmup to current + next 5 tracks, plus debug counters for diagnostics (PR #480)', + ], + }, + { + github: 'kilyabin', + since: '1.34.0', + contributions: [ + 'Russian locale improvements (PR #107, PR #120)', + 'Auto-install script for Debian / RHEL (PR #121)', + 'Album cover art in Discord Rich Presence via iTunes API (PR #111)', + 'Tiling WM detection: hide custom TitleBar on Hyprland/Sway/i3/etc. (PR #134)', + 'Russian translation: lyricsServerFirst settings strings (PR #140)', + 'Russian translation refinements (PR #148)', + 'Merge Random Mix & Albums into a single Build a Mix hub (PR #155)', + 'Fullscreen player: software-rendering performance fixes + portrait toggle & dimming setting (PR #156)', + 'Fullscreen player: stop mesh blob and portrait animations in no-compositing mode; remove seekbar box-shadow repaint (PR #175)', + 'Apple Music-style scrolling lyrics with spring-physics scroll for fullscreen player and sidebar; per-style controls (PR #205)', + 'Golos Text and Unbounded fonts with Cyrillic support (PR #206)', + 'Fullscreen & sidebar lyrics: duration-based ease-out scroll animator replacing spring physics; bottom fade for plain lyrics (PR #214)', + 'Sidebar lyrics: YouLy+ source strings render in a single line (PR #215)', + ], + }, + { + github: 'kveld9', + since: '1.34.4', + contributions: [ + 'Spanish (es) translation — 964 strings (PR #159)', + 'Column-header sorting for albums & playlists (PR #160)', + 'Multi-select for albums, artists & playlists with bulk "Add to Playlist"; collapsible sidebar playlist section; infinite scroll on Artists page; "Remove from Playlist" in context menu (PR #168)', + '3 visual toggles: cover art background, playlist cover photo, show bitrate badge (PR #181)', + '8 community themes (AMOLED Black, Monochrome Dark, Amber Night, Phosphor Green, Midnight Blue, Rose Dark, Sepia Dark, Ice Blue) + waveform live theme update (PR #182)', + 'Favorites redesign: sortable columns, genre filter, age range filter, new columns (PR #184)', + 'Albums and playlist headers redesign with improved layout and theme integration (PR #186)', + 'Tracklist column picker overflow fix in AlbumTrackList (PR #188)', + 'Spotify CSV playlist import (PR #190)', + 'Context menu for songs in AdvancedSearch and SearchResults (PR #191)', + 'Tracklist column picker alignment and toggle fix across Favorites and PlaylistDetail (PR #192)', + 'CSV import: dynamic match threshold, cleaned title search, score display in report (PR #199)', + 'Discord Rich Presence: configurable text templates for details, state and album tooltip (PR #198)', + 'Click-to-toggle duration / remaining time in player bar with persisted preference (PR #212)', + 'Opt-in floating player bar with themed background, accent-colored border, rounded album art, and centered volume section (PR #216)', + 'Linux GPU-vendor auto-detection to configure the WebKitGTK DMA-BUF renderer (disabled on NVIDIA proprietary) (PR #217)', + 'Artist page: continue playback when starting top songs (PR #220)', + 'Floating player bar: scroll-padding fix (PR #221)', + 'Queue Panel — position counter, tri-state duration toggle (total/remaining/ETA), persistent Now Playing collapse, animated EQ indicator (PR #419)', + 'Community themes — redesign pass: removed 5 overlapping / eye-straining palettes, added 8 new dark themes (Obsidian Black, Carbon Grey, Volcanic Dark, Forest Green, Violet Haze, Copper Oxide, Sakura Night, Obsidian Gold) (PR #490)', + 'Customizable queue toolbar — drag-and-drop button reordering, per-button visibility toggles, optional separator, persisted across restarts (PR #534)', + ], + }, + { + github: 'nisrael', + since: '1.34.0', + contributions: [ + 'Nightfox.nvim theme group in Open Source Classics (PR #114)', + 'Switch reqwest to rustls-tls for cross-platform TLS (PR #112)', + 'ICY stream metadata & AzuraCast Now Playing support (PR #146)', + ], + }, + { + github: 'peri4ko', + since: '1.43.0', + contributions: [ + 'WebView2 idle hooks when Tauri windows are hidden — Windows GPU and compositor mitigation (PR #273)', + ], + }, + { + github: 'Sayykii', + since: '1.46.0', + contributions: [ + 'Discord Rich Presence: cover art from your own server (Subsonic getAlbumInfo2) with three-way picker — none / server / Apple Music (PR #462)', + 'Artist page: group albums by OpenSubsonic releaseTypes (Album / EP / Single / Compilation / Live / Soundtrack / Remix) with deterministic order and i18n section headers in all 8 locales (PR #471)', + ], + }, + { + github: 'Psychotoxical', + since: '1.0.0', + contributions: [ + 'Initial app scaffold — Tauri v2 + React + Zustand + Subsonic protocol, multi-server auth (v1.0)', + 'Rust/rodio audio engine replacing Howler.js (v1.2)', + 'Waveform seekbar, MilkDrop visualizer, EQ bars (v1.3)', + '10-band parametric equalizer with per-theme UI (v1.5)', + 'Replay Gain, Crossfade, Download Folder Modal (v1.6)', + 'Last.fm scrobbling, Similar Artists, Statistics page (v1.7)', + 'TooltipPortal + CustomSelect portal-based UI primitives (v1.7)', + 'Keybindings system + font picker (v1.9)', + 'Lyrics system with LRCLIB integration (v1.12)', + 'Queue management overhaul + DnD (v1.22)', + 'Advanced Search + Genre Mix overhaul (v1.23)', + 'Playlist Management — create/edit/cover upload, drag reorder (v1.24)', + 'Functional tray icon, Minimize to Tray, Sidebar customization (v1.25)', + 'Bulk Select, Song Info modal, Recently Played (v1.26)', + 'In-App Auto-Update + Configurable Home (v1.27)', + 'Infinite Queue + Start Radio + single-click play (v1.28)', + 'Internet Radio with fast-start, ICY metadata support (v1.29)', + 'Discord Rich Presence, offline bulk download, artist images, lazy loading (v1.30)', + 'AutoEQ integration, resizable tracklist columns (v1.31)', + 'Genre browser with filter + FLAC seek fix (v1.20)', + 'Fullscreen Player with dynamic accent color + mesh blobs (v1.34)', + 'Bit-perfect hi-res playback + underrun hardening (v1.34)', + 'Fullscreen lyrics overlay with FsArt crossfade (v1.34.1)', + 'Offline Mode (beta) + MPRIS seek (v1.18)', + 'NSIS Windows installer (v1.19)', + 'WCAG contrast audits across 60+ themes (v1.17-v1.34)', + 'Custom Linux title bar with now-playing display (v1.34)', + 'Device Sync — fixed cross-OS naming scheme + playlist folders (v1.40)', + 'macOS signing + notarization + Tauri auto-updater (v1.40)', + 'Mini player — floating window, custom titlebar, queue DnD, persistent geometry, keyboard shortcut, WebView2 lifecycle fix (PR #162, v1.42.x)', + '67 themes across 8 groups (Mediaplayer, OS, Games, Movies, Series, Social Media, OSS Classics, Psysonic)', + 'Admin-gated User Management tab with per-user library assignment (PR #222)', + 'Comprehensive mobile UI overhaul (PR #238)', + 'Runtime log levels and debug log export (PR #241)', + 'ReplayGain Auto mode — picks track vs album gain from queue context (PR #242)', + 'Now-Playing Info tab with artist bio, song credits, Bandsintown tour dates (PR #244)', + 'Performance suite — search cover cache, DeviceSync N+1, Albums prefetch, Lyrics IDB, bundle splitting, Artists memo, Genres pagination (PRs #245-#251)', + 'Artist page — user-configurable section visibility and order (PR #254)', + 'Album enqueue via cover hover, context menu, multi-select toolbar (PR #256)', + 'Settings refactor — thematic tab regroup, accordion sub-sections, in-page search (PR #259)', + 'Navidrome admin API hardening (PR #260)', + 'Now-Playing redesign as info dashboard + draggable widget cards (PRs #266, #267)', + 'Sleep timer — circular ring + in-button countdown (PR #272)', + 'Streaming seek UI freeze + snapback fix (PR #236)', + 'Windows: tighten WebView2 idle hooks (follow-up to #273) (PR #276)', + 'Audio: defer chained-track volume to gapless transition (PR #277)', + 'Mini-player: portal volume popover so it cannot get clipped (PR #279)', + 'Mini-player: drop saved position when its monitor is gone (PR #280)', + 'Toolbar: swap Gapless / Infinite Queue icons (closes #274) (PR #284)', + 'Linux audio: prefer PipeWire / Pulse aliases over raw ALSA default (PR #288)', + 'Playlists: bulk-delete button in selection-mode header (PR #290)', + 'Home: refresh Mainstage when active server changes (PR #291)', + 'Search: enqueue on live-search click + reposition context menu on right-click (PR #298)', + 'Tracks library hub page (closes #299) (PR #300)', + 'Home: Discover Songs rail in Mainstage (PR #301)', + 'Search: right-click context menu on artist and album rows (PR #302)', + 'Unified SongRow + paginated song results in search pages (PR #303)', + 'Orbit — Multi-User Listen-Together merged to main (PR #304)', + 'Genres: tag-cloud refactor — log-scaled colour-tinted pills replace icon cards (PR #311)', + 'imageCache: per-component object URLs to fix the blob: load flood (PR #313)', + 'Queue: preserve scroll context on manual click (PR #314)', + 'Seekbar: split waveform style into truewave (analysed) + pseudowave (deterministic) (PR #316)', + 'Settings: restructure Normalization section for clarity (PR #317)', + 'Cross-device resume: flush play-queue position on pause + all exit paths (PR #318)', + 'Login: language picker on the login page (PR #328)', + 'Playlists: confirm before re-adding songs already in the playlist (PR #329)', + 'Discord: debug-build logging for Rich Presence IPC path (PR #330)', + 'Player: persist queue panel visibility + drop dead loudness binding (PR #336)', + 'Playlists: suggestion-row preview UX (PR #365)', + 'macOS: Equalizer canvas blanking fix when laid-out dimensions are missing (PR #384)', + 'Themes: Kanagawa, Atom One and 1984 palettes; OSS Classics regrouped by family (PR #390)', + 'Rust track preview engine + player-bar preview indicator with smart stop semantics (PRs #392, #394)', + 'Tray: now-playing track tooltip + i18n menu labels (PR #395)', + 'Preview: audio start sync, ring animation, download timeout (PR #423)', + 'Statistics: shareable Top-Albums card export (PR #425)', + 'Windows: playback stutter under GPU load — MMCSS Pro Audio promotion + animation pause + reduce-animations toggle (PR #426)', + 'Audio: frame-align gapless-off track-separation silence (fixes mono-channel playback after natural track end) (PR #439)', + 'Settings: 3-state animation mode (Full / Reduced / Static) — replaces boolean reduce-animations toggle (PR #441)', + 'Tracks: Highly Rated rail and per-card star display, with cache layer for ndListSongs (PR #443)', + 'Random Mix: playlist-size picker (50/75/100/125/150) and filter-panel layout cleanup (PR #445)', + 'Queue: optional "Preserve Play Next order" toggle — multiple Play Next inserts queue up behind each other instead of latest-on-top (PR #464)', + 'Library: "favorites only" filter on Albums, Artists and Advanced Search — toolbar toggle reading star overrides live (PR #466)', + 'Settings: keep current active server when adding a new one — no more auto-switch interrupting playback or library context (PR #475)', + 'Help page: full rewrite with 45 focused entries across 10 themed sections (Getting Started / Playback & Queue / Audio Tools / Library & Discovery / Lyrics / Sharing & Social / Personalization / Power User / Offline & Sync / Integrations & Troubleshooting), in-page live search with case-insensitive substring matching and auto-expand on hits, translated to all 8 locales (PR #485)', + 'Library: Browse by Composer — native-API role listing for classical libraries, library-scoped queries, composer as a first-class share entity (PR #487)', + 'Home: "Because you listened" recommendation rail — Last.fm-anchored similar-artist surfacing with round-robin anchor rotation per server (PR #489)', + 'Song Info: absolute file path on Navidrome via native /api/song/{id} — Subsonic only ever returned a relative path (or none on Navidrome), the native endpoint surfaces the full server-side location (PR #504)', + 'Home: Lossless Albums rail + dedicated /lossless-albums page with infinite scroll and header parity (selection mode, enqueue, offline, download ZIPs), streaming load via per-fetch onProgress, sidebar entry default visible, detection via Navidrome native bit_depth-sorted song cursor with always-lossless suffix allowlist (PR #506)', + 'Accessibility: OpenDyslexic font option in the Settings picker — bundled locally via @fontsource/opendyslexic, asymmetric glyph shapes for easier b/d, p/q tracking, Latin-only with translated subtitle in all 8 locales calling out the dyslexia-friendly intent and the Cyrillic/CJK fallback (PR #507)', + ], + }, +] as const; + +export const MAINTAINERS = [ + { github: 'Psychotoxical' }, + { github: 'cucadmuh' }, +] as const; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 2776daff..163359e4 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -45,11 +45,16 @@ import { AddServerForm } from '../components/settings/AddServerForm'; import { ArtistLayoutCustomizer } from '../components/settings/ArtistLayoutCustomizer'; import { BackupSection } from '../components/settings/BackupSection'; import { HomeCustomizer } from '../components/settings/HomeCustomizer'; +import { LoudnessLufsButtonGroup } from '../components/settings/LoudnessLufsButtonGroup'; import { LyricsSourcesCustomizer } from '../components/settings/LyricsSourcesCustomizer'; import { QueueToolbarCustomizer } from '../components/settings/QueueToolbarCustomizer'; import { ServerGripHandle } from '../components/settings/ServerGripHandle'; +import { SETTINGS_INDEX, type Tab, matchScore, resolveTab } from '../components/settings/settingsTabs'; import { SidebarCustomizer } from '../components/settings/SidebarCustomizer'; import { UserManagementSection } from '../components/settings/UserManagementSection'; +import { CONTRIBUTORS, MAINTAINERS } from '../config/settingsCredits'; +import { buildAudioDeviceSelectOptions, formatAudioDeviceLabel, sortAudioDeviceIds } from '../utils/audioDeviceLabels'; +import { formatBytes, snapHotCacheMb } from '../utils/formatBytes'; import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic'; import { ndLogin } from '../api/navidromeAdmin'; import { switchActiveServer } from '../utils/switchActiveServer'; @@ -65,526 +70,6 @@ const AUDIOBOOK_GENRES_DISPLAY = ['Hörbuch', 'Hoerbuch', 'Hörspiel', 'Hoerspie const AUDIOMUSE_NV_PLUGIN_URL = 'https://github.com/NeptuneHub/AudioMuse-AI-NV-plugin'; -const LOUDNESS_LUFS_BUTTON_ORDER: LoudnessLufsPreset[] = [-10, -12, -14, -16]; - -function LoudnessLufsButtonGroup(props: { - value: LoudnessLufsPreset; - onSelect: (v: LoudnessLufsPreset) => void; -}) { - return ( -
- {LOUDNESS_LUFS_BUTTON_ORDER.map(v => ( - - ))} -
- ); -} - -const CONTRIBUTORS = [ - { - github: 'jiezhuo', - since: '1.21', - contributions: [ - 'Chinese (Simplified) translation', - ], - }, - { - github: 'nullobject', - since: '1.22.0', - contributions: [ - 'Seek debounce & race condition fix (PR #7)', - 'Waveform seekbar stability on position update (PR #8)', - ], - }, - { - github: 'trbn1', - since: '1.22.0', - contributions: [ - 'Replay Gain metadata propagation (PR #9)', - 'songToTrack() — unified track construction across all sources', - ], - }, - { - github: 'nisarg-78', - since: '1.29.0', - contributions: [ - 'Click-to-seek in synced lyrics (PR #38)', - 'Volume scroll wheel on volume slider (PR #38)', - 'Lyrics line visual states: active / completed / upcoming (PR #38)', - 'Queue auto-scroll to keep upcoming tracks in view; fixed re-renders from currentTime in QueuePanel (PR #115)', - ], - }, - { - github: 'JulianNymark', - since: '1.29.0', - contributions: [ - 'OGG/Vorbis container support via symphonia-format-ogg (PR #42)', - 'Themed toast notifications for audio playback errors (PR #43)', - 'Human-readable audio error messages (PR #44)', - ], - }, - { - github: 'zz5zz', - since: '1.32.0', - contributions: [ - 'Norwegian (Bokmål) translation (PR #101)', - ], - }, - { - github: 'cucadmuh', - since: '1.33.0', - contributions: [ - 'Russian translation & i18n locale split (PR #106)', - 'Russian locale refinements using phrasing from ru2 (PR #113)', - 'Gapless manual skip: honor user-initiated play over pre-chained track (PR #119)', - 'Hot playback cache — queue prefetch (PR #123)', - 'Per-server music folder filter and sidebar library picker (PR #124, PR #125)', - 'Richer star ratings, skip threshold, and library filtering (PR #130)', - 'Statistics: scope album and song totals to selected music library (PR #138)', - 'AudioMuse-AI discovery integration for Navidrome (PR #147)', - 'Hot playback cache — eviction budgeting, grace period, and live Audio settings readout (PR #153)', - 'Folder Browser: keyboard navigation, context menus, now-playing path emphasis, and adaptive column layout (PR #158)', - 'Infinite queue: artist-driven candidates via Top Songs + Similar Songs with random fallback (PR #163)', - 'Folder Browser: per-column filter with keyboard flow and Shift+Enter queue append (PR #165)', - 'Keybindings: modifier + key chords for in-app shortcuts; fixed seek ±10s units (PR #167)', - 'Statistics: genre insights scoped to music library, cached Subsonic fetches, localized duration formatting (PR #144)', - 'Audio output device picker: clearer ALSA labels, duplicate disambiguation, system-default mark, live refresh (PR #173)', - 'Folder Browser: arrow navigation blocked when modifier keys are held (PR #174)', - 'Linux audio output device picker: stable watcher (disable false enumeration-miss resets), canonicalize ALSA name drift, ghost entry for unlisted device (PR #176)', - 'Opus audio playback via symphonia-adapter-libopus with bundled libopus (PR #183)', - 'CLI player controls with D-Bus forwarding, shell completions for bash/zsh, library/audio-device/instant-mix CLI commands, active server switcher in header (PR #187)', - 'Streaming playback stability: stream-first start, seek recovery, crossfade/gapless backup preload, hot-cache promotion (PR #200)', - 'ReplayGain values in Queue tech strip (PR #196)', - 'Playback source badge (offline / cache / stream) in Queue tech strip (PR #201)', - 'WebKitGTK wheel scroll mode: smooth (kinetic) default with optional linear toggle (PR #207)', - 'ArtistCardLocal i18n: use plural-aware artists.albumCount key instead of hardcoded German', - 'NixOS / flake install guide with Cachix setup (PR #209)', - 'Subsonic: align Rust HTTP UA with main WebView UA (PR #235)', - 'UI: overlay scrollbars, resizer hit-test, and Linux mini-wheel (PR #255)', - 'Server invites: magic-string paste, Navidrome admin share, add-user validation (PR #258)', - 'Library deep links (psysonic2 scheme) — paste track/album/artist/queue (PR #261)', - 'Linux: stop Wayland GTK drag proxy and PsyDnD ghost (PR #268)', - 'Sidebar: long-press drag to reorder and hide nav items (PR #269)', - 'Lucky Mix — instant queue from listening history, ratings, and AudioMuse similar tracks (PR #278)', - 'UI perf: fix spike when medulla-perch lines up with hair-fan gestures (PR #283)', - 'Navidrome smart playlists workflow in the Playlists page (PR #289)', - 'Loudness normalization — LUFS / EBU R128 analysis cache + persistent waveform store (PR #315)', - 'Queue resize handle: no longer blocked by main scroll hit-test (PR #324)', - 'LUFS loop stabilization, serialized CPU seed work, mixed mean/peak waveform bins (PR #326)', - 'Queue undo/redo — hotkeys, playback-aware restore, scroll restoration (PR #331)', - 'Lucky Mix + mix rating filter (Navidrome / OpenSubsonic) (PR #332)', - 'Loudness: target sync, effective pre-analysis trim, queue/settings copy (PR #333)', - 'CLI logs mode with tail/follow and shell completions (PR #337)', - 'Player: reserve preview row space in delay modal (PR #344)', - 'Infinite queue: respect rating filter in top-up (PR #357)', - 'NixOS install guide refresh for channels and CI (PR #380)', - 'UI refinements — sidebar discovery indicators, adaptive header, interaction polish (PR #397)', - 'Queue: drag rows outside to remove with trash ghost (PR #420)', - 'Tauri: modularize audio and lib command layers (PR #422)', - 'Shortcuts: action registry, dynamic CLI help, 9 new input targets + F1 help binding (PR #435)', - 'Environment upgrade & hot-cache playback — replay via RAM/disk on same-track and queue-end resume, playback-source icon stays correct after resume/undo/gapless, sidebar new-releases 500-id cap merge, Windows tray double-click fix, lazy-loaded routes, rodio 0.22 migration (PR #463)', - 'Audio: post-sleep stream recovery (Windows + Linux) with poll-gap-armed stall watchdog; preview seekbar freeze + anti-jump on preview end; remove card hover lift and per-card GPU compositing hints (PR #476)', - 'Analysis queue control: prune stale http-backfill / cpu-seed jobs when tracks leave the playback queue, cap loudness backfill warmup to current + next 5 tracks, plus debug counters for diagnostics (PR #480)', - ], - }, - { - github: 'kilyabin', - since: '1.34.0', - contributions: [ - 'Russian locale improvements (PR #107, PR #120)', - 'Auto-install script for Debian / RHEL (PR #121)', - 'Album cover art in Discord Rich Presence via iTunes API (PR #111)', - 'Tiling WM detection: hide custom TitleBar on Hyprland/Sway/i3/etc. (PR #134)', - 'Russian translation: lyricsServerFirst settings strings (PR #140)', - 'Russian translation refinements (PR #148)', - 'Merge Random Mix & Albums into a single Build a Mix hub (PR #155)', - 'Fullscreen player: software-rendering performance fixes + portrait toggle & dimming setting (PR #156)', - 'Fullscreen player: stop mesh blob and portrait animations in no-compositing mode; remove seekbar box-shadow repaint (PR #175)', - 'Apple Music-style scrolling lyrics with spring-physics scroll for fullscreen player and sidebar; per-style controls (PR #205)', - 'Golos Text and Unbounded fonts with Cyrillic support (PR #206)', - 'Fullscreen & sidebar lyrics: duration-based ease-out scroll animator replacing spring physics; bottom fade for plain lyrics (PR #214)', - 'Sidebar lyrics: YouLy+ source strings render in a single line (PR #215)', - ], - }, - { - github: 'kveld9', - since: '1.34.4', - contributions: [ - 'Spanish (es) translation — 964 strings (PR #159)', - 'Column-header sorting for albums & playlists (PR #160)', - 'Multi-select for albums, artists & playlists with bulk "Add to Playlist"; collapsible sidebar playlist section; infinite scroll on Artists page; "Remove from Playlist" in context menu (PR #168)', - '3 visual toggles: cover art background, playlist cover photo, show bitrate badge (PR #181)', - '8 community themes (AMOLED Black, Monochrome Dark, Amber Night, Phosphor Green, Midnight Blue, Rose Dark, Sepia Dark, Ice Blue) + waveform live theme update (PR #182)', - 'Favorites redesign: sortable columns, genre filter, age range filter, new columns (PR #184)', - 'Albums and playlist headers redesign with improved layout and theme integration (PR #186)', - 'Tracklist column picker overflow fix in AlbumTrackList (PR #188)', - 'Spotify CSV playlist import (PR #190)', - 'Context menu for songs in AdvancedSearch and SearchResults (PR #191)', - 'Tracklist column picker alignment and toggle fix across Favorites and PlaylistDetail (PR #192)', - 'CSV import: dynamic match threshold, cleaned title search, score display in report (PR #199)', - 'Discord Rich Presence: configurable text templates for details, state and album tooltip (PR #198)', - 'Click-to-toggle duration / remaining time in player bar with persisted preference (PR #212)', - 'Opt-in floating player bar with themed background, accent-colored border, rounded album art, and centered volume section (PR #216)', - 'Linux GPU-vendor auto-detection to configure the WebKitGTK DMA-BUF renderer (disabled on NVIDIA proprietary) (PR #217)', - 'Artist page: continue playback when starting top songs (PR #220)', - 'Floating player bar: scroll-padding fix (PR #221)', - 'Queue Panel — position counter, tri-state duration toggle (total/remaining/ETA), persistent Now Playing collapse, animated EQ indicator (PR #419)', - 'Community themes — redesign pass: removed 5 overlapping / eye-straining palettes, added 8 new dark themes (Obsidian Black, Carbon Grey, Volcanic Dark, Forest Green, Violet Haze, Copper Oxide, Sakura Night, Obsidian Gold) (PR #490)', - 'Customizable queue toolbar — drag-and-drop button reordering, per-button visibility toggles, optional separator, persisted across restarts (PR #534)', - ], - }, - { - github: 'nisrael', - since: '1.34.0', - contributions: [ - 'Nightfox.nvim theme group in Open Source Classics (PR #114)', - 'Switch reqwest to rustls-tls for cross-platform TLS (PR #112)', - 'ICY stream metadata & AzuraCast Now Playing support (PR #146)', - ], - }, - { - github: 'peri4ko', - since: '1.43.0', - contributions: [ - 'WebView2 idle hooks when Tauri windows are hidden — Windows GPU and compositor mitigation (PR #273)', - ], - }, - { - github: 'Sayykii', - since: '1.46.0', - contributions: [ - 'Discord Rich Presence: cover art from your own server (Subsonic getAlbumInfo2) with three-way picker — none / server / Apple Music (PR #462)', - 'Artist page: group albums by OpenSubsonic releaseTypes (Album / EP / Single / Compilation / Live / Soundtrack / Remix) with deterministic order and i18n section headers in all 8 locales (PR #471)', - ], - }, - { - github: 'Psychotoxical', - since: '1.0.0', - contributions: [ - 'Initial app scaffold — Tauri v2 + React + Zustand + Subsonic protocol, multi-server auth (v1.0)', - 'Rust/rodio audio engine replacing Howler.js (v1.2)', - 'Waveform seekbar, MilkDrop visualizer, EQ bars (v1.3)', - '10-band parametric equalizer with per-theme UI (v1.5)', - 'Replay Gain, Crossfade, Download Folder Modal (v1.6)', - 'Last.fm scrobbling, Similar Artists, Statistics page (v1.7)', - 'TooltipPortal + CustomSelect portal-based UI primitives (v1.7)', - 'Keybindings system + font picker (v1.9)', - 'Lyrics system with LRCLIB integration (v1.12)', - 'Queue management overhaul + DnD (v1.22)', - 'Advanced Search + Genre Mix overhaul (v1.23)', - 'Playlist Management — create/edit/cover upload, drag reorder (v1.24)', - 'Functional tray icon, Minimize to Tray, Sidebar customization (v1.25)', - 'Bulk Select, Song Info modal, Recently Played (v1.26)', - 'In-App Auto-Update + Configurable Home (v1.27)', - 'Infinite Queue + Start Radio + single-click play (v1.28)', - 'Internet Radio with fast-start, ICY metadata support (v1.29)', - 'Discord Rich Presence, offline bulk download, artist images, lazy loading (v1.30)', - 'AutoEQ integration, resizable tracklist columns (v1.31)', - 'Genre browser with filter + FLAC seek fix (v1.20)', - 'Fullscreen Player with dynamic accent color + mesh blobs (v1.34)', - 'Bit-perfect hi-res playback + underrun hardening (v1.34)', - 'Fullscreen lyrics overlay with FsArt crossfade (v1.34.1)', - 'Offline Mode (beta) + MPRIS seek (v1.18)', - 'NSIS Windows installer (v1.19)', - 'WCAG contrast audits across 60+ themes (v1.17-v1.34)', - 'Custom Linux title bar with now-playing display (v1.34)', - 'Device Sync — fixed cross-OS naming scheme + playlist folders (v1.40)', - 'macOS signing + notarization + Tauri auto-updater (v1.40)', - 'Mini player — floating window, custom titlebar, queue DnD, persistent geometry, keyboard shortcut, WebView2 lifecycle fix (PR #162, v1.42.x)', - '67 themes across 8 groups (Mediaplayer, OS, Games, Movies, Series, Social Media, OSS Classics, Psysonic)', - 'Admin-gated User Management tab with per-user library assignment (PR #222)', - 'Comprehensive mobile UI overhaul (PR #238)', - 'Runtime log levels and debug log export (PR #241)', - 'ReplayGain Auto mode — picks track vs album gain from queue context (PR #242)', - 'Now-Playing Info tab with artist bio, song credits, Bandsintown tour dates (PR #244)', - 'Performance suite — search cover cache, DeviceSync N+1, Albums prefetch, Lyrics IDB, bundle splitting, Artists memo, Genres pagination (PRs #245-#251)', - 'Artist page — user-configurable section visibility and order (PR #254)', - 'Album enqueue via cover hover, context menu, multi-select toolbar (PR #256)', - 'Settings refactor — thematic tab regroup, accordion sub-sections, in-page search (PR #259)', - 'Navidrome admin API hardening (PR #260)', - 'Now-Playing redesign as info dashboard + draggable widget cards (PRs #266, #267)', - 'Sleep timer — circular ring + in-button countdown (PR #272)', - 'Streaming seek UI freeze + snapback fix (PR #236)', - 'Windows: tighten WebView2 idle hooks (follow-up to #273) (PR #276)', - 'Audio: defer chained-track volume to gapless transition (PR #277)', - 'Mini-player: portal volume popover so it cannot get clipped (PR #279)', - 'Mini-player: drop saved position when its monitor is gone (PR #280)', - 'Toolbar: swap Gapless / Infinite Queue icons (closes #274) (PR #284)', - 'Linux audio: prefer PipeWire / Pulse aliases over raw ALSA default (PR #288)', - 'Playlists: bulk-delete button in selection-mode header (PR #290)', - 'Home: refresh Mainstage when active server changes (PR #291)', - 'Search: enqueue on live-search click + reposition context menu on right-click (PR #298)', - 'Tracks library hub page (closes #299) (PR #300)', - 'Home: Discover Songs rail in Mainstage (PR #301)', - 'Search: right-click context menu on artist and album rows (PR #302)', - 'Unified SongRow + paginated song results in search pages (PR #303)', - 'Orbit — Multi-User Listen-Together merged to main (PR #304)', - 'Genres: tag-cloud refactor — log-scaled colour-tinted pills replace icon cards (PR #311)', - 'imageCache: per-component object URLs to fix the blob: load flood (PR #313)', - 'Queue: preserve scroll context on manual click (PR #314)', - 'Seekbar: split waveform style into truewave (analysed) + pseudowave (deterministic) (PR #316)', - 'Settings: restructure Normalization section for clarity (PR #317)', - 'Cross-device resume: flush play-queue position on pause + all exit paths (PR #318)', - 'Login: language picker on the login page (PR #328)', - 'Playlists: confirm before re-adding songs already in the playlist (PR #329)', - 'Discord: debug-build logging for Rich Presence IPC path (PR #330)', - 'Player: persist queue panel visibility + drop dead loudness binding (PR #336)', - 'Playlists: suggestion-row preview UX (PR #365)', - 'macOS: Equalizer canvas blanking fix when laid-out dimensions are missing (PR #384)', - 'Themes: Kanagawa, Atom One and 1984 palettes; OSS Classics regrouped by family (PR #390)', - 'Rust track preview engine + player-bar preview indicator with smart stop semantics (PRs #392, #394)', - 'Tray: now-playing track tooltip + i18n menu labels (PR #395)', - 'Preview: audio start sync, ring animation, download timeout (PR #423)', - 'Statistics: shareable Top-Albums card export (PR #425)', - 'Windows: playback stutter under GPU load — MMCSS Pro Audio promotion + animation pause + reduce-animations toggle (PR #426)', - 'Audio: frame-align gapless-off track-separation silence (fixes mono-channel playback after natural track end) (PR #439)', - 'Settings: 3-state animation mode (Full / Reduced / Static) — replaces boolean reduce-animations toggle (PR #441)', - 'Tracks: Highly Rated rail and per-card star display, with cache layer for ndListSongs (PR #443)', - 'Random Mix: playlist-size picker (50/75/100/125/150) and filter-panel layout cleanup (PR #445)', - 'Queue: optional "Preserve Play Next order" toggle — multiple Play Next inserts queue up behind each other instead of latest-on-top (PR #464)', - 'Library: "favorites only" filter on Albums, Artists and Advanced Search — toolbar toggle reading star overrides live (PR #466)', - 'Settings: keep current active server when adding a new one — no more auto-switch interrupting playback or library context (PR #475)', - 'Help page: full rewrite with 45 focused entries across 10 themed sections (Getting Started / Playback & Queue / Audio Tools / Library & Discovery / Lyrics / Sharing & Social / Personalization / Power User / Offline & Sync / Integrations & Troubleshooting), in-page live search with case-insensitive substring matching and auto-expand on hits, translated to all 8 locales (PR #485)', - 'Library: Browse by Composer — native-API role listing for classical libraries, library-scoped queries, composer as a first-class share entity (PR #487)', - 'Home: "Because you listened" recommendation rail — Last.fm-anchored similar-artist surfacing with round-robin anchor rotation per server (PR #489)', - 'Song Info: absolute file path on Navidrome via native /api/song/{id} — Subsonic only ever returned a relative path (or none on Navidrome), the native endpoint surfaces the full server-side location (PR #504)', - 'Home: Lossless Albums rail + dedicated /lossless-albums page with infinite scroll and header parity (selection mode, enqueue, offline, download ZIPs), streaming load via per-fetch onProgress, sidebar entry default visible, detection via Navidrome native bit_depth-sorted song cursor with always-lossless suffix allowlist (PR #506)', - 'Accessibility: OpenDyslexic font option in the Settings picker — bundled locally via @fontsource/opendyslexic, asymmetric glyph shapes for easier b/d, p/q tracking, Latin-only with translated subtitle in all 8 locales calling out the dyslexia-friendly intent and the Cyrillic/CJK fallback (PR #507)', - ], - }, -] as const; - -const MAINTAINERS = [ - { github: 'Psychotoxical' }, - { github: 'cucadmuh' }, -] as const; - -type Tab = - | 'library' - | 'servers' - | 'audio' - | 'lyrics' - | 'appearance' - | 'personalisation' - | 'integrations' - | 'input' - | 'storage' - | 'system' - | 'users'; - -// Legacy Tab-IDs die via Route-State oder persisted State noch aufschlagen koennen -// auf die neue Struktur mappen. Gibt es keinen Match, faellt die Settings-Page -// einfach auf 'library' zurueck. -const LEGACY_TAB_ALIAS: Record = { - general: 'library', - server: 'servers', -}; - -function resolveTab(input: string | undefined | null): Tab { - if (!input) return 'servers'; - const aliased = LEGACY_TAB_ALIAS[input]; - if (aliased) return aliased; - const known: Tab[] = ['library', 'servers', 'audio', 'lyrics', 'appearance', 'personalisation', 'integrations', 'input', 'storage', 'system', 'users']; - return (known as string[]).includes(input) ? (input as Tab) : 'servers'; -} - -// Statischer Suchindex ueber alle Sub-Sections aller Tabs. Mitpflegen, wenn eine -// neue SettingsSubSection hinzukommt — sonst taucht sie nicht in der Suche auf. -type SearchIndexEntry = { tab: Tab; titleKey: string; keywords?: string }; -const SETTINGS_INDEX: SearchIndexEntry[] = [ - { tab: 'audio', titleKey: 'settings.audioOutputDevice', keywords: 'output device speakers headphones alsa wasapi coreaudio' }, - { tab: 'audio', titleKey: 'settings.hiResTitle', keywords: 'hi-res hires resampling bit depth sample rate dsd 24bit' }, - { tab: 'audio', titleKey: 'settings.eqTitle', keywords: 'equalizer eq bass treble autoeq filter pre-gain' }, - { tab: 'audio', titleKey: 'settings.playbackTitle', keywords: 'playback crossfade gapless replaygain replay gain volume' }, - { tab: 'lyrics', titleKey: 'settings.lyricsSourcesTitle', keywords: 'lyrics sources providers lrclib netease server youlyplus karaoke standard static' }, - { tab: 'lyrics', titleKey: 'settings.sidebarLyricsStyle', keywords: 'lyrics scroll style classic apple music' }, - { tab: 'integrations', titleKey: 'settings.lfmTitle', keywords: 'last.fm lastfm scrobble' }, - { tab: 'integrations', titleKey: 'settings.discordRichPresence', keywords: 'discord rich presence rpc' }, - { tab: 'integrations', titleKey: 'settings.enableBandsintown', keywords: 'bandsintown concerts tours events' }, - { tab: 'integrations', titleKey: 'settings.nowPlayingEnabled', keywords: 'now playing share dropdown presence' }, - { tab: 'personalisation',titleKey: 'settings.sidebarTitle', keywords: 'sidebar nav navigation items reorder customize' }, - { tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' }, - { tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'home page customize sections' }, - { tab: 'personalisation',titleKey: 'settings.queueToolbarTitle', keywords: 'queue toolbar buttons reorder customize shuffle save load' }, - { tab: 'library', titleKey: 'settings.randomMixTitle', keywords: 'random mix blacklist genre keywords filter audiobook' }, - { tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' }, - { tab: 'storage', titleKey: 'settings.offlineDirTitle', keywords: 'offline library download directory folder cache' }, - { tab: 'storage', titleKey: 'settings.nextTrackBufferingTitle', keywords: 'next track buffering preload hot cache streaming' }, - { tab: 'storage', titleKey: 'settings.downloadsTitle', keywords: 'downloads zip export archive folder' }, - { tab: 'appearance', titleKey: 'settings.theme', keywords: 'theme color palette dark light' }, - { tab: 'appearance', titleKey: 'settings.themeSchedulerTitle', keywords: 'theme scheduler auto time dark mode sunset' }, - { tab: 'appearance', titleKey: 'settings.visualOptionsTitle', keywords: 'visual options animations effects titlebar mini player' }, - { tab: 'appearance', titleKey: 'settings.uiScaleTitle', keywords: 'ui scale zoom dpi size' }, - { tab: 'appearance', titleKey: 'settings.font', keywords: 'font typography typeface' }, - { tab: 'appearance', titleKey: 'settings.fsPlayerSection', keywords: 'fullscreen player mesh blob' }, - { tab: 'appearance', titleKey: 'settings.seekbarStyle', keywords: 'seekbar progress bar waveform reduced animations performance gpu fps low-end framerate cap' }, - { tab: 'input', titleKey: 'settings.inputKeybindingsTitle', keywords: 'keybindings shortcuts hotkeys keyboard' }, - { tab: 'input', titleKey: 'settings.globalShortcutsTitle', keywords: 'global shortcuts hotkeys system-wide media keys' }, - { tab: 'system', titleKey: 'settings.language', keywords: 'language locale translation i18n' }, - { tab: 'system', titleKey: 'settings.behavior', keywords: 'behavior tray minimize close start smooth scroll linux' }, - { tab: 'system', titleKey: 'settings.backupTitle', keywords: 'backup export import settings restore' }, - { tab: 'system', titleKey: 'settings.loggingTitle', keywords: 'log logs diagnostic debug verbose' }, - { tab: 'system', titleKey: 'settings.aboutTitle', keywords: 'about version update changelog release notes' }, - { tab: 'system', titleKey: 'settings.aboutContributorsLabel', keywords: 'contributors credits maintainers' }, - { tab: 'system', titleKey: 'licenses.title', keywords: 'licenses license open source attribution copyright third party dependencies oss' }, -]; - -// Substring-first, Fuzzy-Fallback (alle Query-Zeichen in Reihenfolge im -// Haystack). Rueckgabe 0 = kein Match. Hoeher = besser. -function matchScore(haystack: string, needle: string): number { - if (!needle) return 0; - const h = haystack.toLowerCase(); - const n = needle.toLowerCase(); - const idx = h.indexOf(n); - if (idx >= 0) return 1000 - Math.min(999, idx); - let hi = 0; - for (const ch of n) { - const j = h.indexOf(ch, hi); - if (j < 0) return 0; - hi = j + 1; - } - return Math.max(1, 100 - Math.min(99, hi - n.length)); -} - - -function formatBytes(bytes: number): string { - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; - if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; - return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`; -} - -/** Align hot-cache size slider (step 32 MB) to valid values. */ -function snapHotCacheMb(v: number): number { - const x = Math.min(20000, Math.max(32, Math.round(v))); - return Math.round((x - 32) / 32) * 32 + 32; -} - -/** Makes raw ALSA device names more readable on Linux. - * Values are kept as-is (rodio needs the ALSA name); only the displayed label is cleaned. - * e.g. "sysdefault:CARD=U192k" → "U192k" - * "hw:CARD=U192k,DEV=0" → "U192k (hw · PCM 0)" - * "hdmi:CARD=NVidia,DEV=1" → "NVidia (HDMI · DEV 1)" (same DEV as in ALSA string) - * "iec958:CARD=PCH,DEV=0" → "PCH (S/PDIF)" - * Names without ALSA prefix (pipewire, pulse, default…) are returned unchanged. */ -function formatAudioDeviceLabel(name: string): string { - const cardMatch = name.match(/CARD=([^,]+)/); - if (!cardMatch) return name; - const card = cardMatch[1]; - const devM = name.match(/DEV=(\d+)/); - const devNum = devM ? parseInt(devM[1], 10) : null; - const subM = name.match(/SUBDEV=(\d+)/); - const subNum = subM ? parseInt(subM[1], 10) : null; - - if (name.startsWith('iec958:')) return `${card} (S/PDIF)`; - if (name.startsWith('hdmi:')) { - const d = devNum !== null ? devNum : 0; - return `${card} (HDMI · DEV ${d})`; - } - if (name.startsWith('sysdefault:')) { - if (devNum !== null && devNum > 0) return `${card} (default · PCM ${devNum})`; - return card; - } - if (name.startsWith('plughw:')) { - if (devNum !== null) { - const sub = subNum !== null ? ` · sub ${subNum}` : ''; - return `${card} (plug · PCM ${devNum}${sub})`; - } - return card; - } - if (name.startsWith('hw:')) { - if (devNum !== null) { - const sub = subNum !== null ? ` · sub ${subNum}` : ''; - return `${card} (hw · PCM ${devNum}${sub})`; - } - return `${card} (hw)`; - } - if (name.startsWith('front:')) return `${card} (Front)`; - if (name.startsWith('surround')) return `${card} (${name.split(':')[0]})`; - // Other ALSA iface:card,dev — show plugin + PCM so identical cards differ - const iface = name.split(':')[0]; - if (iface && !['default', 'pulse', 'pipewire'].includes(iface)) { - if (devNum !== null) return `${card} (${iface} · PCM ${devNum})`; - return `${card} (${iface})`; - } - return card; -} - -/** Readable tail when two devices still share the same label (rare after formatAudioDeviceLabel). */ -function audioDeviceDuplicateHint(raw: string): string { - const cardM = raw.match(/CARD=([^,]+)/); - const devM = raw.match(/DEV=(\d+)/); - const subM = raw.match(/SUBDEV=(\d+)/); - const iface = raw.split(':')[0] || ''; - const parts: string[] = []; - if (iface) parts.push(iface); - if (cardM) parts.push(cardM[1]); - if (devM) parts.push(`PCM ${devM[1]}`); - if (subM) parts.push(`sub ${subM[1]}`); - if (parts.length > 1) return parts.join(' · '); - return raw.length > 56 ? `…${raw.slice(-53)}` : raw; -} - -/** When several devices share the same display label, append a disambiguator. */ -function disambiguatedAudioDeviceLabel(raw: string, baseLabel: string, duplicateBase: boolean): string { - if (!duplicateBase) return baseLabel; - return `${baseLabel} · ${audioDeviceDuplicateHint(raw)}`; -} - -/** cpal order is arbitrary; sort by readable label, current OS default first. */ -function sortAudioDeviceIds(devices: string[], osDefaultDeviceId: string | null): string[] { - return [...devices].sort((a, b) => { - const aDef = osDefaultDeviceId && a === osDefaultDeviceId; - const bDef = osDefaultDeviceId && b === osDefaultDeviceId; - if (aDef !== bDef) return aDef ? -1 : 1; - const la = formatAudioDeviceLabel(a); - const lb = formatAudioDeviceLabel(b); - const byLabel = la.localeCompare(lb, undefined, { sensitivity: 'base' }); - if (byLabel !== 0) return byLabel; - return a.localeCompare(b); - }); -} - -function buildAudioDeviceSelectOptions( - devices: string[], - defaultLabel: string, - osDefaultDeviceId: string | null, - osDefaultMark: string, - pinnedDevice: string | null, - notInListSuffix: string, -): { value: string; label: string }[] { - const baseLabels = devices.map(formatAudioDeviceLabel); - const countByBase = new Map(); - for (const b of baseLabels) countByBase.set(b, (countByBase.get(b) ?? 0) + 1); - const pinned = pinnedDevice?.trim() || null; - const pinnedNotListed = !!(pinned && !devices.includes(pinned)); - const ghost: { value: string; label: string }[] = pinnedNotListed - ? (() => { - const base = formatAudioDeviceLabel(pinned); - let label = `${base} · ${notInListSuffix}`; - if (osDefaultDeviceId && pinned === osDefaultDeviceId) label = `${label} · ${osDefaultMark}`; - return [{ value: pinned, label }]; - })() - : []; - return [ - { value: '', label: defaultLabel }, - ...ghost, - ...devices.map((d, i) => { - const base = baseLabels[i]; - const dup = (countByBase.get(base) ?? 0) > 1; - let label = disambiguatedAudioDeviceLabel(d, base, dup); - if (osDefaultDeviceId && d === osDefaultDeviceId) label = `${label} · ${osDefaultMark}`; - return { value: d, label }; - }), - ]; -} export default function Settings() { const auth = useAuthStore(); diff --git a/src/utils/audioDeviceLabels.ts b/src/utils/audioDeviceLabels.ts new file mode 100644 index 00000000..4954ed66 --- /dev/null +++ b/src/utils/audioDeviceLabels.ts @@ -0,0 +1,118 @@ +/** Makes raw ALSA device names more readable on Linux. + * Values are kept as-is (rodio needs the ALSA name); only the displayed label is cleaned. + * e.g. "sysdefault:CARD=U192k" → "U192k" + * "hw:CARD=U192k,DEV=0" → "U192k (hw · PCM 0)" + * "hdmi:CARD=NVidia,DEV=1" → "NVidia (HDMI · DEV 1)" (same DEV as in ALSA string) + * "iec958:CARD=PCH,DEV=0" → "PCH (S/PDIF)" + * Names without ALSA prefix (pipewire, pulse, default…) are returned unchanged. */ +export function formatAudioDeviceLabel(name: string): string { + const cardMatch = name.match(/CARD=([^,]+)/); + if (!cardMatch) return name; + const card = cardMatch[1]; + const devM = name.match(/DEV=(\d+)/); + const devNum = devM ? parseInt(devM[1], 10) : null; + const subM = name.match(/SUBDEV=(\d+)/); + const subNum = subM ? parseInt(subM[1], 10) : null; + + if (name.startsWith('iec958:')) return `${card} (S/PDIF)`; + if (name.startsWith('hdmi:')) { + const d = devNum !== null ? devNum : 0; + return `${card} (HDMI · DEV ${d})`; + } + if (name.startsWith('sysdefault:')) { + if (devNum !== null && devNum > 0) return `${card} (default · PCM ${devNum})`; + return card; + } + if (name.startsWith('plughw:')) { + if (devNum !== null) { + const sub = subNum !== null ? ` · sub ${subNum}` : ''; + return `${card} (plug · PCM ${devNum}${sub})`; + } + return card; + } + if (name.startsWith('hw:')) { + if (devNum !== null) { + const sub = subNum !== null ? ` · sub ${subNum}` : ''; + return `${card} (hw · PCM ${devNum}${sub})`; + } + return `${card} (hw)`; + } + if (name.startsWith('front:')) return `${card} (Front)`; + if (name.startsWith('surround')) return `${card} (${name.split(':')[0]})`; + // Other ALSA iface:card,dev — show plugin + PCM so identical cards differ + const iface = name.split(':')[0]; + if (iface && !['default', 'pulse', 'pipewire'].includes(iface)) { + if (devNum !== null) return `${card} (${iface} · PCM ${devNum})`; + return `${card} (${iface})`; + } + return card; +} + +/** Readable tail when two devices still share the same label (rare after formatAudioDeviceLabel). */ +export function audioDeviceDuplicateHint(raw: string): string { + const cardM = raw.match(/CARD=([^,]+)/); + const devM = raw.match(/DEV=(\d+)/); + const subM = raw.match(/SUBDEV=(\d+)/); + const iface = raw.split(':')[0] || ''; + const parts: string[] = []; + if (iface) parts.push(iface); + if (cardM) parts.push(cardM[1]); + if (devM) parts.push(`PCM ${devM[1]}`); + if (subM) parts.push(`sub ${subM[1]}`); + if (parts.length > 1) return parts.join(' · '); + return raw.length > 56 ? `…${raw.slice(-53)}` : raw; +} + +/** When several devices share the same display label, append a disambiguator. */ +export function disambiguatedAudioDeviceLabel(raw: string, baseLabel: string, duplicateBase: boolean): string { + if (!duplicateBase) return baseLabel; + return `${baseLabel} · ${audioDeviceDuplicateHint(raw)}`; +} + +/** cpal order is arbitrary; sort by readable label, current OS default first. */ +export function sortAudioDeviceIds(devices: string[], osDefaultDeviceId: string | null): string[] { + return [...devices].sort((a, b) => { + const aDef = osDefaultDeviceId && a === osDefaultDeviceId; + const bDef = osDefaultDeviceId && b === osDefaultDeviceId; + if (aDef !== bDef) return aDef ? -1 : 1; + const la = formatAudioDeviceLabel(a); + const lb = formatAudioDeviceLabel(b); + const byLabel = la.localeCompare(lb, undefined, { sensitivity: 'base' }); + if (byLabel !== 0) return byLabel; + return a.localeCompare(b); + }); +} + +export function buildAudioDeviceSelectOptions( + devices: string[], + defaultLabel: string, + osDefaultDeviceId: string | null, + osDefaultMark: string, + pinnedDevice: string | null, + notInListSuffix: string, +): { value: string; label: string }[] { + const baseLabels = devices.map(formatAudioDeviceLabel); + const countByBase = new Map(); + for (const b of baseLabels) countByBase.set(b, (countByBase.get(b) ?? 0) + 1); + const pinned = pinnedDevice?.trim() || null; + const pinnedNotListed = !!(pinned && !devices.includes(pinned)); + const ghost: { value: string; label: string }[] = pinnedNotListed + ? (() => { + const base = formatAudioDeviceLabel(pinned); + let label = `${base} · ${notInListSuffix}`; + if (osDefaultDeviceId && pinned === osDefaultDeviceId) label = `${label} · ${osDefaultMark}`; + return [{ value: pinned, label }]; + })() + : []; + return [ + { value: '', label: defaultLabel }, + ...ghost, + ...devices.map((d, i) => { + const base = baseLabels[i]; + const dup = (countByBase.get(base) ?? 0) > 1; + let label = disambiguatedAudioDeviceLabel(d, base, dup); + if (osDefaultDeviceId && d === osDefaultDeviceId) label = `${label} · ${osDefaultMark}`; + return { value: d, label }; + }), + ]; +} diff --git a/src/utils/formatBytes.ts b/src/utils/formatBytes.ts new file mode 100644 index 00000000..885a3826 --- /dev/null +++ b/src/utils/formatBytes.ts @@ -0,0 +1,11 @@ +export function formatBytes(bytes: number): string { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`; +} + +/** Align hot-cache size slider (step 32 MB) to valid values. */ +export function snapHotCacheMb(v: number): number { + const x = Math.min(20000, Math.max(32, Math.round(v))); + return Math.round((x - 32) / 32) * 32 + 32; +}