linux audioshare: add granular selection, more options, better ui (#621)
This commit is contained in:
		
							parent
							
								
									1a4d173bb4
								
							
						
					
					
						commit
						da7f13288f
					
				
					 7 changed files with 520 additions and 254 deletions
				
			
		|  | @ -27,7 +27,7 @@ | |||
|         "arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c" | ||||
|     }, | ||||
|     "optionalDependencies": { | ||||
|         "@vencord/venmic": "^3.5.0" | ||||
|         "@vencord/venmic": "^6.1.0" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@fal-works/esbuild-plugin-global-externals": "^2.1.2", | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ importers: | |||
|         version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c | ||||
|     optionalDependencies: | ||||
|       '@vencord/venmic': | ||||
|         specifier: ^3.5.0 | ||||
|         version: 3.5.0 | ||||
|         specifier: ^6.1.0 | ||||
|         version: 6.1.0 | ||||
|     devDependencies: | ||||
|       '@fal-works/esbuild-plugin-global-externals': | ||||
|         specifier: ^2.1.2 | ||||
|  | @ -603,8 +603,8 @@ packages: | |||
|   '@vencord/types@1.8.4': | ||||
|     resolution: {integrity: sha512-ogLqIOHVO+5zxKUVxAfGIAUZoEfIomrlg6f0cZ/2yd5vBAn1fA9Gi/NASoKfHZuJt8ZcYw329bgn0ah/VufqMg==} | ||||
| 
 | ||||
|   '@vencord/venmic@3.5.0': | ||||
|     resolution: {integrity: sha512-kPvrPcIeMkuqQriuiQAJ9rEBeqGR2nmFBuUtbZRGyiNRF9RDAfWSJYqhHVm6F7wbcqrSZio6FazZuBo0LvjJRw==} | ||||
|   '@vencord/venmic@6.1.0': | ||||
|     resolution: {integrity: sha512-YiCtzml/W8tYbGhu3jm5jfbbEnl2slKKARNK0jO+8qV979k9eFnfIRTxvhMN/SWq1h8ZNJdXVwvXpffQwq0RuA==} | ||||
|     engines: {node: '>=14.15'} | ||||
|     os: [linux] | ||||
| 
 | ||||
|  | @ -1562,6 +1562,7 @@ packages: | |||
| 
 | ||||
|   inflight@1.0.6: | ||||
|     resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} | ||||
|     deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. | ||||
| 
 | ||||
|   inherits@2.0.4: | ||||
|     resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} | ||||
|  | @ -2156,6 +2157,7 @@ packages: | |||
| 
 | ||||
|   rimraf@3.0.2: | ||||
|     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} | ||||
|     deprecated: Rimraf versions prior to v4 are no longer supported | ||||
|     hasBin: true | ||||
| 
 | ||||
|   roarr@2.15.4: | ||||
|  | @ -3032,7 +3034,7 @@ snapshots: | |||
|       standalone-electron-types: 1.0.0 | ||||
|       type-fest: 3.13.1 | ||||
| 
 | ||||
|   '@vencord/venmic@3.5.0': | ||||
|   '@vencord/venmic@6.1.0': | ||||
|     dependencies: | ||||
|       cmake-js: 7.3.0 | ||||
|       node-addon-api: 8.0.0 | ||||
|  | @ -3078,7 +3080,7 @@ snapshots: | |||
| 
 | ||||
|   app-builder-bin@4.0.0: {} | ||||
| 
 | ||||
|   app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): | ||||
|   app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): | ||||
|     dependencies: | ||||
|       '@develar/schema-utils': 2.6.5 | ||||
|       '@electron/notarize': 2.2.1 | ||||
|  | @ -3092,7 +3094,7 @@ snapshots: | |||
|       builder-util-runtime: 9.2.4 | ||||
|       chromium-pickle-js: 0.2.0 | ||||
|       debug: 4.3.4 | ||||
|       dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) | ||||
|       ejs: 3.1.9 | ||||
|       electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3) | ||||
|       electron-publish: 24.13.1 | ||||
|  | @ -3567,9 +3569,9 @@ snapshots: | |||
|       '@types/react': 17.0.2 | ||||
|       moment: 2.30.1 | ||||
| 
 | ||||
|   dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): | ||||
|   dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): | ||||
|     dependencies: | ||||
|       app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       builder-util: 24.13.1 | ||||
|       builder-util-runtime: 9.2.4 | ||||
|       fs-extra: 10.1.0 | ||||
|  | @ -3615,7 +3617,7 @@ snapshots: | |||
| 
 | ||||
|   electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3): | ||||
|     dependencies: | ||||
|       app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       archiver: 5.3.2 | ||||
|       builder-util: 24.13.1 | ||||
|       fs-extra: 10.1.0 | ||||
|  | @ -3625,11 +3627,11 @@ snapshots: | |||
| 
 | ||||
|   electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): | ||||
|     dependencies: | ||||
|       app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       builder-util: 24.13.1 | ||||
|       builder-util-runtime: 9.2.4 | ||||
|       chalk: 4.1.2 | ||||
|       dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) | ||||
|       dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) | ||||
|       fs-extra: 10.1.0 | ||||
|       is-ci: 3.0.1 | ||||
|       lazy-val: 1.0.5 | ||||
|  |  | |||
|  | @ -4,13 +4,13 @@ | |||
|  * Copyright (c) 2023 Vendicated and Vencord contributors | ||||
|  */ | ||||
| 
 | ||||
| import type { PatchBay as PatchBayType } from "@vencord/venmic"; | ||||
| import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic"; | ||||
| import { app, ipcMain } from "electron"; | ||||
| import { join } from "path"; | ||||
| import { IpcEvents } from "shared/IpcEvents"; | ||||
| import { STATIC_DIR } from "shared/paths"; | ||||
| 
 | ||||
| type LinkData = Parameters<PatchBayType["link"]>[0]; | ||||
| import { Settings } from "./settings"; | ||||
| 
 | ||||
| let PatchBay: typeof PatchBayType | undefined; | ||||
| let patchBayInstance: PatchBayType | undefined; | ||||
|  | @ -69,47 +69,64 @@ function getRendererAudioServicePid() { | |||
| ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { | ||||
|     const audioPid = getRendererAudioServicePid(); | ||||
| 
 | ||||
|     const list = obtainVenmic() | ||||
|         ?.list() | ||||
|         .filter(s => s["application.process.id"] !== audioPid) | ||||
|         .map(s => s["application.name"]); | ||||
|     const { granularSelect } = Settings.store.audio ?? {}; | ||||
| 
 | ||||
|     const uniqueTargets = [...new Set(list)]; | ||||
|     const targets = obtainVenmic() | ||||
|         ?.list(granularSelect ? ["application.process.id"] : undefined) | ||||
|         .filter(s => s["application.process.id"] !== audioPid); | ||||
| 
 | ||||
|     return list ? { ok: true, targets: uniqueTargets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated }; | ||||
|     return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated }; | ||||
| }); | ||||
| 
 | ||||
| ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => { | ||||
| ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => { | ||||
|     const pid = getRendererAudioServicePid(); | ||||
|     const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {}; | ||||
| 
 | ||||
|     const data: LinkData = { | ||||
|         include: targets.map(target => ({ key: "application.name", value: target })), | ||||
|         exclude: [{ key: "application.process.id", value: pid }] | ||||
|         include, | ||||
|         exclude: [{ "application.process.id": pid }], | ||||
|         ignore_devices: ignoreDevices | ||||
|     }; | ||||
| 
 | ||||
|     if (ignoreInputMedia ?? true) { | ||||
|         data.exclude.push({ "media.class": "Stream/Input/Audio" }); | ||||
|     } | ||||
| 
 | ||||
|     if (ignoreVirtual) { | ||||
|         data.exclude.push({ "node.virtual": "true" }); | ||||
|     } | ||||
| 
 | ||||
|     if (workaround) { | ||||
|         data.workaround = [ | ||||
|             { key: "application.process.id", value: pid }, | ||||
|             { key: "media.name", value: "RecordStream" } | ||||
|         ]; | ||||
|         data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; | ||||
|     } | ||||
| 
 | ||||
|     return obtainVenmic()?.link(data); | ||||
| }); | ||||
| 
 | ||||
| ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean, onlyDefaultSpeakers?: boolean) => { | ||||
| ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => { | ||||
|     const pid = getRendererAudioServicePid(); | ||||
| 
 | ||||
|     const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } = | ||||
|         Settings.store.audio ?? {}; | ||||
| 
 | ||||
|     const data: LinkData = { | ||||
|         exclude: [{ key: "application.process.id", value: pid }], | ||||
|         include: [], | ||||
|         exclude: [{ "application.process.id": pid }, ...exclude], | ||||
|         only_speakers: onlySpeakers, | ||||
|         ignore_devices: ignoreDevices, | ||||
|         only_default_speakers: onlyDefaultSpeakers | ||||
|     }; | ||||
| 
 | ||||
|     if (ignoreInputMedia ?? true) { | ||||
|         data.exclude.push({ "media.class": "Stream/Input/Audio" }); | ||||
|     } | ||||
| 
 | ||||
|     if (ignoreVirtual) { | ||||
|         data.exclude.push({ "node.virtual": "true" }); | ||||
|     } | ||||
| 
 | ||||
|     if (workaround) { | ||||
|         data.workaround = [ | ||||
|             { key: "application.process.id", value: pid }, | ||||
|             { key: "media.name", value: "RecordStream" } | ||||
|         ]; | ||||
|         data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; | ||||
|     } | ||||
| 
 | ||||
|     return obtainVenmic()?.link(data); | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
|  * Copyright (c) 2023 Vendicated and Vencord contributors | ||||
|  */ | ||||
| 
 | ||||
| import { Node } from "@vencord/venmic"; | ||||
| import { ipcRenderer } from "electron"; | ||||
| import type { Settings } from "shared/settings"; | ||||
| import type { LiteralUnion } from "type-fest"; | ||||
|  | @ -63,11 +64,10 @@ export const VesktopNative = { | |||
|     virtmic: { | ||||
|         list: () => | ||||
|             invoke< | ||||
|                 { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: string[]; hasPipewirePulse: boolean } | ||||
|                 { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean } | ||||
|             >(IpcEvents.VIRT_MIC_LIST), | ||||
|         start: (targets: string[], workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START, targets, workaround), | ||||
|         startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) => | ||||
|             invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, workaround, onlyDefaultSpeakers), | ||||
|         start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include), | ||||
|         startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), | ||||
|         stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP) | ||||
|     }, | ||||
|     arrpc: { | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 
 | ||||
| import "./screenSharePicker.css"; | ||||
| 
 | ||||
| import { closeModal, Logger, Margins, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; | ||||
| import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; | ||||
| import { findStoreLazy, onceReady } from "@vencord/types/webpack"; | ||||
| import { | ||||
|     Button, | ||||
|  | @ -19,8 +19,10 @@ import { | |||
|     UserStore, | ||||
|     useState | ||||
| } from "@vencord/types/webpack/common"; | ||||
| import { Node } from "@vencord/venmic"; | ||||
| import type { Dispatch, SetStateAction } from "react"; | ||||
| import { addPatch } from "renderer/patches/shared"; | ||||
| import { useSettings } from "renderer/settings"; | ||||
| import { isLinux, isWindows } from "renderer/utils"; | ||||
| 
 | ||||
| const StreamResolutions = ["480", "720", "1080", "1440"] as const; | ||||
|  | @ -31,14 +33,23 @@ const MediaEngineStore = findStoreLazy("MediaEngineStore"); | |||
| export type StreamResolution = (typeof StreamResolutions)[number]; | ||||
| export type StreamFps = (typeof StreamFps)[number]; | ||||
| 
 | ||||
| type SpecialSource = "None" | "Entire System"; | ||||
| 
 | ||||
| type AudioSource = SpecialSource | Node; | ||||
| type AudioSources = SpecialSource | Node[]; | ||||
| 
 | ||||
| interface AudioItem { | ||||
|     name: string; | ||||
|     value: AudioSource; | ||||
| } | ||||
| 
 | ||||
| interface StreamSettings { | ||||
|     resolution: StreamResolution; | ||||
|     fps: StreamFps; | ||||
|     audio: boolean; | ||||
|     audioSource?: string; | ||||
|     contentHint?: string; | ||||
|     workaround?: boolean; | ||||
|     onlyDefaultSpeakers?: boolean; | ||||
|     includeSources?: AudioSources; | ||||
|     excludeSources?: AudioSources; | ||||
| } | ||||
| 
 | ||||
| export interface StreamPick extends StreamSettings { | ||||
|  | @ -118,13 +129,17 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { | |||
|                     modalProps={props} | ||||
|                     submit={async v => { | ||||
|                         didSubmit = true; | ||||
|                         if (v.audioSource && v.audioSource !== "None") { | ||||
|                             if (v.audioSource === "Entire System") { | ||||
|                                 await VesktopNative.virtmic.startSystem(v.workaround); | ||||
| 
 | ||||
|                         if (v.includeSources && v.includeSources !== "None") { | ||||
|                             if (v.includeSources === "Entire System") { | ||||
|                                 await VesktopNative.virtmic.startSystem( | ||||
|                                     !v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources | ||||
|                                 ); | ||||
|                             } else { | ||||
|                                 await VesktopNative.virtmic.start([v.audioSource], v.workaround); | ||||
|                                 await VesktopNative.virtmic.start(v.includeSources); | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         resolve(v); | ||||
|                     }} | ||||
|                     close={() => { | ||||
|  | @ -159,6 +174,113 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre | |||
|     ); | ||||
| } | ||||
| 
 | ||||
| function AudioSettingsModal({ | ||||
|     modalProps, | ||||
|     close, | ||||
|     setAudioSources | ||||
| }: { | ||||
|     modalProps: any; | ||||
|     close: () => void; | ||||
|     setAudioSources: (s: AudioSources) => void; | ||||
| }) { | ||||
|     const Settings = useSettings(); | ||||
| 
 | ||||
|     return ( | ||||
|         <Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}> | ||||
|             <Modals.ModalHeader className="vcd-screen-picker-header"> | ||||
|                 <Forms.FormTitle tag="h2">Venmic Settings</Forms.FormTitle> | ||||
|                 <Modals.ModalCloseButton onClick={close} /> | ||||
|             </Modals.ModalHeader> | ||||
|             <Modals.ModalContent className="vcd-screen-picker-modal"> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })} | ||||
|                     value={Settings.audio?.workaround ?? false} | ||||
|                     note={ | ||||
|                         <> | ||||
|                             Work around an issue that causes the microphone to be shared instead of the correct audio. | ||||
|                             Only enable if you're experiencing this issue. | ||||
|                         </> | ||||
|                     } | ||||
|                 > | ||||
|                     Microphone Workaround | ||||
|                 </Switch> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })} | ||||
|                     value={Settings.audio?.onlySpeakers ?? true} | ||||
|                     note={ | ||||
|                         <> | ||||
|                             When sharing entire desktop audio, only share apps that play to a speaker. You may want to | ||||
|                             disable this when using "mix bussing". | ||||
|                         </> | ||||
|                     } | ||||
|                 > | ||||
|                     Only Speakers | ||||
|                 </Switch> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })} | ||||
|                     value={Settings.audio?.onlyDefaultSpeakers ?? true} | ||||
|                     note={ | ||||
|                         <> | ||||
|                             When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers. | ||||
|                             You may want to disable this when using "mix bussing". | ||||
|                         </> | ||||
|                     } | ||||
|                 > | ||||
|                     Only Default Speakers | ||||
|                 </Switch> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })} | ||||
|                     value={Settings.audio?.ignoreInputMedia ?? true} | ||||
|                     note={<>Exclude nodes that are intended to capture audio.</>} | ||||
|                 > | ||||
|                     Ignore Inputs | ||||
|                 </Switch> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })} | ||||
|                     value={Settings.audio?.ignoreVirtual ?? false} | ||||
|                     note={ | ||||
|                         <> | ||||
|                             Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using | ||||
|                             "mix bussing". | ||||
|                         </> | ||||
|                     } | ||||
|                 > | ||||
|                     Ignore Virtual | ||||
|                 </Switch> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={v => (Settings.audio = { ...Settings.audio, ignoreDevices: v })} | ||||
|                     value={Settings.audio?.ignoreDevices ?? true} | ||||
|                     note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.</>} | ||||
|                 > | ||||
|                     Ignore Devices | ||||
|                 </Switch> | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={value => { | ||||
|                         Settings.audio = { ...Settings.audio, granularSelect: value }; | ||||
|                         setAudioSources("None"); | ||||
|                     }} | ||||
|                     value={Settings.audio?.granularSelect ?? false} | ||||
|                     note={<>Allow to select applications more granularly.</>} | ||||
|                 > | ||||
|                     Granular Selection | ||||
|                 </Switch> | ||||
|             </Modals.ModalContent> | ||||
|             <Modals.ModalFooter className="vcd-screen-picker-footer"> | ||||
|                 <Button color={Button.Colors.TRANSPARENT} onClick={close}> | ||||
|                     Back | ||||
|                 </Button> | ||||
|             </Modals.ModalFooter> | ||||
|         </Modals.ModalRoot> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| function StreamSettings({ | ||||
|     source, | ||||
|     settings, | ||||
|  | @ -170,6 +292,8 @@ function StreamSettings({ | |||
|     setSettings: Dispatch<SetStateAction<StreamSettings>>; | ||||
|     skipPicker: boolean; | ||||
| }) { | ||||
|     const Settings = useSettings(); | ||||
| 
 | ||||
|     const [thumb] = useAwaiter( | ||||
|         () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), | ||||
|         { | ||||
|  | @ -178,230 +302,346 @@ function StreamSettings({ | |||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     const openSettings = () => { | ||||
|         const key = openModal(props => ( | ||||
|             <AudioSettingsModal | ||||
|                 modalProps={props} | ||||
|                 close={() => props.onClose()} | ||||
|                 setAudioSources={sources => | ||||
|                     setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources })) | ||||
|                 } | ||||
|             /> | ||||
|         )); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <div className={isLinux ? "vcd-screen-picker-settings-grid" : ""}> | ||||
|             <div> | ||||
|                 <Forms.FormTitle>What you're streaming</Forms.FormTitle> | ||||
|                 <Card className="vcd-screen-picker-card vcd-screen-picker-preview"> | ||||
|                     <img | ||||
|                         src={thumb} | ||||
|                         alt="" | ||||
|                         className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"} | ||||
|                     /> | ||||
|                     <Text variant="text-sm/normal">{source.name}</Text> | ||||
|                 </Card> | ||||
|         <div> | ||||
|             <Forms.FormTitle>What you're streaming</Forms.FormTitle> | ||||
|             <Card className="vcd-screen-picker-card vcd-screen-picker-preview"> | ||||
|                 <img | ||||
|                     src={thumb} | ||||
|                     alt="" | ||||
|                     className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"} | ||||
|                 /> | ||||
|                 <Text variant="text-sm/normal">{source.name}</Text> | ||||
|             </Card> | ||||
| 
 | ||||
|                 <Forms.FormTitle>Stream Settings</Forms.FormTitle> | ||||
|             <Forms.FormTitle>Stream Settings</Forms.FormTitle> | ||||
| 
 | ||||
|                 <Card className="vcd-screen-picker-card"> | ||||
|                     <div className="vcd-screen-picker-quality"> | ||||
|                         <section> | ||||
|                             <Forms.FormTitle>Resolution</Forms.FormTitle> | ||||
|             <Card className="vcd-screen-picker-card"> | ||||
|                 <div className="vcd-screen-picker-quality"> | ||||
|                     <section> | ||||
|                         <Forms.FormTitle>Resolution</Forms.FormTitle> | ||||
|                         <div className="vcd-screen-picker-radios"> | ||||
|                             {StreamResolutions.map(res => ( | ||||
|                                 <label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}> | ||||
|                                     <Text variant="text-sm/bold">{res}</Text> | ||||
|                                     <input | ||||
|                                         type="radio" | ||||
|                                         name="resolution" | ||||
|                                         value={res} | ||||
|                                         checked={settings.resolution === res} | ||||
|                                         onChange={() => setSettings(s => ({ ...s, resolution: res }))} | ||||
|                                     /> | ||||
|                                 </label> | ||||
|                             ))} | ||||
|                         </div> | ||||
|                     </section> | ||||
| 
 | ||||
|                     <section> | ||||
|                         <Forms.FormTitle>Frame Rate</Forms.FormTitle> | ||||
|                         <div className="vcd-screen-picker-radios"> | ||||
|                             {StreamFps.map(fps => ( | ||||
|                                 <label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}> | ||||
|                                     <Text variant="text-sm/bold">{fps}</Text> | ||||
|                                     <input | ||||
|                                         type="radio" | ||||
|                                         name="fps" | ||||
|                                         value={fps} | ||||
|                                         checked={settings.fps === fps} | ||||
|                                         onChange={() => setSettings(s => ({ ...s, fps }))} | ||||
|                                     /> | ||||
|                                 </label> | ||||
|                             ))} | ||||
|                         </div> | ||||
|                     </section> | ||||
|                 </div> | ||||
|                 <div className="vcd-screen-picker-quality"> | ||||
|                     <section> | ||||
|                         <Forms.FormTitle>Content Type</Forms.FormTitle> | ||||
|                         <div> | ||||
|                             <div className="vcd-screen-picker-radios"> | ||||
|                                 {StreamResolutions.map(res => ( | ||||
|                                     <label | ||||
|                                         className="vcd-screen-picker-radio" | ||||
|                                         data-checked={settings.resolution === res} | ||||
|                                     > | ||||
|                                         <Text variant="text-sm/bold">{res}</Text> | ||||
|                                         <input | ||||
|                                             type="radio" | ||||
|                                             name="resolution" | ||||
|                                             value={res} | ||||
|                                             checked={settings.resolution === res} | ||||
|                                             onChange={() => setSettings(s => ({ ...s, resolution: res }))} | ||||
|                                         /> | ||||
|                                     </label> | ||||
|                                 ))} | ||||
|                             </div> | ||||
|                         </section> | ||||
| 
 | ||||
|                         <section> | ||||
|                             <Forms.FormTitle>Frame Rate</Forms.FormTitle> | ||||
|                             <div className="vcd-screen-picker-radios"> | ||||
|                                 {StreamFps.map(fps => ( | ||||
|                                     <label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}> | ||||
|                                         <Text variant="text-sm/bold">{fps}</Text> | ||||
|                                         <input | ||||
|                                             type="radio" | ||||
|                                             name="fps" | ||||
|                                             value={fps} | ||||
|                                             checked={settings.fps === fps} | ||||
|                                             onChange={() => setSettings(s => ({ ...s, fps }))} | ||||
|                                         /> | ||||
|                                     </label> | ||||
|                                 ))} | ||||
|                             </div> | ||||
|                         </section> | ||||
|                     </div> | ||||
|                     <div className="vcd-screen-picker-quality"> | ||||
|                         <section> | ||||
|                             <Forms.FormTitle>Content Type</Forms.FormTitle> | ||||
|                             <div> | ||||
|                                 <div className="vcd-screen-picker-radios"> | ||||
|                                     <label | ||||
|                                         className="vcd-screen-picker-radio" | ||||
|                                         data-checked={settings.contentHint === "motion"} | ||||
|                                     > | ||||
|                                         <Text variant="text-sm/bold">Prefer Smoothness</Text> | ||||
|                                         <input | ||||
|                                             type="radio" | ||||
|                                             name="contenthint" | ||||
|                                             value="motion" | ||||
|                                             checked={settings.contentHint === "motion"} | ||||
|                                             onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))} | ||||
|                                         /> | ||||
|                                     </label> | ||||
|                                     <label | ||||
|                                         className="vcd-screen-picker-radio" | ||||
|                                         data-checked={settings.contentHint === "detail"} | ||||
|                                     > | ||||
|                                         <Text variant="text-sm/bold">Prefer Clarity</Text> | ||||
|                                         <input | ||||
|                                             type="radio" | ||||
|                                             name="contenthint" | ||||
|                                             value="detail" | ||||
|                                             checked={settings.contentHint === "detail"} | ||||
|                                             onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))} | ||||
|                                         /> | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                                 <div className="vcd-screen-picker-hint-description"> | ||||
|                                     <p> | ||||
|                                         Choosing "Prefer Clarity" will result in a significantly lower framerate in | ||||
|                                         exchange for a much sharper and clearer image. | ||||
|                                     </p> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             {isWindows && ( | ||||
|                                 <Switch | ||||
|                                     value={settings.audio} | ||||
|                                     onChange={checked => setSettings(s => ({ ...s, audio: checked }))} | ||||
|                                     hideBorder | ||||
|                                     className="vcd-screen-picker-audio" | ||||
|                                 <label | ||||
|                                     className="vcd-screen-picker-radio" | ||||
|                                     data-checked={settings.contentHint === "motion"} | ||||
|                                 > | ||||
|                                     Stream With Audio | ||||
|                                 </Switch> | ||||
|                             )} | ||||
|                         </section> | ||||
|                     </div> | ||||
|                 </Card> | ||||
|             </div> | ||||
|                                     <Text variant="text-sm/bold">Prefer Smoothness</Text> | ||||
|                                     <input | ||||
|                                         type="radio" | ||||
|                                         name="contenthint" | ||||
|                                         value="motion" | ||||
|                                         checked={settings.contentHint === "motion"} | ||||
|                                         onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))} | ||||
|                                     /> | ||||
|                                 </label> | ||||
|                                 <label | ||||
|                                     className="vcd-screen-picker-radio" | ||||
|                                     data-checked={settings.contentHint === "detail"} | ||||
|                                 > | ||||
|                                     <Text variant="text-sm/bold">Prefer Clarity</Text> | ||||
|                                     <input | ||||
|                                         type="radio" | ||||
|                                         name="contenthint" | ||||
|                                         value="detail" | ||||
|                                         checked={settings.contentHint === "detail"} | ||||
|                                         onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))} | ||||
|                                     /> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                             <div className="vcd-screen-picker-hint-description"> | ||||
|                                 <p> | ||||
|                                     Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange | ||||
|                                     for a much sharper and clearer image. | ||||
|                                 </p> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         {isWindows && ( | ||||
|                             <Switch | ||||
|                                 value={settings.audio} | ||||
|                                 onChange={checked => setSettings(s => ({ ...s, audio: checked }))} | ||||
|                                 hideBorder | ||||
|                                 className="vcd-screen-picker-audio" | ||||
|                             > | ||||
|                                 Stream With Audio | ||||
|                             </Switch> | ||||
|                         )} | ||||
|                     </section> | ||||
|                 </div> | ||||
| 
 | ||||
|             <div> | ||||
|                 {isLinux && ( | ||||
|                     <AudioSourcePickerLinux | ||||
|                         audioSource={settings.audioSource} | ||||
|                         workaround={settings.workaround} | ||||
|                         onlyDefaultSpeakers={settings.onlyDefaultSpeakers} | ||||
|                         setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))} | ||||
|                         setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))} | ||||
|                         setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))} | ||||
|                         openSettings={openSettings} | ||||
|                         includeSources={settings.includeSources} | ||||
|                         excludeSources={settings.excludeSources} | ||||
|                         granularSelect={Settings.audio?.granularSelect} | ||||
|                         setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))} | ||||
|                         setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))} | ||||
|                     /> | ||||
|                 )} | ||||
|             </div> | ||||
|             </Card> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| function isSpecialSource(value?: AudioSource | AudioSources): value is SpecialSource { | ||||
|     return typeof value === "string"; | ||||
| } | ||||
| 
 | ||||
| function hasMatchingProps(value: Node, other: Node) { | ||||
|     return Object.keys(value).every(key => value[key] === other[key]); | ||||
| } | ||||
| 
 | ||||
| function mapToAudioItem(node: AudioSource, granularSelect?: boolean): AudioItem[] { | ||||
|     if (isSpecialSource(node)) { | ||||
|         return [{ name: node, value: node }]; | ||||
|     } | ||||
| 
 | ||||
|     const rtn: AudioItem[] = []; | ||||
| 
 | ||||
|     const name = node["application.name"]; | ||||
| 
 | ||||
|     if (name) { | ||||
|         rtn.push({ name: name, value: { "application.name": name } }); | ||||
|     } | ||||
| 
 | ||||
|     if (!granularSelect) { | ||||
|         return rtn; | ||||
|     } | ||||
| 
 | ||||
|     const binary = node["application.process.binary"]; | ||||
| 
 | ||||
|     if (!name) { | ||||
|         rtn.push({ name: binary, value: { "application.process.binary": binary } }); | ||||
|     } | ||||
| 
 | ||||
|     const pid = node["application.process.id"]; | ||||
| 
 | ||||
|     const first = rtn[0]; | ||||
|     const firstValues = first.value as Node; | ||||
| 
 | ||||
|     rtn.push({ | ||||
|         name: `${first.name} (${pid})`, | ||||
|         value: { ...firstValues, "application.process.id": pid } | ||||
|     }); | ||||
| 
 | ||||
|     const mediaName = node["media.name"]; | ||||
| 
 | ||||
|     if (mediaName) { | ||||
|         rtn.push({ | ||||
|             name: `${first.name} [${mediaName}]`, | ||||
|             value: { ...firstValues, "media.name": mediaName } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     const mediaClass = node["media.class"]; | ||||
| 
 | ||||
|     if (!mediaClass) { | ||||
|         return rtn; | ||||
|     } | ||||
| 
 | ||||
|     rtn.push({ | ||||
|         name: `${first.name} [${mediaClass}]`, | ||||
|         value: { ...firstValues, "media.class": mediaClass } | ||||
|     }); | ||||
| 
 | ||||
|     return rtn; | ||||
| } | ||||
| 
 | ||||
| function isItemSelected(sources?: AudioSources) { | ||||
|     return (value: AudioSource) => { | ||||
|         if (!sources) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (isSpecialSource(sources) || isSpecialSource(value)) { | ||||
|             return sources === value; | ||||
|         } | ||||
| 
 | ||||
|         return sources.some(source => hasMatchingProps(source, value)); | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| function updateItems(setSources: (s: AudioSources) => void, sources?: AudioSources) { | ||||
|     return (value: AudioSource) => { | ||||
|         if (isSpecialSource(value)) { | ||||
|             setSources(value); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (isSpecialSource(sources)) { | ||||
|             setSources([value]); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (isItemSelected(sources)(value)) { | ||||
|             setSources(sources?.filter(x => !hasMatchingProps(x, value)) ?? "None"); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         setSources([...(sources || []), value]); | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| function AudioSourcePickerLinux({ | ||||
|     audioSource, | ||||
|     workaround, | ||||
|     onlyDefaultSpeakers, | ||||
|     setAudioSource, | ||||
|     setWorkaround, | ||||
|     setOnlyDefaultSpeakers | ||||
|     includeSources, | ||||
|     excludeSources, | ||||
|     granularSelect, | ||||
|     openSettings, | ||||
|     setIncludeSources, | ||||
|     setExcludeSources | ||||
| }: { | ||||
|     audioSource?: string; | ||||
|     workaround?: boolean; | ||||
|     onlyDefaultSpeakers?: boolean; | ||||
|     setAudioSource(s: string): void; | ||||
|     setWorkaround(b: boolean): void; | ||||
|     setOnlyDefaultSpeakers(b: boolean): void; | ||||
|     includeSources?: AudioSources; | ||||
|     excludeSources?: AudioSources; | ||||
|     granularSelect?: boolean; | ||||
|     openSettings: () => void; | ||||
|     setIncludeSources: (s: AudioSources) => void; | ||||
|     setExcludeSources: (s: AudioSources) => void; | ||||
| }) { | ||||
|     const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { | ||||
|         fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } | ||||
|     }); | ||||
| 
 | ||||
|     const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null; | ||||
|     const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; | ||||
| 
 | ||||
|     const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); | ||||
| 
 | ||||
|     if (!sources.ok && sources.isGlibCxxOutdated) { | ||||
|         return ( | ||||
|             <Forms.FormText> | ||||
|                 Failed to retrieve Audio Sources because your C++ library is too old to run | ||||
|                 <a href="https://github.com/Vencord/venmic" target="_blank"> | ||||
|                     venmic | ||||
|                 </a> | ||||
|                 . See{" "} | ||||
|                 <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> | ||||
|                     this guide | ||||
|                 </a>{" "} | ||||
|                 for possible solutions. | ||||
|             </Forms.FormText> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     if (!hasPipewirePulse && !ignorePulseWarning) { | ||||
|         return ( | ||||
|             <Text variant="text-sm/normal"> | ||||
|                 Could not find pipewire-pulse. See{" "} | ||||
|                 <a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank"> | ||||
|                     this guide | ||||
|                 </a>{" "} | ||||
|                 on how to switch to pipewire. <br /> | ||||
|                 You can still continue, however, please{" "} | ||||
|                 <b>beware that you can only share audio of apps that are running under pipewire</b>.{" "} | ||||
|                 <a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a> | ||||
|             </Text> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     const specialSources: SpecialSource[] = ["None", "Entire System"] as const; | ||||
| 
 | ||||
|     const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) => | ||||
|         list.findIndex(x => x.name === value.name) === index; | ||||
| 
 | ||||
|     const allSources = sources.ok | ||||
|         ? [...specialSources, ...sources.targets] | ||||
|               .map(target => mapToAudioItem(target, granularSelect)) | ||||
|               .flat() | ||||
|               .filter(uniqueName) | ||||
|         : []; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Forms.FormTitle>Audio Settings</Forms.FormTitle> | ||||
|             <Card className="vcd-screen-picker-card"> | ||||
|                 {loading ? ( | ||||
|                     <Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle> | ||||
|                 ) : ( | ||||
|                     <Forms.FormTitle>Audio Source</Forms.FormTitle> | ||||
|                 )} | ||||
| 
 | ||||
|                 {!sources.ok && sources.isGlibCxxOutdated && ( | ||||
|                     <Forms.FormText> | ||||
|                         Failed to retrieve Audio Sources because your C++ library is too old to run | ||||
|                         <a href="https://github.com/Vencord/venmic" target="_blank"> | ||||
|                             venmic | ||||
|                         </a> | ||||
|                         . See{" "} | ||||
|                         <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> | ||||
|                             this guide | ||||
|                         </a>{" "} | ||||
|                         for possible solutions. | ||||
|                     </Forms.FormText> | ||||
|                 )} | ||||
| 
 | ||||
|                 {hasPipewirePulse || ignorePulseWarning ? ( | ||||
|                     allSources && ( | ||||
|             <div className={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}> | ||||
|                 <section> | ||||
|                     <Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle> | ||||
|                     <Select | ||||
|                         options={allSources.map(({ name, value }) => ({ | ||||
|                             label: name, | ||||
|                             value: value, | ||||
|                             default: name === "None" | ||||
|                         }))} | ||||
|                         isSelected={isItemSelected(includeSources)} | ||||
|                         select={updateItems(setIncludeSources, includeSources)} | ||||
|                         serialize={String} | ||||
|                         popoutPosition="top" | ||||
|                         closeOnSelect={false} | ||||
|                     /> | ||||
|                 </section> | ||||
|                 {includeSources === "Entire System" && ( | ||||
|                     <section> | ||||
|                         <Forms.FormTitle>Exclude Sources</Forms.FormTitle> | ||||
|                         <Select | ||||
|                             options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))} | ||||
|                             isSelected={s => s === audioSource} | ||||
|                             select={setAudioSource} | ||||
|                             options={allSources | ||||
|                                 .filter(x => x.name !== "Entire System") | ||||
|                                 .map(({ name, value }) => ({ | ||||
|                                     label: name, | ||||
|                                     value: value, | ||||
|                                     default: name === "None" | ||||
|                                 }))} | ||||
|                             isSelected={isItemSelected(excludeSources)} | ||||
|                             select={updateItems(setExcludeSources, excludeSources)} | ||||
|                             serialize={String} | ||||
|                             popoutPosition="top" | ||||
|                             closeOnSelect={false} | ||||
|                         /> | ||||
|                     ) | ||||
|                 ) : ( | ||||
|                     <Text variant="text-sm/normal"> | ||||
|                         Could not find pipewire-pulse. This usually means that you do not run pipewire as your main | ||||
|                         audio-server. <br /> | ||||
|                         You can still continue, however, please beware that you can only share audio of apps that are | ||||
|                         running under pipewire. | ||||
|                         <br /> | ||||
|                         <a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing</a> | ||||
|                     </Text> | ||||
|                     </section> | ||||
|                 )} | ||||
| 
 | ||||
|                 <Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} /> | ||||
| 
 | ||||
|                 <Switch | ||||
|                     onChange={setWorkaround} | ||||
|                     value={workaround ?? false} | ||||
|                     note={ | ||||
|                         <> | ||||
|                             Work around an issue that causes the microphone to be shared instead of the correct audio. | ||||
|                             Only enable if you're experiencing this issue. | ||||
|                         </> | ||||
|                     } | ||||
|                 > | ||||
|                     Microphone Workaround | ||||
|                 </Switch> | ||||
| 
 | ||||
|                 <Switch | ||||
|                     hideBorder | ||||
|                     onChange={setOnlyDefaultSpeakers} | ||||
|                     disabled={audioSource !== "Entire System"} | ||||
|                     value={onlyDefaultSpeakers ?? true} | ||||
|                     note={ | ||||
|                         <> | ||||
|                             When sharing entire desktop audio, only share apps that play to the default speakers and | ||||
|                             ignore apps that play to other speakers or devices. | ||||
|                         </> | ||||
|                     } | ||||
|                 > | ||||
|                     Only Default Speakers | ||||
|                 </Switch> | ||||
|             </Card> | ||||
|             </div> | ||||
|             <Button | ||||
|                 color={Button.Colors.TRANSPARENT} | ||||
|                 onClick={openSettings} | ||||
|                 className="vcd-screen-picker-settings-button" | ||||
|             > | ||||
|                 Open Audio Settings | ||||
|             </Button> | ||||
|         </> | ||||
|     ); | ||||
| } | ||||
|  | @ -424,7 +664,8 @@ function ModalComponent({ | |||
|         resolution: "1080", | ||||
|         fps: "60", | ||||
|         contentHint: "motion", | ||||
|         audio: true | ||||
|         audio: true, | ||||
|         includeSources: "None" | ||||
|     }); | ||||
| 
 | ||||
|     return ( | ||||
|  |  | |||
|  | @ -11,17 +11,6 @@ | |||
|     gap: 1em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-settings-grid { | ||||
|     gap: 1em; | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr 1fr; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-settings-grid>div { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-card { | ||||
|     flex-grow: 1; | ||||
| } | ||||
|  | @ -68,7 +57,7 @@ | |||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-preview-img-linux { | ||||
|     width: 100%; | ||||
|     width: 60%; | ||||
|     margin-bottom: 0.5em; | ||||
| } | ||||
| 
 | ||||
|  | @ -120,6 +109,11 @@ | |||
|     flex: 1 1 auto; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-settings-button { | ||||
|     margin-left: auto; | ||||
|     margin-top: 0.3rem; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radios { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|  | @ -148,4 +142,4 @@ | |||
|     font-size: 14px; | ||||
|     line-height: 20px; | ||||
|     font-weight: 400; | ||||
| } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										12
									
								
								src/shared/settings.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								src/shared/settings.d.ts
									
									
									
									
										vendored
									
									
								
							|  | @ -30,6 +30,18 @@ export interface Settings { | |||
|     splashBackground?: string; | ||||
| 
 | ||||
|     spellCheckLanguages?: string[]; | ||||
| 
 | ||||
|     audio?: { | ||||
|         workaround?: boolean; | ||||
|         granularSelect?: boolean; | ||||
| 
 | ||||
|         ignoreVirtual?: boolean; | ||||
|         ignoreDevices?: boolean; | ||||
|         ignoreInputMedia?: boolean; | ||||
| 
 | ||||
|         onlySpeakers?: boolean; | ||||
|         onlyDefaultSpeakers?: boolean; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface State { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Noah
						Noah