mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 00:58:51 +08:00
添加设置音频输出选择
This commit is contained in:
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
Reference in New Issue
Block a user