fix(voice): keep saved linux audio apps in the source picker (#3150)

This commit is contained in:
Hampus
2026-10-03 02:14:06 +02:00
committed by GitHub
parent cf9752db4f
commit eb0e8366bc
2 changed files with 32 additions and 14 deletions
@@ -15,8 +15,10 @@ import {
type LinuxAudioSourceFilterOptions, type LinuxAudioSourceFilterOptions,
type LinuxAudioSourceItem, type LinuxAudioSourceItem,
linuxAudioSourceItemKey, linuxAudioSourceItemKey,
linuxAudioSourcePatternsEqual,
mapLinuxAudioNodeToItems, mapLinuxAudioNodeToItems,
uniqueLinuxAudioSourceItems, uniqueLinuxAudioSourceItems,
withSelectedLinuxAudioSources,
} from '@app/features/voice/utils/LinuxAudioSourceRules'; } from '@app/features/voice/utils/LinuxAudioSourceRules';
import { import {
formatScreenShareAudioSummary, formatScreenShareAudioSummary,
@@ -78,15 +80,8 @@ const EMPTY_SNAPSHOT: AudioSourceSnapshot = {
error: null, error: null,
}; };
function nodesEqual(a: VirtmicNode, b: VirtmicNode): boolean {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => a[key] === b[key]);
}
function isItemSelected(value: VirtmicNode, sources: Array<VirtmicNode>): boolean { function isItemSelected(value: VirtmicNode, sources: Array<VirtmicNode>): boolean {
return sources.some((source) => nodesEqual(source, value)); return sources.some((source) => linuxAudioSourcePatternsEqual(source, value));
} }
async function fetchAudioSources(options: LinuxAudioSourceFilterOptions): Promise<AudioSourceSnapshot> { async function fetchAudioSources(options: LinuxAudioSourceFilterOptions): Promise<AudioSourceSnapshot> {
@@ -188,7 +183,7 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
(item: LinuxAudioSourceItem) => { (item: LinuxAudioSourceItem) => {
const isSelected = isItemSelected(item.value, includeSources); const isSelected = isItemSelected(item.value, includeSources);
const nextSources = isSelected const nextSources = isSelected
? includeSources.filter((source) => !nodesEqual(source, item.value)) ? includeSources.filter((source) => !linuxAudioSourcePatternsEqual(source, item.value))
: [...includeSources, item.value]; : [...includeSources, item.value];
VoiceSettingsCommands.update({ VoiceSettingsCommands.update({
screenShareAudioSourceMode: nextSources.length > 0 ? 'specific' : 'system', screenShareAudioSourceMode: nextSources.length > 0 ? 'specific' : 'system',
@@ -203,7 +198,7 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
(item: LinuxAudioSourceItem) => { (item: LinuxAudioSourceItem) => {
const isSelected = isItemSelected(item.value, excludeSources); const isSelected = isItemSelected(item.value, excludeSources);
const nextSources = isSelected const nextSources = isSelected
? excludeSources.filter((source) => !nodesEqual(source, item.value)) ? excludeSources.filter((source) => !linuxAudioSourcePatternsEqual(source, item.value))
: [...excludeSources, item.value]; : [...excludeSources, item.value];
VoiceSettingsCommands.update({ VoiceSettingsCommands.update({
screenShareAudioExcludeSources: nextSources, screenShareAudioExcludeSources: nextSources,
@@ -230,6 +225,8 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
usesDeviceMicrophone, usesDeviceMicrophone,
}); });
const showsWideSourceLists = !offersWindowScope || resolvedScope === 'system'; const showsWideSourceLists = !offersWindowScope || resolvedScope === 'system';
const includeItems = withSelectedLinuxAudioSources(snapshot.items, includeSources);
const excludeItems = withSelectedLinuxAudioSources(snapshot.items, excludeSources);
const wideSourceIsSelected = isDeviceShare ? usesDeviceMicrophone : sourceMode === 'system'; const wideSourceIsSelected = isDeviceShare ? usesDeviceMicrophone : sourceMode === 'system';
if (!snapshot.available && !snapshot.loading) { if (!snapshot.available && !snapshot.loading) {
return null; return null;
@@ -269,12 +266,12 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
</MenuItemRadio> </MenuItemRadio>
)} )}
</MenuGroup> </MenuGroup>
{showsWideSourceLists && snapshot.items.length > 0 && ( {showsWideSourceLists && includeItems.length > 0 && (
<MenuGroup data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.menu-group--2"> <MenuGroup data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.menu-group--2">
<MenuGroupLabel data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.group-label.include-apps"> <MenuGroupLabel data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.group-label.include-apps">
{i18n._(INCLUDE_APPS_DESCRIPTOR)} {i18n._(INCLUDE_APPS_DESCRIPTOR)}
</MenuGroupLabel> </MenuGroupLabel>
{snapshot.items.map((item) => ( {includeItems.map((item) => (
<CheckboxItem <CheckboxItem
key={linuxAudioSourceItemKey(item)} key={linuxAudioSourceItemKey(item)}
checked={isItemSelected(item.value, includeSources)} checked={isItemSelected(item.value, includeSources)}
@@ -286,12 +283,12 @@ export const AudioSourcePickerLinuxSubmenu = observer((props: AudioSourcePickerL
))} ))}
</MenuGroup> </MenuGroup>
)} )}
{!isDeviceShare && showsWideSourceLists && sourceMode === 'system' && snapshot.items.length > 0 && ( {!isDeviceShare && showsWideSourceLists && sourceMode === 'system' && excludeItems.length > 0 && (
<MenuGroup data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.menu-group--3"> <MenuGroup data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.menu-group--3">
<MenuGroupLabel data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.group-label.exclude-from-system"> <MenuGroupLabel data-flx="voice.audio-source-picker-linux.audio-source-picker-linux-submenu.group-label.exclude-from-system">
{i18n._(EXCLUDE_FROM_SYSTEM_DESCRIPTOR)} {i18n._(EXCLUDE_FROM_SYSTEM_DESCRIPTOR)}
</MenuGroupLabel> </MenuGroupLabel>
{snapshot.items.map((item) => ( {excludeItems.map((item) => (
<CheckboxItem <CheckboxItem
key={`exclude-${linuxAudioSourceItemKey(item)}`} key={`exclude-${linuxAudioSourceItemKey(item)}`}
checked={isItemSelected(item.value, excludeSources)} checked={isItemSelected(item.value, excludeSources)}
@@ -135,6 +135,27 @@ export function mapLinuxAudioNodeToItems(
return items; return items;
} }
export function linuxAudioSourcePatternsEqual(a: VirtmicNode, b: VirtmicNode): boolean {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => a[key] === b[key]);
}
export function withSelectedLinuxAudioSources(
items: ReadonlyArray<LinuxAudioSourceItem>,
selected: ReadonlyArray<VirtmicNode>,
): Array<LinuxAudioSourceItem> {
const missing = selected.filter((source) => !items.some((item) => linuxAudioSourcePatternsEqual(item.value, source)));
return [
...items,
...missing.map((source) => ({
name: getLinuxAudioSourceDisplayName(source) ?? JSON.stringify(source),
value: source,
})),
];
}
export function uniqueLinuxAudioSourceItems(items: Array<LinuxAudioSourceItem>): Array<LinuxAudioSourceItem> { export function uniqueLinuxAudioSourceItems(items: Array<LinuxAudioSourceItem>): Array<LinuxAudioSourceItem> {
const seen = new Set<string>(); const seen = new Set<string>();
const result: Array<LinuxAudioSourceItem> = []; const result: Array<LinuxAudioSourceItem> = [];