refactor(audio-tab): I.3 — split AudioTab.tsx 521 → 97 LOC across 6 files (#674)

* refactor(audio-tab): extract useAudioDevicesProbe hook

Pull the device-list state, refreshAudioDevices callback, mount probe,
and the audio:device-changed / audio:device-reset listener wiring into
hooks/useAudioDevicesProbe.ts. macOS short-circuit lives in the hook.

AudioTab.tsx: 521 → 463 LOC.

* refactor(audio-tab): extract AudioOutputDeviceSection

Pull the audio output device picker (macOS notice + CustomSelect +
refresh button) into components/settings/audio/AudioOutputDeviceSection.tsx.

AudioTab.tsx: 463 → 418 LOC.

* refactor(audio-tab): extract NormalizationBlock

Pull the engine picker (Off / ReplayGain / LUFS) and the engine-specific
config blocks (RG mode + pre-gain + fallback; LUFS target + pre-analysis
attenuation with reset) into components/settings/audio/NormalizationBlock.tsx.

AudioTab.tsx: 418 → 267 LOC.

* refactor(audio-tab): extract PlaybackBehaviorBlock

Pull Crossfade ↔ Gapless mutually-exclusive toggles + Preserve Play Next
Order into components/settings/audio/PlaybackBehaviorBlock.tsx. The
crossfade-seconds slider only renders while crossfade is the active mode.

AudioTab.tsx: 267 → 201 LOC.

* refactor(audio-tab): extract TrackPreviewsSection

Pull the track previews subsection (master toggle, per-location grid,
start-ratio slider, duration slider) into
components/settings/audio/TrackPreviewsSection.tsx.

AudioTab.tsx: 201 → 97 LOC.
This commit is contained in:
Frank Stellmacher
2026-05-14 00:20:01 +02:00
committed by GitHub
parent f14c8f21e6
commit 59772db5ee
6 changed files with 625 additions and 451 deletions
+100
View File
@@ -0,0 +1,100 @@
import { useCallback, useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../store/authStore';
import { IS_MACOS } from '../utils/platform';
import { sortAudioDeviceIds } from '../utils/audioDeviceLabels';
import { showToast } from '../utils/toast';
interface UseAudioDevicesProbeResult {
audioDevices: string[];
osDefaultAudioDeviceId: string | null;
deviceSwitching: boolean;
devicesLoading: boolean;
setDeviceSwitching: (v: boolean) => void;
refreshAudioDevices: (opts?: { silent?: boolean }) => void;
}
/**
* Drive the audio-output-device picker in the audio settings tab. Calls
* the Rust side to list devices + the current OS default, canonicalises
* the persisted selection (filenames may have shifted between runs), and
* re-runs whenever the backend reopens the stream
* (`audio:device-changed` / `audio:device-reset`).
*
* macOS short-circuits — the audio stream is pinned to the system default
* there (see `audioOutputDeviceMacNotice`) and the picker UI is hidden.
*/
export function useAudioDevicesProbe(t: TFunction): UseAudioDevicesProbeResult {
const [audioDevices, setAudioDevices] = useState<string[]>([]);
const [osDefaultAudioDeviceId, setOsDefaultAudioDeviceId] = useState<string | null>(null);
const [deviceSwitching, setDeviceSwitching] = useState(false);
const [devicesLoading, setDevicesLoading] = useState(false);
const refreshAudioDevices = useCallback((opts?: { silent?: boolean }) => {
const silent = !!opts?.silent;
if (!silent) setDevicesLoading(true);
const listP = invoke<string[]>('audio_list_devices').catch((e) => {
console.error(e);
showToast(t('settings.audioOutputDeviceListError'), 5000, 'error');
return [] as string[];
});
const defP = invoke<string | null>('audio_default_output_device_name').catch(() => null);
Promise.all([listP, defP])
.then(async ([devices, osDefault]) => {
let canon: string | null = null;
try {
canon = await invoke<string | null>('audio_canonicalize_selected_device');
if (canon) useAuthStore.getState().setAudioOutputDevice(canon);
} catch {
/* ignore */
}
const finalList = canon
? await invoke<string[]>('audio_list_devices').catch(() => devices)
: devices;
const defId = osDefault ?? null;
setAudioDevices(sortAudioDeviceIds(finalList, defId));
setOsDefaultAudioDeviceId(defId);
})
.finally(() => {
if (!silent) setDevicesLoading(false);
});
}, [t]);
useEffect(() => {
if (IS_MACOS) return;
refreshAudioDevices();
}, [refreshAudioDevices]);
useEffect(() => {
if (IS_MACOS) return;
let cancelled = false;
const unlisteners: Array<() => void> = [];
(async () => {
for (const ev of ['audio:device-changed', 'audio:device-reset'] as const) {
const u = await listen(ev, () => {
if (!cancelled) refreshAudioDevices({ silent: true });
});
if (cancelled) {
u();
return;
}
unlisteners.push(u);
}
})();
return () => {
cancelled = true;
for (const u of unlisteners) u();
};
}, [refreshAudioDevices]);
return {
audioDevices,
osDefaultAudioDeviceId,
deviceSwitching,
devicesLoading,
setDeviceSwitching,
refreshAudioDevices,
};
}