-
What you're streaming
-
-
- {source.name}
-
+
+
What you're streaming
+
+
+ {source.name}
+
-
Stream Settings
+
Stream Settings
-
-
-
- Resolution
+
+
+
+
+ Content Type
+
-
-
- {isWindows && (
- setSettings(s => ({ ...s, audio: checked }))}
- hideBorder
- className="vcd-screen-picker-audio"
- >
- Stream With Audio
-
- )}
+
+ {isWindows && (
+ setSettings(s => ({ ...s, audio: checked }))}
+ hideBorder
+ className="vcd-screen-picker-audio"
+ >
+ Stream With Audio
+
+ )}
+
+
{isLinux && (
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 }))}
/>
)}
-
+
);
}
+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 (
+
+ Failed to retrieve Audio Sources because your C++ library is too old to run
+
+ venmic
+
+ . See{" "}
+
+ this guide
+ {" "}
+ for possible solutions.
+
+ );
+ }
+
+ if (!hasPipewirePulse && !ignorePulseWarning) {
+ return (
+
+ Could not find pipewire-pulse. See{" "}
+
+ this guide
+ {" "}
+ on how to switch to pipewire.
+ You can still continue, however, please{" "}
+ beware that you can only share audio of apps that are running under pipewire .{" "}
+ setIgnorePulseWarning(true)}>I know what I'm doing!
+
+ );
+ }
+
+ 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 (
<>
-
Audio Settings
-
- {loading ? (
- Loading Audio Sources...
- ) : (
- Audio Source
- )}
-
- {!sources.ok && sources.isGlibCxxOutdated && (
-
- Failed to retrieve Audio Sources because your C++ library is too old to run
-
- venmic
-
- . See{" "}
-
- this guide
- {" "}
- for possible solutions.
-
- )}
-
- {hasPipewirePulse || ignorePulseWarning ? (
- allSources && (
+
+
+ {loading ? "Loading Sources..." : "Audio Sources"}
+ ({
+ label: name,
+ value: value,
+ default: name === "None"
+ }))}
+ isSelected={isItemSelected(includeSources)}
+ select={updateItems(setIncludeSources, includeSources)}
+ serialize={String}
+ popoutPosition="top"
+ closeOnSelect={false}
+ />
+
+ {includeSources === "Entire System" && (
+
+ Exclude Sources
({ 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}
/>
- )
- ) : (
-
- Could not find pipewire-pulse. This usually means that you do not run pipewire as your main
- audio-server.
- You can still continue, however, please beware that you can only share audio of apps that are
- running under pipewire.
-
- setIgnorePulseWarning(true)}>I know what I'm doing
-
+
)}
-
-
-
-
- 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
-
-
-
- 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
-
-
+
+
+ Open Audio Settings
+
>
);
}
@@ -432,10 +661,11 @@ function ModalComponent({
}) {
const [selected, setSelected] = useState(skipPicker ? screens[0].id : void 0);
const [settings, setSettings] = useState({
- resolution: "1080",
- fps: "60",
+ resolution: "720",
+ fps: "30",
contentHint: "motion",
- audio: true
+ audio: true,
+ includeSources: "None"
});
return (
diff --git a/src/renderer/components/screenSharePicker.css b/src/renderer/components/screenSharePicker.css
index 9233837..4f49323 100644
--- a/src/renderer/components/screenSharePicker.css
+++ b/src/renderer/components/screenSharePicker.css
@@ -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;
}
@@ -38,7 +27,7 @@
}
.vcd-screen-picker-selected img {
- border: 2px solid var(--brand-experiment);
+ border: 2px solid var(--brand-500);
border-radius: 3px;
}
@@ -49,7 +38,7 @@
}
.vcd-screen-picker-grid label:hover {
- outline: 2px solid var(--brand-experiment);
+ outline: 2px solid var(--brand-500);
}
@@ -67,8 +56,13 @@
box-sizing: border-box;
}
-.vcd-screen-picker-preview img {
- width: 100%;
+.vcd-screen-picker-preview-img-linux {
+ width: 60%;
+ margin-bottom: 0.5em;
+}
+
+.vcd-screen-picker-preview-img {
+ width: 90%;
margin-bottom: 0.5em;
}
@@ -96,8 +90,8 @@
}
.vcd-screen-picker-radio[data-checked="true"] {
- background-color: var(--brand-experiment);
- border-color: var(--brand-experiment);
+ background-color: var(--brand-500);
+ border-color: var(--brand-500);
}
.vcd-screen-picker-radio[data-checked="true"] h2 {
@@ -115,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%;
@@ -143,4 +142,4 @@
font-size: 14px;
line-height: 20px;
font-weight: 400;
-}
\ No newline at end of file
+}
diff --git a/src/renderer/index.ts b/src/renderer/index.ts
index cd0e00c..278b195 100644
--- a/src/renderer/index.ts
+++ b/src/renderer/index.ts
@@ -12,7 +12,7 @@ import "./themedSplash";
console.log("read if cute :3");
export * as Components from "./components";
-import { findByPropsLazy } from "@vencord/types/webpack";
+import { findByPropsLazy, onceReady } from "@vencord/types/webpack";
import { FluxDispatcher } from "@vencord/types/webpack/common";
import SettingsUi from "./components/settings/Settings";
@@ -54,8 +54,10 @@ const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as {
handleEvent(e: MessageEvent): void;
};
-VesktopNative.arrpc.onActivity(data => {
+VesktopNative.arrpc.onActivity(async data => {
if (!Settings.store.arRPC) return;
+ await onceReady;
+
arRPC.handleEvent(new MessageEvent("message", { data }));
});
diff --git a/src/renderer/patches/enableNotificationsByDefault.ts b/src/renderer/patches/enableNotificationsByDefault.ts
index f48115d..5854977 100644
--- a/src/renderer/patches/enableNotificationsByDefault.ts
+++ b/src/renderer/patches/enableNotificationsByDefault.ts
@@ -13,7 +13,7 @@ addPatch({
replacement: {
// FIXME: fix eslint rule
// eslint-disable-next-line no-useless-escape
- match: /\.isPlatformEmbedded(?=\?\i\.DesktopNotificationTypes\.ALL)/g,
+ match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g,
replace: "$&||true"
}
}
diff --git a/src/renderer/patches/hideSwitchDevice.tsx b/src/renderer/patches/hideSwitchDevice.tsx
index 911aed7..20aa51a 100644
--- a/src/renderer/patches/hideSwitchDevice.tsx
+++ b/src/renderer/patches/hideSwitchDevice.tsx
@@ -12,7 +12,7 @@ addPatch({
find: "lastOutputSystemDevice.justChanged",
replacement: {
// eslint-disable-next-line no-useless-escape
- match: /(\i)\.default\.getState\(\).neverShowModal/,
+ match: /(\i)\.\i\.getState\(\).neverShowModal/,
replace: "$& || $self.shouldIgnore($1)"
}
}
diff --git a/src/renderer/patches/spellCheck.tsx b/src/renderer/patches/spellCheck.tsx
index 9f0dbbd..040d41b 100644
--- a/src/renderer/patches/spellCheck.tsx
+++ b/src/renderer/patches/spellCheck.tsx
@@ -6,7 +6,8 @@
import { addContextMenuPatch } from "@vencord/types/api/ContextMenu";
import { findStoreLazy } from "@vencord/types/webpack";
-import { FluxDispatcher, Menu, useStateFromStores } from "@vencord/types/webpack/common";
+import { FluxDispatcher, Menu, useMemo, useStateFromStores } from "@vencord/types/webpack/common";
+import { useSettings } from "renderer/settings";
import { addPatch } from "./shared";
@@ -50,7 +51,16 @@ addContextMenuPatch("textarea-context", children => {
const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled());
const hasCorrections = Boolean(word && corrections?.length);
- children.push(
+ const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []);
+
+ const settings = useSettings();
+ const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]);
+
+ const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some(c => c?.props?.id === "paste"));
+
+ children.splice(
+ pasteSectionIndex === -1 ? children.length : pasteSectionIndex,
+ 0,
{hasCorrections && (
<>
@@ -69,14 +79,39 @@ addContextMenuPatch("textarea-context", children => {
/>
>
)}
- {
- FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
- }}
- />
+
+
+ {
+ FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
+ }}
+ />
+
+
+ {availableLanguages.map(lang => {
+ const isEnabled = spellCheckLanguages.includes(lang);
+ return (
+ = 5}
+ action={() => {
+ const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang);
+ if (newSpellCheckLanguages.length === spellCheckLanguages.length) {
+ newSpellCheckLanguages.push(lang);
+ }
+
+ settings.spellCheckLanguages = newSpellCheckLanguages;
+ }}
+ />
+ );
+ })}
+
+
);
});
diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts
index 05cbcc8..987e5c9 100644
--- a/src/shared/IpcEvents.ts
+++ b/src/shared/IpcEvents.ts
@@ -29,7 +29,7 @@ export const enum IpcEvents {
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
- SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES",
+ SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING",
SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",
diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts
index 7f6e74a..3eb96a0 100644
--- a/src/shared/settings.d.ts
+++ b/src/shared/settings.d.ts
@@ -21,8 +21,6 @@ export interface Settings {
appBadge?: boolean;
disableMinSize?: boolean;
clickTrayToShowHide?: boolean;
- /** @deprecated use customTitleBar */
- discordWindowsTitleBar?: boolean;
customTitleBar?: boolean;
checkUpdates?: boolean;
@@ -30,12 +28,27 @@ export interface Settings {
splashTheming?: boolean;
splashColor?: string;
splashBackground?: string;
+
+ spellCheckLanguages?: string[];
+
+ audio?: {
+ workaround?: boolean;
+ granularSelect?: boolean;
+
+ ignoreVirtual?: boolean;
+ ignoreDevices?: boolean;
+ ignoreInputMedia?: boolean;
+
+ onlySpeakers?: boolean;
+ onlyDefaultSpeakers?: boolean;
+ };
}
export interface State {
maximized?: boolean;
minimized?: boolean;
windowBounds?: Rectangle;
+ displayid: int;
skippedUpdate?: string;
firstLaunch?: boolean;
diff --git a/src/updater/main.ts b/src/updater/main.ts
index 207687e..4c19ffd 100644
--- a/src/updater/main.ts
+++ b/src/updater/main.ts
@@ -5,6 +5,7 @@
*/
import { app, BrowserWindow, shell } from "electron";
+import { PORTABLE } from "main/constants";
import { Settings, State } from "main/settings";
import { handle } from "main/utils/ipcWrappers";
import { makeLinksOpenExternally } from "main/utils/makeLinksOpenExternally";
@@ -23,17 +24,12 @@ let updateData: UpdateData;
handle(IpcEvents.UPDATER_GET_DATA, () => updateData);
handle(IpcEvents.UPDATER_DOWNLOAD, () => {
- const portable = !!process.env.PORTABLE_EXECUTABLE_FILE;
-
const { assets } = updateData.release;
const url = (() => {
switch (process.platform) {
case "win32":
return assets.find(a => {
- if (!a.name.endsWith(".exe")) return false;
-
- const isSetup = a.name.includes("Setup");
- return portable ? !isSetup : isSetup;
+ return a.name.endsWith(PORTABLE ? "win.zip" : ".exe");
})!.browser_download_url;
case "darwin":
return assets.find(a =>