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 {}
|
} 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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user