添加设置音频输出选择

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 {}
updater.checkForUpdate(true);
// 恢复保存的输出设备设置
if(settings.outputDevice) {
try {
await invoke('set_output_device', { device: settings.outputDevice });
}
catch{}
}
});
const currentWindow = getCurrentWindow();

View File

@ -2,22 +2,22 @@
<div class="relative" ref="container">
<button
@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 }"
>
<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>
</button>
<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
v-for="(label, key) in options"
:key="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'"
>
<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>
</button>
</div>

View File

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

View File

@ -8,6 +8,14 @@
<section class="mb-8">
<h2 class="text-sm text-content-2 uppercase tracking-wider mb-4">播放</h2>
<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>
<p class="text-sm font-medium">音质选择</p>
@ -246,13 +254,45 @@ const settings = useSettingsStore();
const { showToast } = useToast();
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 defaultDownloadPath = ref('');
onMounted(async () => {
appVersion.value = await getVersion();
try {
defaultDownloadPath.value = await invoke<string>('get_default_download_path');
} catch {}
} catch { }
loadDevices();
});
const closeActionValue = computed({