mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
209dd61442
Move domain-agnostic, feature-free helpers out of the flat utils/ and store/ roots into src/lib/ (plan M4, §3 lib/ layer): - lib/format/: formatBytes, formatClockTime, formatDuration, formatHumanDuration, relativeTime (pure format/date/byte/clock helpers) - lib/i18n.ts: i18next bootstrap (global app infra) - lib/util/: sanitizeHtml, platform, dedupeById, safeStorage (pure helpers + the zustand storage adapter) playbackScheduleFormat stays in utils/format (runtime usePlayerStore dep = audio-core coupling, not generic). formatClockTime keeps a type-only @/store/authStoreTypes import (erased, no runtime inversion — accepted per the deviceSync precedent). Pure move via deep @/lib/* specifiers (no barrel → no mock-collapse surface). tsc 0, lint 0/0, full suite 319 files / 2353 tests green. Tooling note: lib_move.py regex extended to also rewrite side-effect imports (import './i18n') and vi.importActual paths — both were silent gaps.
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Blend, Gauge, Sliders, Volume2, Waves } from 'lucide-react';
|
|
import { useAuthStore } from '@/store/authStore';
|
|
import Equalizer from '@/components/Equalizer';
|
|
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
|
|
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
|
|
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
|
|
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/utils/audio/loudnessPreAnalysisSlider';
|
|
import { useAudioDevicesProbe } from '@/hooks/useAudioDevicesProbe';
|
|
import { IS_MACOS } from '@/lib/util/platform';
|
|
import { AudioOutputDeviceSection } from '@/features/settings/components/audio/AudioOutputDeviceSection';
|
|
import { NormalizationBlock } from '@/features/settings/components/audio/NormalizationBlock';
|
|
import { PlaybackRateBlock } from '@/features/settings/components/audio/PlaybackRateBlock';
|
|
import { TrackTransitionsBlock } from '@/features/settings/components/audio/TrackTransitionsBlock';
|
|
import { TrackPreviewsSection } from '@/features/settings/components/audio/TrackPreviewsSection';
|
|
import { HiResCrossfadeResampleBlock } from '@/features/settings/components/audio/HiResCrossfadeResampleBlock';
|
|
|
|
export function AudioTab() {
|
|
const { t } = useTranslation();
|
|
const auth = useAuthStore();
|
|
const {
|
|
audioDevices,
|
|
osDefaultAudioDeviceId,
|
|
deviceSwitching,
|
|
devicesLoading,
|
|
setDeviceSwitching,
|
|
refreshAudioDevices,
|
|
} = useAudioDevicesProbe(t);
|
|
|
|
const preAnalysisEffectiveDb = useMemo(
|
|
() => effectiveLoudnessPreAnalysisAttenuationDb(
|
|
auth.loudnessPreAnalysisAttenuationDb,
|
|
auth.loudnessTargetLufs,
|
|
),
|
|
[auth.loudnessPreAnalysisAttenuationDb, auth.loudnessTargetLufs],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Output-device picker is hidden on macOS — the stream is pinned to the
|
|
system default there, so the whole category is gated out. */}
|
|
{!IS_MACOS && (
|
|
<AudioOutputDeviceSection
|
|
audioDevices={audioDevices}
|
|
osDefaultAudioDeviceId={osDefaultAudioDeviceId}
|
|
deviceSwitching={deviceSwitching}
|
|
devicesLoading={devicesLoading}
|
|
setDeviceSwitching={setDeviceSwitching}
|
|
refreshAudioDevices={refreshAudioDevices}
|
|
t={t}
|
|
/>
|
|
)}
|
|
|
|
{/* Normalization — loudness levelling (own category) */}
|
|
<SettingsSubSection
|
|
title={t('settings.normalization')}
|
|
description={t('settings.normalizationDesc')}
|
|
icon={<Volume2 size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<NormalizationBlock preAnalysisEffectiveDb={preAnalysisEffectiveDb} t={t} />
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
{/* Track transitions — crossfade / gapless / AutoDJ (own category) */}
|
|
<SettingsSubSection
|
|
title={t('settings.transitionsTitle')}
|
|
description={t('settings.transitionsDesc')}
|
|
icon={<Blend size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<TrackTransitionsBlock t={t} />
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
{/* Native Hi-Res Playback */}
|
|
<SettingsSubSection
|
|
title={t('settings.hiResTitle')}
|
|
icon={<Waves size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<SettingsGroup>
|
|
<SettingsToggle
|
|
desc={t('settings.hiResDesc')}
|
|
ariaLabel={t('settings.hiResEnabled')}
|
|
id="hires-enabled-toggle"
|
|
checked={auth.enableHiRes}
|
|
onChange={auth.setEnableHiRes}
|
|
/>
|
|
<HiResCrossfadeResampleBlock
|
|
enabled={auth.enableHiRes}
|
|
resampleHz={auth.hiResCrossfadeResampleHz}
|
|
onResampleHzChange={auth.setHiResCrossfadeResampleHz}
|
|
t={t}
|
|
/>
|
|
</SettingsGroup>
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
{/* Equalizer */}
|
|
<SettingsSubSection
|
|
title={t('settings.eqTitle')}
|
|
icon={<Sliders size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<SettingsGroup>
|
|
<Equalizer />
|
|
</SettingsGroup>
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
{/* Playback speed */}
|
|
<SettingsSubSection
|
|
title={t('settings.playbackRateTitle')}
|
|
icon={<Gauge size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<SettingsGroup>
|
|
<PlaybackRateBlock t={t} />
|
|
</SettingsGroup>
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
<TrackPreviewsSection t={t} />
|
|
</>
|
|
);
|
|
}
|