添加设置音频输出选择

This commit is contained in:
2026-05-18 15:52:51 +08:00
parent 38c079ed5c
commit baa6235c56
4 changed files with 67 additions and 7 deletions

View File

@ -440,6 +440,14 @@ onMounted(async () => {
} catch {} } catch {}
updater.checkForUpdate(true); updater.checkForUpdate(true);
// 恢复保存的输出设备设置
if(settings.outputDevice) {
try {
await invoke('set_output_device', { device: settings.outputDevice });
}
catch{}
}
}); });
const currentWindow = getCurrentWindow(); const currentWindow = getCurrentWindow();

View File

@ -2,22 +2,22 @@
<div class="relative" ref="container"> <div class="relative" ref="container">
<button <button
@click="toggle" @click="toggle"
class="flex items-center justify-between bg-subtle border border-line rounded-lg px-3 py-1.5 text-sm text-content outline-none transition min-w-[140px] hover:border-content-3 focus:border-accent focus:shadow-[0_0_0_2px_var(--c-accent-dim)]" class="flex items-center justify-between bg-subtle border border-line rounded-lg px-3 py-1.5 text-sm text-content outline-none transition min-w-[140px] max-w-[320px] hover:border-content-3 focus:border-accent focus:shadow-[0_0_0_2px_var(--c-accent-dim)]"
:class="{ 'border-accent shadow-[0_0_0_2px_var(--c-accent-dim)]': isOpen }" :class="{ 'border-accent shadow-[0_0_0_2px_var(--c-accent-dim)]': isOpen }"
> >
<span>{{ currentLabel }}</span> <span class="truncate">{{ currentLabel }}</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="transition-transform flex-shrink-0 ml-2" :class="{ 'rotate-180': isOpen }"><polyline points="6 9 12 15 18 9"/></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="transition-transform flex-shrink-0 ml-2" :class="{ 'rotate-180': isOpen }"><polyline points="6 9 12 15 18 9"/></svg>
</button> </button>
<Transition name="dropdown"> <Transition name="dropdown">
<div v-if="isOpen" class="absolute right-0 top-full mt-1 bg-surface border border-line rounded-lg shadow-xl z-50 py-1 min-w-full overflow-hidden"> <div v-if="isOpen" class="absolute right-0 top-full mt-1 bg-surface border border-line rounded-lg shadow-xl z-50 py-1 min-w-full max-w-[360px] overflow-hidden">
<button <button
v-for="(label, key) in options" v-for="(label, key) in options"
:key="key" :key="key"
@click="select(key)" @click="select(key)"
class="w-full text-left px-3 py-2 text-sm transition flex items-center justify-between" class="w-full text-left px-3 py-2 text-sm transition flex items-center justify-between gap-2"
:class="modelValue === key ? 'bg-accent-dim text-accent-text' : 'text-content-2 hover:bg-subtle hover:text-content'" :class="modelValue === key ? 'bg-accent-dim text-accent-text' : 'text-content-2 hover:bg-subtle hover:text-content'"
> >
<span>{{ label }}</span> <span class="truncate">{{ label }}</span>
<svg v-if="modelValue === key" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <svg v-if="modelValue === key" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</button> </button>
</div> </div>

View File

@ -41,6 +41,7 @@ interface SettingsData {
theme: ThemeMode; theme: ThemeMode;
closeAction: CloseAction; closeAction: CloseAction;
shortcuts: Record<string, ShortcutBinding>; shortcuts: Record<string, ShortcutBinding>;
outputDevice: string | null;
} }
function loadSettings(): SettingsData { function loadSettings(): SettingsData {
@ -54,6 +55,7 @@ function loadSettings(): SettingsData {
theme: parsed.theme || 'dark', theme: parsed.theme || 'dark',
closeAction: parsed.closeAction || 'ask', closeAction: parsed.closeAction || 'ask',
shortcuts: { ...defaultShortcuts, ...(parsed.shortcuts || {}) }, shortcuts: { ...defaultShortcuts, ...(parsed.shortcuts || {}) },
outputDevice: parsed.outputDevice || null,
}; };
} }
} catch {} } catch {}
@ -63,6 +65,7 @@ function loadSettings(): SettingsData {
theme: 'dark', theme: 'dark',
closeAction: 'ask', closeAction: 'ask',
shortcuts: { ...defaultShortcuts }, shortcuts: { ...defaultShortcuts },
outputDevice: null,
}; };
} }
@ -74,6 +77,7 @@ export const useSettingsStore = defineStore('settings', () => {
const theme = ref<ThemeMode>(saved.theme); const theme = ref<ThemeMode>(saved.theme);
const closeAction = ref<CloseAction>(saved.closeAction || 'ask'); const closeAction = ref<CloseAction>(saved.closeAction || 'ask');
const shortcuts = ref<Record<string, ShortcutBinding>>(saved.shortcuts); const shortcuts = ref<Record<string, ShortcutBinding>>(saved.shortcuts);
const outputDevice = ref<string | null>(saved.outputDevice);
function setAudioQuality(q: AudioQuality) { function setAudioQuality(q: AudioQuality) {
audioQuality.value = q; audioQuality.value = q;
@ -99,21 +103,27 @@ export const useSettingsStore = defineStore('settings', () => {
shortcuts.value = { ...defaultShortcuts }; shortcuts.value = { ...defaultShortcuts };
} }
function setOutputDevice(device: string | null) {
outputDevice.value = device;
}
function resetAll() { function resetAll() {
audioQuality.value = 'standard'; audioQuality.value = 'standard';
downloadPath.value = ''; downloadPath.value = '';
theme.value = 'dark'; theme.value = 'dark';
closeAction.value = 'ask'; closeAction.value = 'ask';
shortcuts.value = { ...defaultShortcuts }; shortcuts.value = { ...defaultShortcuts };
outputDevice.value = null;
} }
watch([audioQuality, downloadPath, theme, closeAction, shortcuts], () => { watch([audioQuality, downloadPath, theme, closeAction, shortcuts, outputDevice], () => {
const data: SettingsData = { const data: SettingsData = {
audioQuality: audioQuality.value, audioQuality: audioQuality.value,
downloadPath: downloadPath.value, downloadPath: downloadPath.value,
theme: theme.value, theme: theme.value,
closeAction: closeAction.value, closeAction: closeAction.value,
shortcuts: shortcuts.value, shortcuts: shortcuts.value,
outputDevice: outputDevice.value,
}; };
localStorage.setItem('app_settings', JSON.stringify(data)); localStorage.setItem('app_settings', JSON.stringify(data));
}, { deep: true }); }, { deep: true });
@ -124,10 +134,12 @@ export const useSettingsStore = defineStore('settings', () => {
theme, theme,
closeAction, closeAction,
shortcuts, shortcuts,
outputDevice,
setAudioQuality, setAudioQuality,
setDownloadPath, setDownloadPath,
setTheme, setTheme,
setCloseAction, setCloseAction,
setOutputDevice,
setShortcut, setShortcut,
resetShortcuts, resetShortcuts,
resetAll, resetAll,

View File

@ -8,6 +8,14 @@
<section class="mb-8"> <section class="mb-8">
<h2 class="text-sm text-content-2 uppercase tracking-wider mb-4">播放</h2> <h2 class="text-sm text-content-2 uppercase tracking-wider mb-4">播放</h2>
<div class="space-y-5"> <div class="space-y-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium">输出设备</p>
<p class="text-xs text-content-3 mt-0.5">选择音频播放设备</p>
</div>
<CustomSelect v-model="selectedDevice" :options="deviceOptions" />
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium">音质选择</p> <p class="text-sm font-medium">音质选择</p>
@ -246,6 +254,37 @@ const settings = useSettingsStore();
const { showToast } = useToast(); const { showToast } = useToast();
const updater = useUpdater(); const updater = useUpdater();
const devices = ref<string[]>([]);
const deviceOptions = computed(() => {
const options: Record<string, string> = { '': '跟随系统默认' };
for (const name of devices.value) {
options[name] = name;
}
return options;
});
const selectedDevice = computed({
get: () => settings.outputDevice || '',
set: (val: string) => {
const device = val === '' ? null : val;
settings.setOutputDevice(device);
invoke('set_output_device', { device }).then(() => {
showToast(device ? `已切换到: ${device}` : '已切换到系统默认', 'success');
}).catch((e) => {
console.error('切换设备失败: ', e);
showToast('切换设备失败', 'error');
});
}
});
async function loadDevices() {
try {
devices.value = await invoke<string[]>('get_output_devices');
} catch (e) {
console.error('获取设备失败: ', e);
}
}
const appVersion = ref(''); const appVersion = ref('');
const defaultDownloadPath = ref(''); const defaultDownloadPath = ref('');
onMounted(async () => { onMounted(async () => {
@ -253,6 +292,7 @@ onMounted(async () => {
try { try {
defaultDownloadPath.value = await invoke<string>('get_default_download_path'); defaultDownloadPath.value = await invoke<string>('get_default_download_path');
} catch { } } catch { }
loadDevices();
}); });
const closeActionValue = computed({ const closeActionValue = computed({