This commit is contained in:
thororen1234 2024-07-18 00:53:55 -04:00
parent 7809f5b67c
commit 3c4d217312
108 changed files with 7134 additions and 38 deletions

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "AmITyping",
description: "Shows you if other people can see you typing.",
authors: [EquicordDevs.MrDiamond],
patches: [
{
find: "\"handleDismissInviteEducation\"",
replacement: {
match: /\i\.default\.getCurrentUser\(\)/,
replace: "\"\""
}
}
]
});

View file

@ -0,0 +1,100 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms, i18n, TextInput } from "@webpack/common";
function ReasonsComponent() {
const { reasons } = settings.use(["reasons"]);
return (
<Forms.FormSection title="Reasons">
{reasons.map((reason: string, index: number) => (
<div
className="vc-bbr-reason-wrapper"
>
<TextInput
type="text"
key={index}
value={reason}
onChange={v => {
reasons[index] = v;
settings.store.reasons = [...reasons];
}}
placeholder="Reason"
/>
<Button
color={Button.Colors.RED}
className="vc-bbr-remove-button"
onClick={() => {
reasons.splice(index, 1);
settings.store.reasons = [...reasons];
}}
>
Remove
</Button>
</div>
))}
<Button
onClick={() => {
settings.store.reasons = [...reasons, ""];
}}
>
Add new
</Button>
</Forms.FormSection>
);
}
const settings = definePluginSettings({
reasons: {
description: "Your custom reasons",
type: OptionType.COMPONENT,
default: [],
component: ReasonsComponent,
},
textInputDefault: {
type: OptionType.BOOLEAN,
description: 'Shows a text input instead of a select menu by default. (Equivalent to clicking the "Other" option)'
}
});
export default definePlugin({
name: "BetterBanReasons",
description: "Create custom reasons to use in the Discord ban modal, and/or show a text input by default instead of the options.",
authors: [Devs.Inbestigator],
patches: [
{
find: "Messages.BAN_MULTIPLE_CONFIRM_TITLE",
replacement: [{
match: /\[\{name:\i\.\i\.Messages\.BAN_REASON_OPTION_SPAM_ACCOUNT.+?\}\]/,
replace: "$self.getReasons()"
},
{
match: /useState\(0\)(?=.{0,100}targetUserId:)/,
replace: "useState($self.getDefaultState())"
}]
}
],
getReasons() {
const reasons = settings.store.reasons.length
? settings.store.reasons
: [
i18n.Messages.BAN_REASON_OPTION_SPAM_ACCOUNT,
i18n.Messages.BAN_REASON_OPTION_HACKED_ACCOUNT,
i18n.Messages.BAN_REASON_OPTION_BREAKING_RULES
];
return reasons.map(s => ({ name: s, value: s }));
},
getDefaultState() {
return settings.store.textInputDefault ? 1 : 0;
},
settings,
});

View file

@ -0,0 +1,11 @@
.vc-bbr-reason-wrapper {
display: grid;
padding: 0;
padding-bottom: 0.5rem;
gap: 0.5rem;
grid-template-columns: 6fr 1fr;
}
.vc-bbr-remove-button {
height: 100%;
}

View file

@ -0,0 +1,331 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { Switch } from "@components/Switch";
import { ModalSize } from "@utils/modal";
import { Card, Forms, Select, Slider, TextInput, useEffect, useState } from "@webpack/common";
import { SelectOption } from "@webpack/types";
import {
ProfilableStore,
SettingsModal,
SettingsModalCard,
SettingsModalCardItem,
SettingsModalCardRow,
SettingsModalProfilesCard,
validateNumberInput,
validateTextInputNumber
} from "../../philsPluginLibrary";
import { Styles } from "../../philsPluginLibrary/styles";
import { MicrophoneProfile, MicrophoneStore } from "../stores";
const simpleVoiceBitrates: readonly SelectOption[] = [
{
label: "Normal",
value: 96
},
{
label: "Medium-High",
value: 160
},
{
label: "High",
value: 320
},
{
label: "Very-High",
value: 512
}
] as const;
export interface MicrophoneSettingsModalProps extends React.ComponentProps<typeof SettingsModal> {
microphoneStore: ProfilableStore<MicrophoneStore, MicrophoneProfile>;
showInfo?: boolean;
}
export const MicrophoneSettingsModal = (props: MicrophoneSettingsModalProps) => {
const { microphoneStore, showInfo } = props;
const {
currentProfile,
simpleMode,
setSimpleMode,
deleteProfile,
duplicateProfile,
getCurrentProfile,
getDefaultProfiles,
getProfile,
getProfiles,
isCurrentProfileADefaultProfile,
profiles,
saveProfile,
setChannels,
setChannelsEnabled,
setCurrentProfile,
setFreq,
setFreqEnabled,
setPacsize,
setPacsizeEnabled,
setRate,
setRateEnabled,
setVoiceBitrate,
setVoiceBitrateEnabled
} = microphoneStore.use();
const {
name,
channels,
channelsEnabled,
freq,
freqEnabled,
pacsize,
pacsizeEnabled,
rate,
rateEnabled,
voiceBitrate,
voiceBitrateEnabled
} = currentProfile;
const [isSaving, setIsSaving] = useState(false);
const [rateInput, setRateInput] = useState<string>(rate ? rate.toString() : "");
const [freqInput, setFreqInput] = useState<string>(freq ? freq.toString() : "");
const [pacsizeInput, setPacsizeInput] = useState<string>(pacsize ? pacsize.toString() : "");
const [channelsInput, setChannelsInput] = useState<string>(channels ? channels.toString() : "");
useEffect(() => {
setRateInput(rate ? rate.toString() : "");
setFreqInput(freq ? freq.toString() : "");
setPacsizeInput(pacsize ? pacsize.toString() : "");
setChannelsInput(channels ? channels.toString() : "");
}, [rate, freq, pacsize, channels]);
const simpleToggle =
<Flex style={{ justifyContent: "center", alignItems: "center", gap: "0.6em" }}>
<Forms.FormTitle style={{ margin: 0 }} tag="h5">Simple</Forms.FormTitle>
<Switch checked={simpleMode ?? false} disabled={isSaving} onChange={checked => setSimpleMode(checked)} />
</Flex>;
const settingsCardVoiceBitrateSimple =
<SettingsModalCard
title="Audio Bitrate"
switchEnabled
flex={0.8}
switchProps={{
checked: voiceBitrateEnabled ?? false,
disabled: isSaving,
onChange: status => setVoiceBitrateEnabled(status)
}}>
<SettingsModalCardItem>
<Select
isDisabled={!voiceBitrateEnabled || isSaving}
options={simpleVoiceBitrates}
select={(value: number) => setVoiceBitrate(value)}
isSelected={(value: number) => value === voiceBitrate}
serialize={() => ""} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardChannelsSimple =
<SettingsModalCard
title="Stereo"
flex={0.2}
switchEnabled
switchProps={{
checked: (channelsEnabled && channels === 2) ?? false,
disabled: isSaving,
onChange: status => void setChannelsEnabled(status) ?? setChannels(2)
}}>
</SettingsModalCard>;
const settingsCardVoiceBitrate =
<SettingsModalCard
title="Audio Bitrate"
switchEnabled
flex={0.4}
switchProps={{
checked: voiceBitrateEnabled ?? false,
disabled: isSaving,
onChange: status => setVoiceBitrateEnabled(status)
}}>
<SettingsModalCardItem title="Kb/s">
<div style={{ paddingTop: "0.3em", paddingRight: "0.4em", paddingLeft: "0.4em", boxSizing: "border-box" }}>
<Slider
disabled={!voiceBitrateEnabled || isSaving}
onValueChange={value => setVoiceBitrate(value)}
initialValue={voiceBitrate || 8}
minValue={8}
maxValue={512}
markers={[8, 96, 320, 512]}
onValueRender={value => `${value.toFixed(0)}kb/s`} />
</div>
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardRate =
<SettingsModalCard
title="Sample Rate"
switchEnabled
switchProps={{
checked: rateEnabled ?? false,
disabled: isSaving,
onChange: status => setRateEnabled(status)
}}>
<SettingsModalCardItem>
<TextInput
disabled={!rateEnabled || isSaving}
value={rateInput}
onChange={value => validateTextInputNumber(value) && setRateInput(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setRate(result);
setRateInput(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardFreq =
<SettingsModalCard
title="Sample Frequency"
switchEnabled
switchProps={{
checked: freqEnabled ?? false,
disabled: isSaving,
onChange: status => setFreqEnabled(status)
}}>
<SettingsModalCardItem>
<TextInput
disabled={!freqEnabled || isSaving}
value={freqInput}
onChange={value => validateTextInputNumber(value) && setFreqInput(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setFreq(result);
setFreqInput(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardPacsize =
<SettingsModalCard
title="Pac Size"
switchEnabled
switchProps={{
checked: pacsizeEnabled ?? false,
disabled: isSaving,
onChange: status => setPacsizeEnabled(status)
}}>
<SettingsModalCardItem>
<TextInput
disabled={!pacsizeEnabled || isSaving}
value={pacsizeInput}
onChange={value => validateTextInputNumber(value) && setPacsizeInput(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setPacsize(result);
setPacsizeInput(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardChannels =
<SettingsModalCard
title="Channels"
switchEnabled
switchProps={{
checked: channelsEnabled ?? false,
disabled: isSaving,
onChange: status => setChannelsEnabled(status)
}}>
<SettingsModalCardItem>
<TextInput
disabled={!channelsEnabled || isSaving}
value={channelsInput}
onChange={value => validateTextInputNumber(value) && setChannelsInput(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setChannels(result);
setChannelsInput(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardProfiles =
<SettingsModalProfilesCard
flex={0.6}
onSaveStateChanged={state => setIsSaving(state)}
profileableStore={microphoneStore} />;
const infoCard =
<Card style={{ ...Styles.infoCard }}>
<Forms.FormTitle tag="h5">Important</Forms.FormTitle>
<Forms.FormText>
To take full advantage of this plugin, please disable <span style={{ fontWeight: "bold" }}>Krisp</span> and <span style={{ fontWeight: "bold" }}>Echo Cancellation</span>, otherwise features like Stereo (Channels) will not work.
</Forms.FormText>
</Card>;
return (
<SettingsModal
size={simpleMode ? ModalSize.DYNAMIC : ModalSize.DYNAMIC}
title="Microphone Settings"
closeButtonName="Apply"
footerContent={
<Flex style={{ justifyContent: "center", alignItems: "center", marginLeft: "auto" }}>
{simpleToggle}
</Flex>
}
{...props}
onDone={() => {
props.onClose();
props.onDone && props.onDone();
}}
>
{simpleMode
? <div style={{ width: "30em", display: "flex", flexDirection: "column", gap: "1em" }}>
<SettingsModalCardRow>
{settingsCardVoiceBitrateSimple}
{settingsCardChannelsSimple}
</SettingsModalCardRow>
{showInfo &&
<SettingsModalCardRow>
{infoCard}
</SettingsModalCardRow>
}
</div>
: <div style={{ display: "flex", flexDirection: "column", width: "50em", gap: "1em", maxHeight: "30em" }}>
<SettingsModalCardRow>
{settingsCardFreq}
{settingsCardRate}
{settingsCardPacsize}
{settingsCardChannels}
</SettingsModalCardRow>
<SettingsModalCardRow>
{settingsCardVoiceBitrate}
{settingsCardProfiles}
</SettingsModalCardRow>
{showInfo &&
<SettingsModalCardRow>
{infoCard}
</SettingsModalCardRow>
}
</div>
}
</SettingsModal>
);
};

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./MicrophoneSettingsModal";

View file

@ -0,0 +1,31 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import { types } from "../../philsPluginLibrary";
export const PluginInfo = {
PLUGIN_NAME: "BetterMicrophone",
DESCRIPTION: "This plugin allows you to further customize your microphone.",
AUTHOR: {
...Devs.philhk,
github: "https://github.com/philhk"
},
CONTRIBUTORS: {}
} as const satisfies types.PluginInfo;

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./constants";

View file

@ -0,0 +1,63 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { addSettingsPanelButton, Emitter, MicrophoneSettingsIcon, removeSettingsPanelButton } from "../philsPluginLibrary";
import { PluginInfo } from "./constants";
import { openMicrophoneSettingsModal } from "./modals";
import { MicrophonePatcher } from "./patchers";
import { initMicrophoneStore } from "./stores";
var microphonePatcher;
// const settings = definePluginSettings({
// openSettings: {
// description: "",
// type: OptionType.COMPONENT,
// component: (() => {
// return (
// <Button onClick={() => openMicrophoneSettingsModal()}>Open Settings</Button>
// );
// })
// }
// });
export default definePlugin({
name: "PhilsBetterMicrophone",
description: "This plugin allows you to further customize your microphone.",
authors: [Devs.philhk],
dependencies: ["PhilsPluginLibrary"],
start() {
initMicrophoneStore();
microphonePatcher = new MicrophonePatcher().patch();
addSettingsPanelButton({ name: PluginInfo.PLUGIN_NAME, icon: MicrophoneSettingsIcon, tooltipText: "Microphone Settings", onClick: openMicrophoneSettingsModal });
},
stop() {
microphonePatcher?.unpatch();
Emitter.removeAllListeners(PluginInfo.PLUGIN_NAME);
removeSettingsPanelButton(PluginInfo.PLUGIN_NAME);
},
// settings,
});
export { microphonePatcher };

View file

@ -0,0 +1,23 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { PluginInfo } from "../constants";
export const logger = new Logger(PluginInfo.PLUGIN_NAME);

View file

@ -0,0 +1,43 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { openModalLazy } from "@utils/modal";
import { MicrophoneSettingsModal } from "../components";
import { PluginInfo } from "../constants";
import Plugin from "../index";
import { microphoneStore } from "../stores";
const onMicrophoneModalDone = () => {
const { microphonePatcher } = Plugin;
if (microphonePatcher)
microphonePatcher.forceUpdateTransportationOptions();
};
export const openMicrophoneSettingsModal =
() => openModalLazy(async () => {
return props =>
<MicrophoneSettingsModal
onDone={onMicrophoneModalDone}
showInfo
microphoneStore={microphoneStore}
author={PluginInfo.AUTHOR}
contributors={Object.values(PluginInfo.CONTRIBUTORS)}
{...props} />;
});

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./microphone";

View file

@ -0,0 +1,72 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Emitter, MediaEngineStore, Patcher, types } from "../../philsPluginLibrary";
import { patchConnectionAudioTransportOptions } from "../../philsPluginLibrary/patches/audio";
import { PluginInfo } from "../constants";
import { logger } from "../logger";
import { microphoneStore } from "../stores";
export class MicrophonePatcher extends Patcher {
private mediaEngineStore: types.MediaEngineStore;
private mediaEngine: types.MediaEngine;
public connection?: types.Connection;
public oldSetTransportOptions: (...args: any[]) => void;
public forceUpdateTransportationOptions: () => void;
constructor() {
super();
this.mediaEngineStore = MediaEngineStore;
this.mediaEngine = this.mediaEngineStore.getMediaEngine();
this.oldSetTransportOptions = () => void 0;
this.forceUpdateTransportationOptions = () => void 0;
}
public patch(): this {
this.unpatch();
const { get } = microphoneStore;
const connectionEventFunction =
(connection: types.Connection) => {
if (connection.context !== "default") return;
this.connection = connection;
const { oldSetTransportOptions, forceUpdateTransportationOptions } = patchConnectionAudioTransportOptions(connection, get, logger);
this.oldSetTransportOptions = oldSetTransportOptions;
this.forceUpdateTransportationOptions = forceUpdateTransportationOptions;
};
Emitter.addListener(
this.mediaEngine.emitter,
"on",
// @ts-ignore
"connection",
connectionEventFunction,
PluginInfo.PLUGIN_NAME
);
return this;
}
public unpatch(): this {
return this._unpatch();
}
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./microphoneStore";

View file

@ -0,0 +1,94 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createPluginStore, ProfilableInitializer, ProfilableStore, profileable, ProfileableProfile } from "../../philsPluginLibrary";
import { PluginInfo } from "../constants";
export interface MicrophoneProfile {
freq?: number,
pacsize?: number,
channels?: number,
rate?: number,
voiceBitrate?: number;
freqEnabled?: boolean,
pacsizeEnabled?: boolean;
channelsEnabled?: boolean;
rateEnabled?: boolean;
voiceBitrateEnabled?: boolean;
}
export interface MicrophoneStore {
simpleMode?: boolean;
setSimpleMode: (enabled?: boolean) => void;
setFreq: (freq?: number) => void;
setPacsize: (pacsize?: number) => void;
setChannels: (channels?: number) => void;
setRate: (rate?: number) => void;
setVoiceBitrate: (voiceBitrate?: number) => void;
setFreqEnabled: (enabled?: boolean) => void;
setPacsizeEnabled: (enabled?: boolean) => void;
setChannelsEnabled: (enabled?: boolean) => void;
setRateEnabled: (enabled?: boolean) => void;
setVoiceBitrateEnabled: (enabled?: boolean) => void;
}
export const defaultMicrophoneProfiles = {
normal: {
name: "Normal",
channels: 2,
channelsEnabled: true,
voiceBitrate: 96,
voiceBitrateEnabled: true
},
high: {
name: "High",
channels: 2,
channelsEnabled: true,
voiceBitrate: 320,
voiceBitrateEnabled: true
},
} as const satisfies Record<string, MicrophoneProfile & ProfileableProfile>;
export const microphoneStoreDefault: ProfilableInitializer<MicrophoneStore, MicrophoneProfile> = (set, get) => ({
simpleMode: true,
setSimpleMode: enabled => get().simpleMode = enabled,
setChannels: channels => get().currentProfile.channels = channels,
setRate: rate => get().currentProfile.rate = rate,
setVoiceBitrate: voiceBitrate => get().currentProfile.voiceBitrate = voiceBitrate,
setPacsize: pacsize => get().currentProfile.pacsize = pacsize,
setFreq: freq => get().currentProfile.freq = freq,
setChannelsEnabled: enabled => get().currentProfile.channelsEnabled = enabled,
setFreqEnabled: enabled => get().currentProfile.freqEnabled = enabled,
setPacsizeEnabled: enabled => get().currentProfile.pacsizeEnabled = enabled,
setRateEnabled: enabled => get().currentProfile.rateEnabled = enabled,
setVoiceBitrateEnabled: enabled => get().currentProfile.voiceBitrateEnabled = enabled,
});
export let microphoneStore: ProfilableStore<MicrophoneStore, MicrophoneProfile>;
export const initMicrophoneStore = () =>
microphoneStore = createPluginStore(
PluginInfo.PLUGIN_NAME,
"MicrophoneStore",
profileable(
microphoneStoreDefault,
{ name: "" },
Object.values(defaultMicrophoneProfiles)
)
);

View file

@ -0,0 +1,62 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Select, useEffect, useState } from "@webpack/common";
import React from "react";
import { MediaEngineStore, types } from "../../philsPluginLibrary";
import { screenshareStore } from "../stores";
export const AudioSourceSelect = (props?: typeof Select["defaultProps"]) => {
const { use } = screenshareStore;
const { audioSource, setAudioSource } = use();
const [windowPreviews, setWindowPreviews] = useState<types.WindowPreview[]>([]);
useEffect(() => {
const intervalFn = async () => {
const newPreviews = await MediaEngineStore.getMediaEngine().getWindowPreviews(1, 1);
setWindowPreviews(oldPreviews => [...oldPreviews, ...newPreviews].filter((preview, index, array) => array.findIndex(t => t.id === preview.id) === index));
};
intervalFn();
const intervals = [
setInterval(async () => {
intervalFn();
}, 4000), setInterval(async () => {
setWindowPreviews(await MediaEngineStore.getMediaEngine().getWindowPreviews(1, 1));
}, 30000)
];
return () => intervals.forEach(interval => clearInterval(interval));
}, []);
return (
<Select
options={windowPreviews.map(({ name, id }) => ({
label: name,
value: id
}))}
isSelected={value => audioSource === value}
select={value => setAudioSource(value)}
serialize={() => ""}
{...props}
></Select>
);
};

View file

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button } from "@webpack/common";
import React from "react";
import { openScreenshareModal } from "../modals";
export interface OpenScreenshareSettingsButtonProps {
title?: string;
}
export const OpenScreenshareSettingsButton = (props: OpenScreenshareSettingsButtonProps) => {
return (
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
onClick={openScreenshareModal}
>
{props.title ? props.title : "Screenshare Settings"}
</Button>
);
};

View file

@ -0,0 +1,467 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { Switch } from "@components/Switch";
import { ModalSize, openModalLazy } from "@utils/modal";
import { Button, Card, Forms, React, Select, Slider, TextInput, useEffect, useState } from "@webpack/common";
import { SelectOption } from "@webpack/types";
import { MicrophoneSettingsModal } from "../../betterMicrophone.desktop/components";
import {
MediaEngineStore,
openURL,
ProfilableStore,
SettingsModal,
SettingsModalCard,
SettingsModalCardItem,
SettingsModalCardRow,
SettingsModalProfilesCard,
types,
validateNumberInput,
validateTextInputNumber
} from "../../philsPluginLibrary";
import { Styles } from "../../philsPluginLibrary/styles";
import { PluginInfo } from "../constants";
import { ScreenshareAudioProfile, ScreenshareAudioStore, ScreenshareProfile, ScreenshareStore } from "../stores";
const simpleResolutions: readonly (SelectOption & { value: types.Resolution; })[] = [
{
label: "480p",
value: {
height: 480,
width: 720
}
},
{
label: "720p",
value: {
height: 720,
width: 1280
}
},
{
label: "1080p",
value: {
height: 1080,
width: 1920
}
},
{
label: "1440p",
value: {
height: 1440,
width: 2560
}
},
{
label: "2160p",
value: {
height: 2160,
width: 3840
}
}
] as const;
const simpleVideoBitrates: readonly SelectOption[] = [
{
label: "Low",
value: 2500
},
{
label: "Medium",
value: 5000
},
{
label: "Medium-High",
value: 7500
},
{
label: "High",
value: 10000
}
] as const;
export interface ScreenshareSettingsModalProps extends React.ComponentProps<typeof SettingsModal> {
screenshareStore: ProfilableStore<ScreenshareStore, ScreenshareProfile>;
screenshareAudioStore?: ProfilableStore<ScreenshareAudioStore, ScreenshareAudioProfile>;
onAudioDone?: () => void;
}
export const ScreenshareSettingsModal = (props: ScreenshareSettingsModalProps) => {
const { screenshareStore, screenshareAudioStore, onAudioDone } = props;
const {
currentProfile,
profiles,
simpleMode,
setVideoBitrateEnabled,
setVideoCodec,
setVideoCodecEnabled,
setFramerate,
setFramerateEnabled,
setHeight,
setKeyframeInterval,
setKeyframeIntervalEnabled,
setResolutionEnabled,
setVideoBitrate,
setWidth,
setCurrentProfile,
getProfile,
saveProfile,
setHdrEnabled,
setSimpleMode,
deleteProfile,
duplicateProfile,
getCurrentProfile,
getProfiles
} = screenshareStore.use();
const {
name,
framerate,
framerateEnabled,
height,
keyframeInterval,
keyframeIntervalEnabled,
resolutionEnabled,
videoBitrate,
videoBitrateEnabled,
videoCodec,
videoCodecEnabled,
width,
hdrEnabled
} = currentProfile;
const [videoCodecs, setVideoCodecs] = useState<types.CodecCapabilities[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [textinputWidth, setTextinputWidth] = useState<string>(width ? width.toString() : "");
const [textinputHeight, setTextinputHeight] = useState<string>(height ? height.toString() : "");
const [textinputFramerate, setTextinputFramerate] = useState<string>(framerate ? framerate.toString() : "");
const [textinputKeyframeInterval, setTextinputKeyframeInterval] = useState<string>(keyframeInterval ? keyframeInterval.toString() : "");
useEffect(() => {
setTextinputWidth(width ? width.toString() : "");
setTextinputHeight(height ? height.toString() : "");
setTextinputFramerate(framerate ? framerate.toString() : "");
setTextinputKeyframeInterval(keyframeInterval ? keyframeInterval.toString() : "");
}, [width, height, framerate, keyframeInterval]);
useEffect(() => {
(async () => {
const mediaEngine = MediaEngineStore.getMediaEngine();
const stringifiedCodecs: types.CodecCapabilities[] = JSON.parse(
await new Promise(res => mediaEngine.getCodecCapabilities(res))
);
setVideoCodecs(stringifiedCodecs);
})();
}, []);
const settingsCardResolutionSimple =
<SettingsModalCard
title="Resolution"
switchEnabled
switchProps={{
checked: resolutionEnabled ?? false,
disabled: isSaving,
onChange: status => setResolutionEnabled(status)
}}>
<SettingsModalCardItem>
<Select
isDisabled={!resolutionEnabled || isSaving}
options={simpleResolutions}
select={(value: types.Resolution) => void setWidth(value.width) ?? setHeight(value.height)}
isSelected={(value: types.Resolution) => width === value.width && height === value.height}
serialize={() => ""} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardVideoBitrateSimple =
<SettingsModalCard
title="Video Bitrate"
switchEnabled
switchProps={{
checked: videoBitrateEnabled ?? false,
disabled: isSaving,
onChange: status => setVideoBitrateEnabled(status)
}}>
<SettingsModalCardItem>
<Select
isDisabled={!videoBitrateEnabled || isSaving}
options={simpleVideoBitrates}
select={(value: number) => void setVideoBitrate(value)}
isSelected={(value: number) => videoBitrate === value}
serialize={() => ""} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardResolution =
<SettingsModalCard
title="Resolution"
flex={0.5}
switchEnabled
switchProps={{
checked: resolutionEnabled ?? false,
onChange: status => setResolutionEnabled(status),
disabled: isSaving
}}>
<SettingsModalCardItem title="Width">
<TextInput
disabled={!resolutionEnabled || isSaving}
value={textinputWidth}
onChange={value => validateTextInputNumber(value) && setTextinputWidth(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setWidth(result);
setTextinputWidth(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
<SettingsModalCardItem title="Height">
<TextInput
disabled={!resolutionEnabled || isSaving}
value={textinputHeight}
onChange={value => validateTextInputNumber(value) && setTextinputHeight(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setHeight(result);
setTextinputHeight(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardItemFramerate =
<SettingsModalCardItem>
<TextInput
disabled={!framerateEnabled || isSaving}
value={textinputFramerate}
onChange={value => validateTextInputNumber(value) && setTextinputFramerate(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setFramerate(result);
setTextinputFramerate(result ? result.toString() : "");
}} />
</SettingsModalCardItem>;
const settingsCardFramerateProps: React.ComponentProps<typeof SettingsModalCard> = {
title: "Framerate",
switchEnabled: true,
switchProps: {
checked: framerateEnabled ?? false,
disabled: isSaving,
onChange: status => setFramerateEnabled(status)
}
};
const settingsCardFramerate =
<SettingsModalCard
{...settingsCardFramerateProps}
flex={0.25}
>
{settingsCardItemFramerate}
</SettingsModalCard>;
const settingsCardFramerateSimple =
<SettingsModalCard
{...settingsCardFramerateProps}
>
{settingsCardItemFramerate}
</SettingsModalCard>;
const settingsCardKeyframeInterval =
<SettingsModalCard
title="Keyframe Interval (ms)"
flex={0.25}
switchEnabled
switchProps={{
checked: keyframeIntervalEnabled ?? false,
disabled: isSaving,
onChange: status => setKeyframeIntervalEnabled(status)
}}>
<SettingsModalCardItem>
<TextInput
disabled={!keyframeIntervalEnabled || isSaving}
value={textinputKeyframeInterval}
onChange={value => validateTextInputNumber(value) && setTextinputKeyframeInterval(value)}
onBlur={e => {
const result = validateNumberInput(e.target.value);
setKeyframeInterval(result);
setTextinputKeyframeInterval(result ? result.toString() : "");
}} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardVideoBitrate =
<SettingsModalCard
title="Video Bitrate"
flex={0.4}
switchEnabled
switchProps={{
checked: videoBitrateEnabled ?? false,
disabled: isSaving,
onChange: status => setVideoBitrateEnabled(status)
}}>
<SettingsModalCardItem title="Kb/s">
<div style={{ paddingTop: "0.3em", paddingRight: "0.4em", paddingLeft: "0.4em", boxSizing: "border-box" }}>
<Slider
disabled={!videoBitrateEnabled || isSaving}
onValueChange={value => setVideoBitrate(value)}
initialValue={videoBitrate || 500}
minValue={500}
maxValue={10000}
markers={[500, 10000]}
onValueRender={value => `${value.toFixed(0)}kb/s`} />
</div>
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardAudioProps: React.ComponentProps<typeof SettingsModalCard> = {
title: "Audio Settings"
};
const settingsCardItemAudio =
<SettingsModalCardItem >
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.SMALL}
onClick={() => {
if (screenshareAudioStore)
openModalLazy(async () => {
return props_ =>
<MicrophoneSettingsModal
author={props.author}
contributors={props.contributors}
title="Screenshare Audio Settings"
onDone={onAudioDone}
microphoneStore={screenshareAudioStore}
{...props_} />;
});
}}
children={"Open"} />
</SettingsModalCardItem>;
const settingsCardAudio =
<SettingsModalCard
{...settingsCardAudioProps}
flex={0.2}>
{settingsCardItemAudio}
</SettingsModalCard>;
const settingsCardAudioSimple =
<SettingsModalCard
{...settingsCardAudioProps}>
{settingsCardItemAudio}
</SettingsModalCard>;
const settingsCardVideoCodec =
<SettingsModalCard
title="Video Codec"
flex={0.4}
switchEnabled
switchProps={{
checked: videoCodecEnabled ?? false,
disabled: isSaving,
onChange: status => setVideoCodecEnabled(status)
}}>
<SettingsModalCardItem>
<Select
isDisabled={!videoCodecEnabled || isSaving}
isSelected={value => value === videoCodec}
options={videoCodecs.map(codecCapabilities => ({ label: codecCapabilities.codec, value: codecCapabilities.codec }))}
select={value => setVideoCodec(value)}
serialize={() => ""} />
</SettingsModalCardItem>
</SettingsModalCard>;
const settingsCardHdr =
<SettingsModalCard
title="Hdr"
flex={0.1}
switchEnabled
switchProps={{
checked: hdrEnabled ?? false,
disabled: isSaving,
onChange: status => setHdrEnabled(status)
}} />;
const guideCard =
<Card style={{ ...Styles.infoCard, flex: 0.4 }}>
<Forms.FormTitle tag="h5">How to use?</Forms.FormTitle>
<Forms.FormText>If you want to know more about the settings or possible issues, please read <a onClick={() => openURL(PluginInfo.README + "#better-screenshare-plugin")}>this</a>.</Forms.FormText>
</Card>;
const settingsCardProfiles =
<SettingsModalProfilesCard flex={0.5} onSaveStateChanged={state => setIsSaving(state)} profileableStore={screenshareStore} />;
const simpleToggle =
<Flex style={{ justifyContent: "center", alignItems: "center", gap: "0.6em" }}>
<Forms.FormTitle style={{ margin: 0 }} tag="h5">Simple</Forms.FormTitle>
<Switch checked={simpleMode ?? false} disabled={isSaving} onChange={checked => setSimpleMode(checked)} />
</Flex>;
return (
<SettingsModal
size={simpleMode ? ModalSize.DYNAMIC : ModalSize.LARGE}
title="Screenshare Settings"
closeButtonName="Apply"
footerContent={
<Flex style={{ justifyContent: "center", alignItems: "center", marginLeft: "auto" }}>
{simpleToggle}
</Flex>
}
{...props}
onDone={() => {
props.onClose();
props.onDone && props.onDone();
}}
>
{simpleMode
? <div style={{ width: "55em" }}>
<SettingsModalCardRow>
{settingsCardResolutionSimple}
{settingsCardFramerateSimple}
{settingsCardVideoBitrateSimple}
{screenshareAudioStore && settingsCardAudioSimple}
</SettingsModalCardRow>
</div>
: <>
<SettingsModalCardRow>
{settingsCardResolution}
{settingsCardFramerate}
{settingsCardKeyframeInterval}
</SettingsModalCardRow>
<SettingsModalCardRow>
{settingsCardVideoBitrate}
{settingsCardVideoCodec}
{screenshareAudioStore && settingsCardAudio}
</SettingsModalCardRow>
<SettingsModalCardRow>
{guideCard}
{settingsCardHdr}
{settingsCardProfiles}
</SettingsModalCardRow>
</>
}
</SettingsModal>
);
};

View file

@ -0,0 +1,21 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./AudioSourceSelect";
export * from "./OpenScreenshareSettingsButton";
export * from "./ScreenshareSettingsModal";

View file

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import { types } from "../../philsPluginLibrary";
export const PluginInfo = {
PLUGIN_NAME: "BetterScreenshare",
DESCRIPTION: "This plugin allows you to further customize your screen sharing.",
AUTHOR: {
...Devs.philhk,
github: "https://github.com/philhk"
},
CONTRIBUTORS: {
walrus: {
github: "https://github.com/philhk",
id: 305317288775778306n,
name: "walrus"
},
},
README: "https://github.com/Vendicated/Vencord/tree/main/src/plugins/betterScreenshare"
} as const satisfies types.PluginInfo;

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./constants";

View file

@ -0,0 +1,71 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { addSettingsPanelButton, Emitter, removeSettingsPanelButton, ScreenshareSettingsIcon } from "../philsPluginLibrary";
import { PluginInfo } from "./constants";
import { openScreenshareModal } from "./modals";
import { ScreenshareAudioPatcher, ScreensharePatcher } from "./patchers";
import { replacedScreenshareModalComponent } from "./patches";
import { initScreenshareAudioStore, initScreenshareStore } from "./stores";
var screensharePatcher;
var screenshareAudioPatcher;
export default definePlugin({
name: "PhilsBetterScreenshare",
description: "This plugin allows you to further customize your screen sharing.",
authors: [Devs.philhk, Devs.walrus],
dependencies: ["PhilsPluginLibrary"],
replacedScreenshareModalComponent: replacedScreenshareModalComponent,
patches: [
{
find: "Messages.SCREENSHARE_RELAUNCH",
replacement: {
match: /(function .{1,2}\(.{1,2}\){)(.{1,40}(?=selectGuild).+?(?:]}\)}\)))(})/,
replace: "$1return $self.replacedScreenshareModalComponent(function(){$2}, this, arguments)$3"
}
}
],
start() {
initScreenshareStore();
initScreenshareAudioStore();
screensharePatcher = new ScreensharePatcher().patch();
screenshareAudioPatcher = new ScreenshareAudioPatcher().patch();
addSettingsPanelButton({
name: PluginInfo.PLUGIN_NAME,
icon: ScreenshareSettingsIcon,
tooltipText: "Screenshare Settings",
onClick: openScreenshareModal
});
},
stop() {
screensharePatcher?.unpatch();
screenshareAudioPatcher?.unpatch();
Emitter.removeAllListeners(PluginInfo.PLUGIN_NAME);
removeSettingsPanelButton(PluginInfo.PLUGIN_NAME);
},
});
export { screenshareAudioPatcher, screensharePatcher };

View file

@ -0,0 +1,23 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { PluginInfo } from "../constants";
export const logger = new Logger(PluginInfo.PLUGIN_NAME);

View file

@ -0,0 +1,55 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { openModalLazy } from "@utils/modal";
import Plugin from "..";
import { ScreenshareSettingsModal } from "../components";
import { PluginInfo } from "../constants";
import { screenshareAudioStore, screenshareStore } from "../stores";
const onScreenshareModalDone = () => {
const { screenshareAudioPatcher, screensharePatcher } = Plugin;
if (screensharePatcher) {
screensharePatcher.forceUpdateTransportationOptions();
screensharePatcher.forceUpdateDesktopSourceOptions();
}
if (screenshareAudioPatcher)
screenshareAudioPatcher.forceUpdateTransportationOptions();
};
const onScreenshareAudioModalDone = () => {
const { screenshareAudioPatcher } = Plugin;
if (screenshareAudioPatcher)
screenshareAudioPatcher.forceUpdateTransportationOptions();
};
export const openScreenshareModal =
() => openModalLazy(async () => {
return props =>
<ScreenshareSettingsModal
onAudioDone={onScreenshareAudioModalDone}
onDone={onScreenshareModalDone}
screenshareStore={screenshareStore}
screenshareAudioStore={screenshareAudioStore}
author={PluginInfo.AUTHOR}
contributors={Object.values(PluginInfo.CONTRIBUTORS)}
{...props} />;
});

View file

@ -0,0 +1,20 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./screenshare";
export * from "./screenshareAudio";

View file

@ -0,0 +1,99 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { UserStore } from "@webpack/common";
import { Emitter, MediaEngineStore, Patcher, types } from "../../philsPluginLibrary";
import { patchConnectionVideoSetDesktopSourceWithOptions, patchConnectionVideoTransportOptions } from "../../philsPluginLibrary/patches/video";
import { PluginInfo } from "../constants";
import { logger } from "../logger";
import { screenshareStore } from "../stores";
export class ScreensharePatcher extends Patcher {
private mediaEngineStore: types.MediaEngineStore;
private mediaEngine: types.MediaEngine;
public connection?: types.Connection;
public oldSetDesktopSourceWithOptions: (...args: any[]) => void;
public oldSetTransportOptions: (...args: any[]) => void;
public forceUpdateTransportationOptions: () => void;
public forceUpdateDesktopSourceOptions: () => void;
constructor() {
super();
this.mediaEngineStore = MediaEngineStore;
this.mediaEngine = this.mediaEngineStore.getMediaEngine();
this.forceUpdateTransportationOptions = () => void 0;
this.forceUpdateDesktopSourceOptions = () => void 0;
this.oldSetDesktopSourceWithOptions = () => void 0;
this.oldSetTransportOptions = () => void 0;
}
public patch(): this {
this.unpatch();
const { get } = screenshareStore;
const connectionEventFunction =
(connection: types.Connection) => {
if (!(connection.context === "stream" && connection.streamUserId === UserStore.getCurrentUser().id)) return;
this.connection = connection;
const {
oldSetDesktopSourceWithOptions,
oldSetTransportOptions,
forceUpdateDesktopSourceOptions,
forceUpdateTransportationOptions
} = {
...patchConnectionVideoTransportOptions(connection, get, logger),
...patchConnectionVideoSetDesktopSourceWithOptions(connection, get, logger)
};
this.oldSetDesktopSourceWithOptions = oldSetDesktopSourceWithOptions;
this.oldSetTransportOptions = oldSetTransportOptions;
this.forceUpdateDesktopSourceOptions = forceUpdateDesktopSourceOptions;
this.forceUpdateTransportationOptions = forceUpdateTransportationOptions;
Emitter.addListener(connection.emitter, "on", "connected", () => {
this.forceUpdateTransportationOptions();
this.forceUpdateDesktopSourceOptions();
});
Emitter.addListener(connection.emitter, "on", "destroy", () => {
this.forceUpdateTransportationOptions = () => void 0;
this.forceUpdateDesktopSourceOptions = () => void 0;
this.oldSetTransportOptions = () => void 0;
this.oldSetDesktopSourceWithOptions = () => void 0;
});
};
Emitter.addListener(
this.mediaEngine.emitter,
"on",
"connection",
connectionEventFunction,
PluginInfo.PLUGIN_NAME
);
return this;
}
public unpatch(): this {
return this._unpatch();
}
}

View file

@ -0,0 +1,85 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { UserStore } from "@webpack/common";
import { Emitter, MediaEngineStore, patchConnectionAudioTransportOptions, Patcher, types } from "../../philsPluginLibrary";
import { PluginInfo } from "../constants";
import { logger } from "../logger";
import { screenshareAudioStore } from "../stores/screenshareAudioStore";
export class ScreenshareAudioPatcher extends Patcher {
private mediaEngineStore: types.MediaEngineStore;
private mediaEngine: types.MediaEngine;
public connection?: types.Connection;
public oldSetTransportOptions: (...args: any[]) => void;
public forceUpdateTransportationOptions: () => void;
constructor() {
super();
this.mediaEngineStore = MediaEngineStore;
this.mediaEngine = this.mediaEngineStore.getMediaEngine();
this.forceUpdateTransportationOptions = () => void 0;
this.oldSetTransportOptions = () => void 0;
}
public patch(): this {
this.unpatch();
const { get } = screenshareAudioStore;
const connectionEventFunction =
(connection: types.Connection) => {
if (connection.context !== "stream" || connection.streamUserId !== UserStore.getCurrentUser().id) return;
this.connection = connection;
const {
forceUpdateTransportationOptions: forceUpdateTransportationOptionsAudio,
oldSetTransportOptions: oldSetTransportOptionsAudio
} = patchConnectionAudioTransportOptions(connection, get, logger);
this.forceUpdateTransportationOptions = forceUpdateTransportationOptionsAudio;
this.oldSetTransportOptions = oldSetTransportOptionsAudio;
Emitter.addListener(connection.emitter, "on", "connected", () => {
this.forceUpdateTransportationOptions();
});
Emitter.addListener(connection.emitter, "on", "destroy", () => {
this.forceUpdateTransportationOptions = () => void 0;
});
};
Emitter.addListener(
this.mediaEngine.emitter,
"on",
"connection",
connectionEventFunction,
PluginInfo.PLUGIN_NAME
);
return this;
}
public unpatch(): this {
return this._unpatch();
}
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./screenshareModal";

View file

@ -0,0 +1,98 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { React } from "@webpack/common";
import { Settings } from "Vencord";
import { SettingsModalCard, SettingsModalCardItem } from "../../philsPluginLibrary";
import Plugin from "..";
import { AudioSourceSelect, OpenScreenshareSettingsButton } from "../components";
import { PluginInfo } from "../constants";
import { screenshareStore } from "../stores";
const ReplacedStreamSettings = () => {
const { use } = screenshareStore;
const { audioSourceEnabled, setAudioSourceEnabled } = use();
const cardProps = { style: { border: "1px solid var(--primary-800)" } };
return (
<div style={{ margin: "1em", display: "flex", flexDirection: "column", gap: "1em" }}>
<SettingsModalCard cardProps={cardProps} title="Stream Settings">
<SettingsModalCardItem>
<Flex flexDirection="column">
<OpenScreenshareSettingsButton title="Advanced Settings" />
</Flex>
</SettingsModalCardItem>
</SettingsModalCard>
<SettingsModalCard
cardProps={cardProps}
switchEnabled
switchProps={{
checked: audioSourceEnabled ?? false,
onChange: status => setAudioSourceEnabled(status)
}}
title="Audio Source">
<SettingsModalCardItem>
<AudioSourceSelect isDisabled={!audioSourceEnabled} />
</SettingsModalCardItem>
</SettingsModalCard>
</div>
);
};
export function replacedScreenshareModalSettingsContentType(oldType: (...args: any[]) => any, thisContext: any, functionArguments: any) {
const { hideDefaultSettings } = Settings.plugins[PluginInfo.PLUGIN_NAME];
const oldTypeResult = Reflect.apply(oldType, thisContext, functionArguments);
if (hideDefaultSettings)
oldTypeResult.props.children = oldTypeResult.props.children.filter(c => !c?.props?.selectedFPS);
oldTypeResult.props.children.push(<ReplacedStreamSettings />);
return oldTypeResult;
}
export function replacedScreenshareModalComponent(oldComponent: (...args: any[]) => any, thisContext: any, functionArguments: any) {
const oldComponentResult = Reflect.apply(oldComponent, thisContext, functionArguments);
const content = oldComponentResult.props.children.props.children[2].props.children[1].props.children[2].props.children.props.children;
const oldContentType = content.type;
content.type = function () {
return replacedScreenshareModalSettingsContentType(oldContentType, this, arguments);
};
const [submitBtn, cancelBtn] = oldComponentResult.props.children.props.children[2].props.children[2].props.children;
submitBtn.props.onClick = () => {
const { screensharePatcher, screenshareAudioPatcher } = Plugin;
if (screensharePatcher) {
screensharePatcher.forceUpdateTransportationOptions();
if (screensharePatcher.connection?.connectionState === "CONNECTED")
screensharePatcher.forceUpdateDesktopSourceOptions();
}
if (screenshareAudioPatcher)
screenshareAudioPatcher.forceUpdateTransportationOptions();
};
return oldComponentResult;
}

View file

@ -0,0 +1,20 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./screenshareAudioStore";
export * from "./screenshareStore";

View file

@ -0,0 +1,41 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
defaultMicrophoneProfiles as defaultScreenshareAudioProfiles,
MicrophoneProfile as ScreenshareAudioProfile,
MicrophoneStore as ScreenshareAudioStore,
microphoneStoreDefault as screenshareAudioStoreDefault
} from "../../betterMicrophone.desktop/stores";
import { createPluginStore, ProfilableStore, profileable } from "../../philsPluginLibrary";
import { PluginInfo } from "../constants";
export let screenshareAudioStore: ProfilableStore<ScreenshareAudioStore, ScreenshareAudioProfile>;
export const initScreenshareAudioStore = () =>
screenshareAudioStore = createPluginStore(
PluginInfo.PLUGIN_NAME,
"ScreenshareAudioStore",
profileable(
screenshareAudioStoreDefault,
{ name: "" },
Object.values(defaultScreenshareAudioProfiles)
)
);
export { defaultScreenshareAudioProfiles, ScreenshareAudioProfile, ScreenshareAudioStore, screenshareAudioStoreDefault };

View file

@ -0,0 +1,122 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createPluginStore, ProfilableInitializer, ProfilableStore, profileable, ProfileableProfile } from "../../philsPluginLibrary";
import { PluginInfo } from "../constants";
export interface ScreenshareProfile {
width?: number,
height?: number,
framerate?: number,
videoCodec?: string,
keyframeInterval?: number,
videoBitrate?: number;
videoBitrateEnabled?: boolean;
resolutionEnabled?: boolean,
framerateEnabled?: boolean,
videoCodecEnabled?: boolean;
keyframeIntervalEnabled?: boolean;
hdrEnabled?: boolean;
}
export interface ScreenshareStore {
audioSource?: string;
audioSourceEnabled?: boolean;
simpleMode?: boolean;
setWidth: (width?: number) => void;
setHeight: (height?: number) => void;
setFramerate: (framerate?: number) => void;
setVideoCodec: (codec?: string) => void;
setKeyframeInterval: (keyframeInterval?: number) => void;
setVideoBitrate: (bitrate?: number) => void;
setKeyframeIntervalEnabled: (enabled?: boolean) => void;
setResolutionEnabled: (enabled?: boolean) => void;
setFramerateEnabled: (enabled?: boolean) => void;
setVideoCodecEnabled: (enabled?: boolean) => void;
setVideoBitrateEnabled: (enabled?: boolean) => void;
setHdrEnabled: (enabled?: boolean) => void;
setAudioSource: (audioSource?: string) => void;
setAudioSourceEnabled: (enabled?: boolean) => void;
setSimpleMode: (enabled?: boolean) => void;
}
export const defaultScreenshareProfiles = {
low: {
name: "Low Quality",
width: 1280,
height: 720,
framerate: 60,
videoBitrate: 2500,
resolutionEnabled: true,
framerateEnabled: true,
videoBitrateEnabled: true,
},
medium: {
name: "Medium Quality",
width: 1920,
height: 1080,
framerate: 60,
videoBitrate: 5000,
resolutionEnabled: true,
framerateEnabled: true,
videoBitrateEnabled: true,
},
high: {
name: "High Quality",
width: 1920,
height: 1080,
framerate: 60,
videoBitrate: 10000,
resolutionEnabled: true,
framerateEnabled: true,
videoBitrateEnabled: true,
}
} as const satisfies Record<string, ScreenshareProfile & ProfileableProfile>;
export const screenshareStoreDefault: ProfilableInitializer<ScreenshareStore, ScreenshareProfile> = (set, get) => ({
setVideoBitrate: bitrate => get().currentProfile.videoBitrate = bitrate,
setVideoBitrateEnabled: enabled => get().currentProfile.videoBitrateEnabled = enabled,
setVideoCodec: codec => get().currentProfile.videoCodec = codec,
setVideoCodecEnabled: enabled => get().currentProfile.videoCodecEnabled = enabled,
setFramerate: framerate => get().currentProfile.framerate = framerate,
setFramerateEnabled: enabled => get().currentProfile.framerateEnabled = enabled,
setHeight: height => get().currentProfile.height = height,
setWidth: width => get().currentProfile.width = width,
setResolutionEnabled: enabled => get().currentProfile.resolutionEnabled = enabled,
setKeyframeInterval: keyframeInterval => get().currentProfile.keyframeInterval = keyframeInterval,
setKeyframeIntervalEnabled: enabled => get().currentProfile.keyframeIntervalEnabled = enabled,
setHdrEnabled: enabled => get().currentProfile.hdrEnabled = enabled,
setAudioSource: audioSource => get().audioSource = audioSource,
setAudioSourceEnabled: enabled => get().audioSourceEnabled = enabled,
setSimpleMode: enabled => get().simpleMode = enabled,
simpleMode: true
});
export let screenshareStore: ProfilableStore<ScreenshareStore, ScreenshareProfile>;
export const initScreenshareStore = () =>
screenshareStore = createPluginStore(
PluginInfo.PLUGIN_NAME,
"ScreenshareStore",
profileable(
screenshareStoreDefault,
{ name: "" },
Object.values(defaultScreenshareProfiles)
)
);

View file

@ -0,0 +1,195 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings";
import { relaunch, showItemInFolder } from "@utils/native";
import { checkForUpdates, getRepo } from "@utils/updater";
import { Clipboard, GuildStore, NavigationRouter, SettingsRouter, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import Plugins from "~plugins";
import { openMultipleChoice } from "./components/MultipleChoice";
import { openSimpleTextInput } from "./components/TextInput";
export interface ButtonAction {
id: string;
label: string;
callback?: () => void;
registrar?: string;
}
export const actions: ButtonAction[] = [
{ id: "openSuncordSettings", label: "Open Suncord tab", callback: async () => await SettingsRouter.open("SuncordSettings"), registrar: "Suncord" },
{ id: "openPluginSettings", label: "Open Plugin tab", callback: () => SettingsRouter.open("SuncordPlugins"), registrar: "Suncord" },
{ id: "openThemesSettings", label: "Open Themes tab", callback: () => SettingsRouter.open("SuncordThemes"), registrar: "Suncord" },
{ id: "openUpdaterSettings", label: "Open Updater tab", callback: () => SettingsRouter.open("SuncordUpdater"), registrar: "Suncord" },
{ id: "openSuncordCloudSettings", label: "Open Cloud tab", callback: () => SettingsRouter.open("SuncordCloud"), registrar: "Suncord" },
{ id: "openBackupSettings", label: "Open Backup & Restore tab", callback: () => SettingsRouter.open("SuncordSettingsSync"), registrar: "Suncord" },
{ id: "restartClient", label: "Restart Client", callback: () => relaunch(), registrar: "Suncord" },
{ id: "openQuickCSSFile", label: "Open Quick CSS File", callback: () => VencordNative.quickCss.openEditor(), registrar: "Suncord" },
{ id: "openSettingsFolder", label: "Open Settings Folder", callback: async () => showItemInFolder(await VencordNative.settings.getSettingsDir()), registrar: "Suncord" },
{ id: "openInGithub", label: "Open in Github", callback: async () => VencordNative.native.openExternal(await getRepo()), registrar: "Suncord" },
{
id: "openInBrowser", label: "Open in Browser", callback: async () => {
const url = await openSimpleTextInput("Enter a URL");
const newUrl = url.replace(/(https?:\/\/)?([a-zA-Z0-9-]+)\.([a-zA-Z0-9-]+)/, "https://$2.$3");
try {
new URL(newUrl); // Throws if invalid
VencordNative.native.openExternal(newUrl);
} catch {
Toasts.show({
message: "Invalid URL",
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
}, registrar: "Suncord"
},
{
id: "togglePlugin", label: "Toggle Plugin", callback: async () => {
const plugins = Object.keys(Plugins);
const options: ButtonAction[] = [];
for (const plugin of plugins) {
options.push({
id: plugin,
label: plugin
});
}
const choice = await openMultipleChoice(options);
const enabled = await openMultipleChoice([
{ id: "enable", label: "Enable" },
{ id: "disable", label: "Disable" }
]);
if (choice && enabled) {
return togglePlugin(choice, enabled.id === "enable");
}
}, registrar: "Suncord"
},
{
id: "quickFetch", label: "Quick Fetch", callback: async () => {
try {
const url = await openSimpleTextInput("Enter URL to fetch (GET only)");
const newUrl = url.replace(/(https?:\/\/)?([a-zA-Z0-9-]+)\.([a-zA-Z0-9-]+)/, "https://$2.$3");
const res = (await fetch(newUrl));
const text = await res.text();
Clipboard.copy(text);
Toasts.show({
message: "Copied response to clipboard!",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
} catch (e) {
Toasts.show({
message: "Issue fetching URL",
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
}, registrar: "Suncord"
},
{
id: "copyGitInfo", label: "Copy Git Info", callback: async () => {
Clipboard.copy(`gitHash: ${gitHash}\ngitRemote: ${gitRemote}`);
Toasts.show({
message: "Copied git info to clipboard!",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}, registrar: "Suncord"
},
{
id: "checkForUpdates", label: "Check for Updates", callback: async () => {
const isOutdated = await checkForUpdates();
if (isOutdated) {
setTimeout(() => showNotification({
title: "A Suncord update is available!",
body: "Click here to view the update",
permanent: true,
noPersist: true,
onClick() {
SettingsRouter.open("SuncordUpdater");
}
}), 10_000);
} else {
Toasts.show({
message: "No updates available",
type: Toasts.Type.MESSAGE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
}, registrar: "Suncord"
},
{
id: "navToServer", label: "Navigate to Server", callback: async () => {
const allServers = Object.values(GuildStore.getGuilds());
const options: ButtonAction[] = [];
for (const server of allServers) {
options.push({
id: server.id,
label: server.name
});
}
const choice = await openMultipleChoice(options);
if (choice) {
NavigationRouter.transitionToGuild(choice.id);
}
}, registrar: "Suncord"
}
];
function togglePlugin(plugin: ButtonAction, enabled: boolean) {
Settings.plugins[plugin.id].enabled = enabled;
Toasts.show({
message: `Successfully ${enabled ? "enabled" : "disabled"} ${plugin.id}`,
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
export function registerAction(action: ButtonAction) {
actions.push(action);
}

View file

@ -0,0 +1,130 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { React, TextInput, useEffect, useState } from "@webpack/common";
import { settings } from "..";
import { actions } from "../commands";
const logger = new Logger("CommandPalette", "#e5c890");
export function CommandPalette({ modalProps }) {
const cl = classNameFactory("vc-command-palette-");
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const [startIndex, setStartIndex] = useState(0);
const allowMouse = settings.store.allowMouseControl;
const sortedActions = actions.slice().sort((a, b) => a.label.localeCompare(b.label));
const [queryEh, setQuery] = useState("");
const filteredActions = sortedActions.filter(
action => action.label.toLowerCase().includes(queryEh.toLowerCase())
);
const visibleActions = filteredActions.slice(startIndex, startIndex + 20);
const totalActions = filteredActions.length;
const handleWheel = (e: React.WheelEvent) => {
if (allowMouse && filteredActions.length > 20) {
if (e.deltaY > 0) {
setStartIndex(prev => Math.min(prev + 2, filteredActions.length - 20));
} else {
setStartIndex(prev => Math.max(prev - 2, 0));
}
}
};
const handleButtonClick = (actionId: string, index: number) => {
const selectedAction = filteredActions.find(action => action.id === actionId);
if (selectedAction) {
logger.log(`${selectedAction.id}'s action was triggered.`);
}
closeAllModals();
selectedAction?.callback?.();
setFocusedIndex(index);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
const currentIndex = focusedIndex !== null ? focusedIndex : -1;
let nextIndex;
switch (e.key) {
case "ArrowUp":
e.preventDefault();
nextIndex = currentIndex > 0 ? currentIndex - 1 : visibleActions.length - 1;
setFocusedIndex(nextIndex);
if (currentIndex === 0 && totalActions > 20) {
setStartIndex(prev => Math.max(prev - 1, 0));
setFocusedIndex(0);
}
break;
case "ArrowDown":
e.preventDefault();
nextIndex = currentIndex < visibleActions.length - 1 ? currentIndex + 1 : 0;
setFocusedIndex(nextIndex);
if (currentIndex === visibleActions.length - 1 && totalActions > 20) {
setStartIndex(prev => Math.min(prev + 1, filteredActions.length - 20));
setFocusedIndex(19);
}
break;
case "Enter":
if (currentIndex !== null && currentIndex >= 0 && currentIndex < visibleActions.length) {
handleButtonClick(visibleActions[currentIndex].id, currentIndex);
}
break;
default:
break;
}
};
useEffect(() => {
setFocusedIndex(0);
setStartIndex(0);
}, [queryEh]);
return (
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}>
<div>
<TextInput
value={queryEh}
onChange={e => setQuery(e)}
style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0", paddingLeft: "0.9rem" }}
placeholder="Search the Command Palette"
/>
<div className={cl("option-container")}>
{visibleActions.map((action, index) => (
<button
key={action.id}
className={cl("option", { "key-hover": index === focusedIndex })}
onClick={() => { if (allowMouse) handleButtonClick(action.id, index); }}
onMouseMove={() => { if (allowMouse) setFocusedIndex(index); }}
style={allowMouse ? { cursor: "pointer" } : { cursor: "default" }}
>
{action.label}
{action.registrar && <span className={cl("registrar")}>{action.registrar}</span>}
</button>
))}
</div>
</div>
</ModalRoot>
);
}
export const openCommandPalette = () => openModal(modalProps => <CommandPalette modalProps={modalProps} />);

View file

@ -0,0 +1,144 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { classNameFactory } from "@api/Styles";
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { React, TextInput, useEffect, useState } from "@webpack/common";
import { settings } from "..";
import { ButtonAction } from "../commands";
interface MultipleChoiceProps {
modalProps: ModalProps;
onSelect: (selectedValue: any) => void;
choices: ButtonAction[];
}
export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoiceProps) {
const cl = classNameFactory("vc-command-palette-");
const [queryEh, setQuery] = useState("");
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const [startIndex, setStartIndex] = useState(0);
const allowMouse = settings.store.allowMouseControl;
const sortedActions = choices.slice().sort((a, b) => a.label.localeCompare(b.label));
const filteredActions = sortedActions.filter(
action => action.label.toLowerCase().includes(queryEh.toLowerCase())
);
const visibleActions = filteredActions.slice(startIndex, startIndex + 20);
const totalActions = filteredActions.length;
const handleButtonClick = (actionId: string, index: number) => {
const selectedAction = filteredActions.find(action => action.id === actionId);
if (selectedAction) {
onSelect(selectedAction);
}
closeAllModals();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
const currentIndex = focusedIndex !== null ? focusedIndex : -1;
let nextIndex;
switch (e.key) {
case "ArrowUp":
e.preventDefault();
nextIndex = currentIndex > 0 ? currentIndex - 1 : visibleActions.length - 1;
setFocusedIndex(nextIndex);
if (currentIndex === 0 && totalActions > 20) {
setStartIndex(prev => Math.max(prev - 1, 0));
setFocusedIndex(0);
}
break;
case "ArrowDown":
e.preventDefault();
nextIndex = currentIndex < visibleActions.length - 1 ? currentIndex + 1 : 0;
setFocusedIndex(nextIndex);
if (currentIndex === visibleActions.length - 1 && totalActions > 20) {
setStartIndex(prev => Math.min(prev + 1, filteredActions.length - 20));
setFocusedIndex(19);
}
break;
case "Enter":
if (currentIndex !== null && currentIndex >= 0 && currentIndex < visibleActions.length) {
handleButtonClick(visibleActions[currentIndex].id, currentIndex);
}
break;
default:
break;
}
};
const handleWheel = (e: React.WheelEvent) => {
if (allowMouse && filteredActions.length > 20) {
if (e.deltaY > 0) {
setStartIndex(prev => Math.min(prev + 2, filteredActions.length - 20));
} else {
setStartIndex(prev => Math.max(prev - 2, 0));
}
}
};
useEffect(() => {
setFocusedIndex(0);
setStartIndex(0);
}, [queryEh]);
return (
// @ts-ignore
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}>
<div>
<TextInput
value={queryEh}
onChange={e => setQuery(e)}
style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0", paddingLeft: "0.9rem" }}
placeholder="Search the Command Palette"
/>
<div className={cl("option-container")}>
{visibleActions.map((action, index) => (
<button
key={action.id}
className={cl("option", { "key-hover": index === focusedIndex })}
onClick={() => { if (allowMouse) handleButtonClick(action.id, index); }}
onMouseMove={() => { if (allowMouse) setFocusedIndex(index); }}
style={allowMouse ? { cursor: "pointer" } : { cursor: "default" }}
>
{action.label}
{action.registrar && <span className={cl("registrar")}>{action.registrar}</span>}
</button>
))}
</div>
</div>
</ModalRoot>
);
}
export function openMultipleChoice(choices: ButtonAction[]): Promise<ButtonAction> {
return new Promise(resolve => {
openModal(modalProps => (
<MultipleChoice
modalProps={modalProps}
onSelect={selectedValue => {
closeAllModals();
resolve(selectedValue);
}}
choices={choices}
/>
));
});
}

View file

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { React, TextInput, useEffect, useState } from "@webpack/common";
interface SimpleTextInputProps {
modalProps: ModalProps;
onSelect: (inputValue: string) => void;
placeholder?: string;
info?: string;
}
export function SimpleTextInput({ modalProps, onSelect, placeholder, info }: SimpleTextInputProps) {
const [inputValue, setInputValue] = useState("");
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case "Enter":
onSelect(inputValue);
closeAllModals();
break;
default:
break;
}
};
useEffect(() => {
setInputValue("");
}, []);
return (
// @ts-ignore
<ModalRoot className="vc-command-palette-simple-text" {...modalProps} size={ModalSize.DYNAMIC} onKeyDown={handleKeyDown}>
<TextInput
value={inputValue}
onChange={e => setInputValue(e as unknown as string)}
style={{ width: "30vw", borderRadius: "5px" }}
placeholder={placeholder ?? "Type and press Enter"}
/>
{info && <div className="vc-command-palette-textinfo">{info}</div>}
</ModalRoot>
);
}
export function openSimpleTextInput(placeholder?: string, info?: string): Promise<string> {
return new Promise(resolve => {
openModal(modalProps => (
<SimpleTextInput
modalProps={modalProps}
onSelect={inputValue => resolve(inputValue)}
placeholder={placeholder}
info={info}
/>
));
});
}

View file

@ -0,0 +1,112 @@
/* cl = vc-command-palette- */
.vc-command-palette-root {
border-radius: 10px;
overflow: hidden;
background-color: var(--background-tertiary);
}
.vc-command-palette-option {
padding: 5px;
background-color: var(--background-tertiary);
color: var(--white-500);
font-family: var(--font-display);
text-align: left;
padding-left: 0.8rem;
}
.vc-command-palette-key-hover {
padding: 5px;
background-color: var(--background-modifier-selected);
border-radius: 3px;
font-family: var(--font-display);
color: var(--interactive-hover);
padding-left: 0.8rem;
}
.vc-command-palette-option-container {
display: grid;
gap: 2px;
margin-left: 0.8rem;
margin-right: 0.8rem;
}
.vc-command-palette-textinfo {
font-family: var(--font-display);
color: var(--white-500);
margin-left: 0.8rem;
margin-right: 0.8rem;
padding: 0.8rem 0;
}
.vc-command-palette-simple-text {
background-color: var(--input-background);
width: 30vh;
}
.vc-command-palette-registrar {
position: absolute;
color: var(--interactive-normal);
white-space: nowrap;
text-overflow: ellipsis;
right: 1.6rem;
}
.vc-command-palette-key-recorder-container {
position: relative;
display: block;
cursor: pointer;
height: 40px;
width: 20rem;
}
.vc-command-palette-key-recorder {
display: flex;
align-items: center;
border-radius: 3px;
background-color: hsl(var(--black-500-hsl) / 10%);
color: var(--header-primary);
line-height: 22px;
font-weight: 600;
padding: 10px 0 10px 10px;
text-overflow: ellipsis;
overflow: hidden;
border: 1px solid;
border-color: hsl(var(--black-500-hsl) / 30%);
transition: border.15s ease;
user-select: none;
font-family: var(--font-primary);
}
.vc-command-palette-recording {
animation: shadowPulse_b16790 1s ease-in infinite;
box-shadow: 0 0 6px hsl(var(--red-400-hsl) / 30%);
border-color: hsl(var(--red-400-hsl) / 30%);
color: var(--status-danger);
}
.vc-command-palette-key-recorder:hover {
border-color: hsl(var(--red-400-hsl) / 30%);
}
.vc-command-palette-recording-button {
color: var(--status-danger) !important;
background-color: hsl(var(--red-400-hsl) / 10%) !important;
opacity: 1;
transition: opacity.2s ease-in-out, transform.2s ease-in-out;
}
.vc-command-palette-key-recorder-button {
position: absolute;
right: 1rem;
height: 30px;
width: 128px;
color: var(--white-500);
background-color: var(--button-secondary-background);
border-radius: 10px;
transition: background-color 0.15s ease;
}
.vc-command-palette-key-recorder-button:hover {
background-color: var(--button-secondary-background-hover);
}

View file

@ -0,0 +1,144 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants";
import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { SettingsRouter, useState } from "@webpack/common";
import { registerAction } from "./commands";
import { openCommandPalette } from "./components/CommandPalette";
const cl = classNameFactory("vc-command-palette-");
let isRecordingGlobal: boolean = false;
export const settings = definePluginSettings({
hotkey: {
description: "The hotkey to open the command palette.",
type: OptionType.COMPONENT,
default: ["Control", "Shift", "P"],
component: () => {
const [isRecording, setIsRecording] = useState(false);
const recordKeybind = (setIsRecording: (value: boolean) => void) => {
const keys: Set<string> = new Set();
const keyLists: string[][] = [];
setIsRecording(true);
isRecordingGlobal = true;
const updateKeys = () => {
if (keys.size === 0 || !document.querySelector(`.${cl("key-recorder-button")}`)) {
const longestArray = keyLists.reduce((a, b) => a.length > b.length ? a : b);
if (longestArray.length > 0) {
settings.store.hotkey = longestArray.map(key => key.toLowerCase());
}
setIsRecording(false);
isRecordingGlobal = false;
document.removeEventListener("keydown", keydownListener);
document.removeEventListener("keyup", keyupListener);
}
keyLists.push(Array.from(keys));
};
const keydownListener = (e: KeyboardEvent) => {
const { key } = e;
if (!keys.has(key)) {
keys.add(key);
}
updateKeys();
};
const keyupListener = (e: KeyboardEvent) => {
keys.delete(e.key);
updateKeys();
};
document.addEventListener("keydown", keydownListener);
document.addEventListener("keyup", keyupListener);
};
return (
<>
<div className={cl("key-recorder-container")} onClick={() => recordKeybind(setIsRecording)}>
<div className={`${cl("key-recorder")} ${isRecording ? cl("recording") : ""}`}>
{settings.store.hotkey.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" + ")}
<button className={`${cl("key-recorder-button")} ${isRecording ? cl("recording-button") : ""}`} disabled={isRecording}>
{isRecording ? "Recording..." : "Record keybind"}
</button>
</div>
</div>
</>
);
}
},
allowMouseControl: {
description: "Allow the mouse to control the command palette.",
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "CommandPalette",
description: "Allows you to navigate the UI with a keyboard.",
authors: [Devs.Ethan],
settings,
start() {
document.addEventListener("keydown", this.event);
if (IS_DEV) {
registerAction({
id: "openDevSettings",
label: "Open Dev tab",
callback: () => SettingsRouter.open("SuncordPatchHelper"),
registrar: "Suncord"
});
}
},
stop() {
document.removeEventListener("keydown", this.event);
},
event(e: KeyboardEvent) {
enum Modifiers {
control = "ctrlKey",
shift = "shiftKey",
alt = "altKey",
meta = "metaKey"
}
const { hotkey } = settings.store;
const pressedKey = e.key.toLowerCase();
if (isRecordingGlobal) return;
for (let i = 0; i < hotkey.length; i++) {
const lowercasedRequiredKey = hotkey[i].toLowerCase();
if (lowercasedRequiredKey in Modifiers && !e[Modifiers[lowercasedRequiredKey]]) {
return;
}
if (!(lowercasedRequiredKey in Modifiers) && pressedKey !== lowercasedRequiredKey) {
return;
}
}
closeAllModals();
if (document.querySelector(`.${cl("root")}`)) return;
openCommandPalette();
}
});

View file

@ -0,0 +1,114 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { Button, Text, useEffect, useState } from "@webpack/common";
export function DependencyModal({ props, options: { key, checkytdlp, checkffmpeg } }: {
props: ModalProps;
options: {
key: string;
checkytdlp: () => Promise<boolean>;
checkffmpeg: () => Promise<boolean>;
};
}) {
const checking = <span>Checking...</span>;
const installed = <span style={{ color: "green" }}>Installed!</span>;
const notInstalled = (color: string) => <span style={{ color }}>Not installed.</span>;
const [ytdlpStatus, setYtdlpStatus] = useState(checking);
const [ffmpegStatus, setFfmpegStatus] = useState(checking);
useEffect(() => {
checkytdlp().then(v => v ? setYtdlpStatus(installed) : setYtdlpStatus(notInstalled("red")));
checkffmpeg().then(v => v ? setFfmpegStatus(installed) : setFfmpegStatus(notInstalled("yellow")));
}, []);
return (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>MediaDownloader: Missing dependencies</Text>
<ModalCloseButton onClick={() => closeModal(key)} />
</ModalHeader>
<ModalContent>
<div style={{ padding: "16px 0" }}>
<Text variant="text-md/normal" >
The MediaDownloader plugin requires a working version of yt-dlp to be installed on your system. For extra features such as higher quality videos and gifs, you can also optionally install ffmpeg.
<br /><br />
If you don't know how to install yt-dlp or ffmpeg, check the installation guides below (if you <i>do</i> know how to install it, make sure it's in your PATH).
<br /><br />
<strong>Note:</strong> You may need to completely restart Discord after installing yt-dlp or ffmpeg for the plugin to detect them.
<br /><br />
<ul style={{ listStyleType: "disc", marginLeft: "1rem" }}>
<li>
<a href="https://github.com/yt-dlp/yt-dlp/wiki/Installation#windows" target="_blank" rel="noreferrer">yt-dlp installation guide</a>
</li>
<li>
<a href="https://phoenixnap.com/kb/ffmpeg-windows" target="_blank" rel="noreferrer">Unofficial ffmpeg installation guide</a>
</li>
</ul>
</Text>
<div style={{
marginTop: "16px",
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gridTemplateRows: "repeat(2, 1fr)",
columnGap: "16px",
rowGap: "8px"
}}>
<div
style={{
gridArea: "1 / 1 / 2 / 2",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<Text variant="text-md/bold">
yt-dlp status: {ytdlpStatus}
</Text>
</div>
<Button
onClick={async () => {
setYtdlpStatus(checking);
setYtdlpStatus(await checkytdlp()
? installed
: notInstalled("red"));
}}
style={{ gridArea: "1 / 2 / 2 / 3" }}
>
Check again
</Button>
<div
style={{
gridArea: "2 / 1 / 3 / 2",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<Text variant="text-md/bold">
ffmpeg status: {ffmpegStatus}
</Text>
</div>
<Button
onClick={async () => {
setFfmpegStatus(checking);
setFfmpegStatus(await checkffmpeg()
? installed
: notInstalled("yellow"));
}}
style={{ gridArea: "2 / 2 / 3 / 3" }}
>
Check again
</Button>
</div>
</div>
</ModalContent>
</ModalRoot>
);
}

View file

@ -0,0 +1,333 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { Button, DraftType, FluxDispatcher, Forms, UploadHandler, UploadManager, UserStore } from "@webpack/common";
import { Channel } from "discord-types/general";
import { DependencyModal } from "./DependencyModal";
type ButtonComponent = {
customId?: string;
disabled?: boolean;
emoji?: {
animated?: boolean | string;
id?: string;
name?: string;
src?: string;
};
id: string;
label?: string;
style: number;
type: number;
url?: string;
};
const Native = VencordNative.pluginHelpers.MediaDownloader as PluginNative<typeof import("./native")>;
const logger = new Logger("MediaDownloader", "#ff0b01");
const maxFileSize = () => {
const premiumType = (UserStore.getCurrentUser().premiumType ?? 0);
if (premiumType > 1) return 500000000; // 500MB
if (premiumType > 0) return 50000000; // 50MB
return 25000000; // 25MB
};
/** Takes a string and splits it into an array of arguments. */
const argParse = (args: string): string[] => args.match(
/(?:[^\s"']+|"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([^\s]+))/g
) ?? [];
function mimetype(extension: "mp4" | "webm" | "gif" | "mp3" | string) {
switch (extension) {
case "mp4":
return "video/mp4";
case "webm":
return "video/webm";
case "gif":
return "image/gif";
case "mp3":
return "audio/mp3";
default:
return "application/octet-stream";
}
}
const CancelButton = [{
components: [{
customId: "media-downloader-stop-download", // ! for some reason customId is always undefined, so I'm just saving the id in the emoji animated field :3
emoji: {
name: "⚪",
animated: "media-downloader-stop-download"
},
label: "Cancel download",
id: "0,0",
style: 4,
type: 2,
}], id: "0", type: 1
}];
async function sendProgress(channelId: string, promise: Promise<{
buffer: Buffer;
title: string;
logs: string;
} | {
error: string;
logs: string;
}>) {
if (!settings.store.showProgress) {
sendBotMessage(channelId, {
components: CancelButton
});
return await promise;
}
const clydeMessage = sendBotMessage(channelId, {
content: "Downloading video...",
components: CancelButton
});
const updateMessage = (stdout: string, append?: string) => {
const text = stdout.toString();
FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message: {
...clydeMessage,
content: `Downloading video...\n\`\`\`\n${text}\n\`\`\`${append || ""}`,
components: append ? [] : clydeMessage.components
}
});
};
// Hacky way to send info from native to renderer for progress updates
const id = setInterval(async () => {
const stdout = await Native.getStdout();
updateMessage(stdout);
}, 500);
const data = await promise;
clearInterval(id);
const stdout = await Native.getStdout();
updateMessage(stdout, "error" in data ? "Error!" : "Done!");
return data;
}
function sendFfmpegWarning(channelId: string) {
sendBotMessage(channelId, {
content: "FFmpeg not detected. You may experience lower download quality and missing features."
});
}
// Mostly taken from viewRaw and betterSessions plugins.
async function openDependencyModal() {
const key = openModal(props => (
<ErrorBoundary>
<DependencyModal props={props} options={{
key,
checkffmpeg: Native.checkffmpeg,
checkytdlp: Native.checkytdlp,
}} />
</ErrorBoundary>
));
}
const settings = definePluginSettings({
supportedWebsites: {
description: "See the link for a list of supported websites.",
type: OptionType.COMPONENT,
default: "none",
component: () => (
<>
<Forms.FormText>
<Link href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md" className="media-downloader-link">
<Button role="link" style={{ width: "100%" }}>
Click to see supported websites.
</Button>
</Link>
</Forms.FormText>
<Forms.FormDivider />
</>
)
},
showProgress: {
type: OptionType.BOOLEAN,
description: "Send a Clyde message with the download progress.",
default: true,
},
showFfmpegWarning: {
type: OptionType.BOOLEAN,
description: "Show a warning message if ffmpeg is not installed.",
default: true,
},
defaultGifQuality: {
type: OptionType.NUMBER,
description: "The quality level to use if no value is specified when downloading gifs. A number between 1 and 5.",
default: 3,
},
ytdlpArgs: {
type: OptionType.STRING,
description: "Additional arguments to pass to yt-dlp. This may overwrite default plugin arguments such format selection. Note: if modifying the ouptup, ensure the filename starts with `download`.",
placeholder: "--format bestvideo+bestaudio",
},
ffmpegArgs: {
type: OptionType.STRING,
description: "Additional arguments to pass to ffmpeg. This may overwrite default plugin arguments such as auto-scaling. Note: if modifying the output, ensure the filename starts with `remux`.",
placeholder: "-vf scale=1280:720",
}
}, {
defaultGifQuality: {
isValid(value) {
return value >= 1 && value <= 5;
}
}
});
export default definePlugin({
name: "MediaDownloader",
description: "Download and send videos with from YouTube, Twitter, Reddit and more.",
authors: [EquicordDevs.Colorman],
dependencies: ["CommandsAPI"],
reporterTestable: ReporterTestable.Patches,
settings,
commands: [{
inputType: ApplicationCommandInputType.BUILT_IN,
name: "download",
description: "Download and send videos, audio or gifs.",
options: [{
name: "url",
description: "The URL of any video supported by yt-dlp.",
required: true,
type: ApplicationCommandOptionType.STRING
}, {
name: "format",
description: "Whether to download a video or audio.",
type: ApplicationCommandOptionType.STRING,
choices: [
{ name: "Video", value: "video", label: "Video" },
{ name: "Audio", value: "audio", label: "Audio" },
{ name: "GIF", value: "gif", label: "GIF" }
],
required: false,
}, {
name: "gif_quality",
type: ApplicationCommandOptionType.INTEGER,
description: "The quality level when using GIF. Try lowering this number if the GIF is too large.",
required: false,
choices: [
{ name: "5", value: "5", label: "5" },
{ name: "4", value: "4", label: "4" },
{ name: "3", value: "3", label: "3" },
{ name: "2", value: "2", label: "2" },
{ name: "1", value: "1", label: "1" }
]
}, {
name: "yt-dlp_args",
description: "Additional arguments to pass to yt-dlp. These will take precedence over arguments set in the settings. This may overwrite default plugin arguments such format selection. Note: if modifying the output, ensure the filename starts with `download`.",
required: false,
type: ApplicationCommandOptionType.STRING
}, {
name: "ffmpeg_args",
description: "Additional arguments to pass to ffmpeg. These will take precedence over arguments set in the settings. This may overwrite default plugin arguments such as auto-scaling. Note: if modifying the output, ensure the filename starts with `remux`.",
required: false,
type: ApplicationCommandOptionType.STRING
}],
execute: async (args, ctx) => {
if (!await Native.isYtdlpAvailable()) return openDependencyModal();
if (!await Native.isFfmpegAvailable() && settings.store.showFfmpegWarning) sendFfmpegWarning(ctx.channel.id);
const url = findOption<string>(args, "url", "");
const format = findOption<"video" | "audio" | "gif">(args, "format", "video");
const gifQuality = parseInt(findOption<string>(args, "gif_quality", settings.store.defaultGifQuality.toString())) as 1 | 2 | 3 | 4 | 5;
const ytdlpArgs = findOption<string>(args, "yt-dlp_args", "");
const ffmpegArgs = findOption<string>(args, "ffmpeg_args", "");
return await download(ctx.channel, {
url,
format,
gifQuality,
ytdlpArgs,
ffmpegArgs
});
}
}],
patches: [
{
find: "missing validator for this component",
replacement: {
match: /(\i)(\.type\)\{case \i\.\i\.BUTTON):return null;/,
replace: "$1$2:return ($self.handleButtonClick($1),null);"
}
}
],
handleButtonClick: (buttonComponent: ButtonComponent) => {
if (!(buttonComponent.emoji?.animated === "media-downloader-stop-download")) return;
Native.interrupt();
},
start: async () => {
await Native.checkytdlp();
await Native.checkffmpeg();
const videoDir = await DataStore.get<string>("media-downloader-video-dir");
const newVideoDir = await Native.start(videoDir);
await DataStore.set("media-downloader-video-dir", newVideoDir);
},
stop: async () => {
// Clean up the temp files
await Native.stop();
await DataStore.del("media-downloader-video-dir");
}
});
async function download(channel: Channel, {
url, format, ytdlpArgs, ffmpegArgs, gifQuality
}: {
url: string;
format: "video" | "audio" | "gif";
ytdlpArgs: string;
ffmpegArgs: string;
gifQuality: 1 | 2 | 3 | 4 | 5;
}) {
const promise = Native.execute({
url,
format,
gifQuality,
ytdlpArgs: [
...argParse(settings.store.ytdlpArgs || ""),
...argParse(ytdlpArgs)
],
ffmpegArgs: [
...argParse(settings.store.ffmpegArgs || ""),
...argParse(ffmpegArgs)
],
maxFileSize: maxFileSize()
});
const data = await sendProgress(channel.id, promise);
for (const log of data.logs.trim().split("\n")) logger.info(log);
if ("error" in data) {
// Open the modal if the error is due to missing formats (could be fixed by downloading ffmpeg)
if (data.error.includes("--list-formats") && !(await Native.isFfmpegAvailable()))
return sendBotMessage(channel.id, { content: "No good streams found. Consider installing ffmpeg to increase the likelihood of a successful stream." }), openDependencyModal();
return sendBotMessage(channel.id, {
content: `Failed to download video: ${data.error.includes("\n") ? "\n```" + data.error + "\n```" : `\`${data.error}\``}`
});
}
const { buffer, title } = data;
UploadManager.clearAll(channel.id, DraftType.SlashCommand);
const file = new File([buffer], title, { type: mimetype(title.split(".")[1]) });
// See petpet
setTimeout(() => UploadHandler.promptToUpload([file], channel, DraftType.ChannelMessage), 10);
}

View file

@ -0,0 +1,300 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ChildProcessWithoutNullStreams, execFileSync, spawn } from "child_process";
import { IpcMainInvokeEvent } from "electron";
import * as fs from "fs";
import os from "os";
import path from "path";
type Format = "video" | "audio" | "gif";
type DownloadOptions = {
url: string;
format?: Format;
gifQuality?: 1 | 2 | 3 | 4 | 5;
ytdlpArgs?: string[];
ffmpegArgs?: string[];
maxFileSize?: number;
};
let workdir: string | null = null;
let stdout_global: string = "";
let logs_global: string = "";
let ytdlpAvailable = false;
let ffmpegAvailable = false;
let ytdlpProcess: ChildProcessWithoutNullStreams | null = null;
let ffmpegProcess: ChildProcessWithoutNullStreams | null = null;
const getdir = () => workdir ?? process.cwd();
const p = (file: string) => path.join(getdir(), file);
const cleanVideoFiles = () => {
if (!workdir) return;
fs.readdirSync(workdir)
.filter(f => f.startsWith("download.") || f.startsWith("remux."))
.forEach(f => fs.unlinkSync(p(f)));
};
const appendOut = (data: string) => ( // Makes carriage return (\r) work
(stdout_global += data), (stdout_global = stdout_global.replace(/^.*\r([^\n])/gm, "$1")));
const log = (...data: string[]) => (console.log(`[Plugin:MediaDownloader] ${data.join(" ")}`), logs_global += `[Plugin:MediaDownloader] ${data.join(" ")}\n`);
const error = (...data: string[]) => console.error(`[Plugin:MediaDownloader] [ERROR] ${data.join(" ")}`);
function ytdlp(args: string[]): Promise<string> {
log(`Executing yt-dlp with args: ["${args.map(a => a.replace('"', '\\"')).join('", "')}"]`);
let errorMsg = "";
return new Promise<string>((resolve, reject) => {
ytdlpProcess = spawn("yt-dlp", args, {
cwd: getdir(),
});
ytdlpProcess.stdout.on("data", data => appendOut(data));
ytdlpProcess.stderr.on("data", data => {
appendOut(data);
error(`yt-dlp encountered an error: ${data}`);
errorMsg += data;
});
ytdlpProcess.on("exit", code => {
ytdlpProcess = null;
code === 0 ? resolve(stdout_global) : reject(new Error(errorMsg || `yt-dlp exited with code ${code}`));
});
});
}
function ffmpeg(args: string[]): Promise<string> {
log(`Executing ffmpeg with args: ["${args.map(a => a.replace('"', '\\"')).join('", "')}"]`);
let errorMsg = "";
return new Promise<string>((resolve, reject) => {
ffmpegProcess = spawn("ffmpeg", args, {
cwd: getdir(),
});
ffmpegProcess.stdout.on("data", data => appendOut(data));
ffmpegProcess.stderr.on("data", data => {
appendOut(data);
error(`ffmpeg encountered an error: ${data}`);
errorMsg += data;
});
ffmpegProcess.on("exit", code => {
ffmpegProcess = null;
code === 0 ? resolve(stdout_global) : reject(new Error(errorMsg || `ffmpeg exited with code ${code}`));
});
});
}
export async function start(_: IpcMainInvokeEvent, _workdir: string | undefined) {
_workdir ||= fs.mkdtempSync(path.join(os.tmpdir(), "vencord_mediaDownloader_"));
if (!fs.existsSync(_workdir)) fs.mkdirSync(_workdir, { recursive: true });
workdir = _workdir;
log("Using workdir: ", workdir);
return workdir;
}
export async function stop(_: IpcMainInvokeEvent) {
if (workdir) {
log("Cleaning up workdir");
fs.rmSync(workdir, { recursive: true });
workdir = null;
}
}
async function metadata(options: DownloadOptions) {
stdout_global = "";
const metadata = JSON.parse(await ytdlp(["-J", options.url, "--no-warnings"]));
if (metadata.is_live) throw "Live streams are not supported.";
stdout_global = "";
return { videoTitle: `${metadata.title || "video"} (${metadata.id})` };
}
function genFormat({ videoTitle }: { videoTitle: string; }, { maxFileSize, format }: DownloadOptions) {
const HAS_LIMIT = !!maxFileSize;
const MAX_VIDEO_SIZE = HAS_LIMIT ? maxFileSize * 0.8 : 0;
const MAX_AUDIO_SIZE = HAS_LIMIT ? maxFileSize * 0.2 : 0;
const audio = {
noFfmpeg: "ba[ext=mp3]{TOT_SIZE}/wa[ext=mp3]{TOT_SIZE}",
ffmpeg: "ba*{TOT_SIZE}/ba{TOT_SIZE}/wa*{TOT_SIZE}/ba*"
};
const video = {
noFfmpeg: "b{TOT_SIZE}{HEIGHT}[ext=webm]/b{TOT_SIZE}{HEIGHT}[ext=mp4]/w{HEIGHT}{TOT_SIZE}",
ffmpeg: "b*{VID_SIZE}{HEIGHT}+ba{AUD_SIZE}/b{TOT_SIZE}{HEIGHT}/b*{HEIGHT}+ba",
};
const gif = {
ffmpeg: "bv{TOT_SIZE}/wv{TOT_SIZE}"
};
let format_group: { noFfmpeg?: string; ffmpeg: string; };
switch (format) {
case "audio":
format_group = audio;
break;
case "gif":
format_group = gif;
break;
case "video":
default:
format_group = video;
break;
}
const format_string = (ffmpegAvailable ? format_group.ffmpeg : format_group.noFfmpeg)
?.replaceAll("{TOT_SIZE}", HAS_LIMIT ? `[filesize<${maxFileSize}]` : "")
.replaceAll("{VID_SIZE}", HAS_LIMIT ? `[filesize<${MAX_VIDEO_SIZE}]` : "")
.replaceAll("{AUD_SIZE}", HAS_LIMIT ? `[filesize<${MAX_AUDIO_SIZE}]` : "")
.replaceAll("{HEIGHT}", "[height<=1080]");
if (!format_string) throw "Gif format is only supported with ffmpeg.";
log("Video formated calculated as ", format_string);
log(`Based on: format=${format}, maxFileSize=${maxFileSize}, ffmpegAvailable=${ffmpegAvailable}`);
return { format: format_string, videoTitle };
}
async function download({ format, videoTitle }: { format: string; videoTitle: string; }, { ytdlpArgs, url, format: usrFormat }: DownloadOptions) {
cleanVideoFiles();
const baseArgs = ["-f", format, "-o", "download.%(ext)s", "--force-overwrites", "-I", "1"];
const remuxArgs = ffmpegAvailable
? usrFormat === "video"
? ["--remux-video", "webm>webm/mp4"]
: usrFormat === "audio"
? ["--extract-audio", "--audio-format", "mp3"]
: []
: [];
const customArgs = ytdlpArgs?.filter(Boolean) || [];
await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]);
const file = fs.readdirSync(getdir()).find(f => f.startsWith("download."));
if (!file) throw "No video file was found!";
return { file, videoTitle };
}
async function remux({ file, videoTitle }: { file: string; videoTitle: string; }, { ffmpegArgs, format, maxFileSize, gifQuality }: DownloadOptions) {
const sourceExtension = file.split(".").pop();
if (!ffmpegAvailable) return log("Skipping remux, ffmpeg is unavailable."), { file, videoTitle, extension: sourceExtension };
// We only really need to remux if
// 1. The file is too big
// 2. The file is in a format not supported by discord
// 3. The user provided custom ffmpeg arguments
// 4. The target format is gif
const acceptableFormats = ["mp3", "mp4", "webm"];
const fileSize = fs.statSync(p(file)).size;
const customArgs = ffmpegArgs?.filter(Boolean) || [];
const isFormatAcceptable = acceptableFormats.includes(sourceExtension ?? "");
const isFileSizeAcceptable = (!maxFileSize || fileSize <= maxFileSize);
const hasCustomArgs = customArgs.length > 0;
const isGif = format === "gif";
if (isFormatAcceptable && isFileSizeAcceptable && !hasCustomArgs && !isGif)
return log("Skipping remux, file type and size are good, and no ffmpeg arguments were specified."), { file, videoTitle, extension: sourceExtension };
const duration = parseFloat(execFileSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", p(file)]).toString());
if (isNaN(duration)) throw "Failed to get video duration.";
// ffmpeg tends to go above the target size, so I'm setting it to 7/8
const targetBits = maxFileSize ? (maxFileSize * 7) / duration : 9999999;
const kilobits = ~~(targetBits / 1024);
let baseArgs: string[];
let ext: string;
switch (format) {
case "audio":
baseArgs = ["-i", p(file), "-b:a", `${kilobits}k`, "-maxrate", `${kilobits}k`, "-bufsize", "1M", "-y"];
ext = "mp3";
break;
case "video":
default:
// Dynamically resize based on target bitrate
const height = kilobits <= 100 ? 480 : kilobits <= 500 ? 720 : 1080;
baseArgs = ["-i", p(file), "-b:v", `${~~(kilobits * 0.8)}k`, "-b:a", `${~~(kilobits * 0.2)}k`, "-maxrate", `${kilobits}k`, "-bufsize", "1M", "-y", "-filter:v", `scale=-1:${height}`];
ext = "mp4";
break;
case "gif":
let fps: number, width: number, colors: number, bayer_scale: number;
// WARNING: these parameters have been arbitrarily chosen, optimization is welcome!
switch (gifQuality) {
case 1:
fps = 5, width = 360, colors = 24, bayer_scale = 5;
break;
case 2:
fps = 10, width = 420, colors = 32, bayer_scale = 5;
break;
default:
case 3:
fps = 15, width = 480, colors = 64, bayer_scale = 4;
break;
case 4:
fps = 20, width = 540, colors = 64, bayer_scale = 3;
break;
case 5:
fps = 30, width = 720, colors = 128, bayer_scale = 1;
break;
}
baseArgs = ["-i", p(file), "-vf", `fps=${fps},scale=w=${width}:h=-1:flags=lanczos,mpdecimate,split[s0][s1];[s0]palettegen=max_colors=${colors}[p];[s1][p]paletteuse=dither=bayer:bayer_scale=${bayer_scale}`, "-loop", "0", "-bufsize", "1M", "-y"];
ext = "gif";
break;
}
await ffmpeg([...baseArgs, ...customArgs, `remux.${ext}`]);
return { file: `remux.${ext}`, videoTitle, extension: ext };
}
function upload({ file, videoTitle, extension }: { file: string; videoTitle: string; extension: string | undefined; }) {
if (!extension) throw "Invalid extension.";
const buffer = fs.readFileSync(p(file));
return { buffer, title: `${videoTitle}.${extension}` };
}
export async function execute(
_: IpcMainInvokeEvent,
opt: DownloadOptions
): Promise<{
buffer: Buffer;
title: string;
logs: string;
} | {
error: string;
logs: string;
}> {
logs_global = "";
try {
const videoMetadata = await metadata(opt);
const videoFormat = genFormat(videoMetadata, opt);
const videoDownload = await download(videoFormat, opt);
const videoRemux = await remux(videoDownload, opt);
const videoUpload = upload(videoRemux);
return { logs: logs_global, ...videoUpload };
} catch (e: any) {
return { error: e.toString(), logs: logs_global };
}
}
export function checkffmpeg(_?: IpcMainInvokeEvent) {
try {
execFileSync("ffmpeg", ["-version"]);
execFileSync("ffprobe", ["-version"]);
ffmpegAvailable = true;
return true;
} catch (e) {
ffmpegAvailable = false;
return false;
}
}
export async function checkytdlp(_?: IpcMainInvokeEvent) {
try {
execFileSync("yt-dlp", ["--version"]);
ytdlpAvailable = true;
return true;
} catch (e) {
ytdlpAvailable = false;
return false;
}
}
export async function interrupt(_: IpcMainInvokeEvent) {
log("Interrupting...");
ytdlpProcess?.kill();
ffmpegProcess?.kill();
cleanVideoFiles();
}
export const getStdout = () => stdout_global;
export const isYtdlpAvailable = () => ytdlpAvailable;
export const isFfmpegAvailable = () => ffmpegAvailable;

View file

@ -0,0 +1,9 @@
/* Only way to make on:hover styles */
.media-downloader-link {
text-decoration: none !important;
}
.media-downloader-link:hover {
text-decoration: none !important;
}

View file

@ -11,47 +11,45 @@ import { User } from "discord-types/general";
let user: ModifiedUser | undefined;
let lastUserId: string | undefined;
let uninject: (() => void) | undefined;
interface ModifiedUser extends User {
_realPremiumType?: number;
}
const onChange = () => {
const newUser = UserStore.getCurrentUser();
if (newUser && newUser.id !== lastUserId) {
user = newUser;
ready(user);
}
};
function ready(user: ModifiedUser) {
if (!user) return;
if ("_realPremiumType" in user) return;
user._realPremiumType = user.premiumType ?? 0;
user.premiumType = 2;
lastUserId = user.id;
}
export default definePlugin({
name: "NoNitroUpsell",
description: "Removes ALL of Discord's nitro upsells by tricking the client into thinking you have nitro.",
authors: [EquicordDevs.thororen],
ready(user: ModifiedUser): void {
if (!user) return;
if ("_realPremiumType" in user) return;
user._realPremiumType = user.premiumType ?? 0;
user.premiumType = 2;
lastUserId = user.id;
},
start(): void {
start() {
user = UserStore.getCurrentUser();
if (user) this.ready(user);
const onChange = (): void => {
const newUser = UserStore.getCurrentUser();
if (newUser && newUser.id !== lastUserId) {
user = newUser;
this.ready(user);
}
};
if (user) ready(user);
UserStore.addChangeListener(onChange);
uninject = () => UserStore.removeChangeListener(onChange);
},
stop(): void {
uninject?.();
stop() {
const user = UserStore.getCurrentUser();
if (!user) return;
if (!("_realPremiumType" in user)) return;
UserStore?.getCurrentUser()?.premiumType;
// @ts-ignore
user.premiumType = user._realPremiumType;
delete user._realPremiumType;
UserStore.removeChangeListener(onChange);
}
});

View file

@ -0,0 +1,57 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginAuthor } from "@utils/types";
import { useEffect, UserUtils, useState } from "@webpack/common";
import { User } from "discord-types/general";
import React from "react";
import { createDummyUser, types, UserSummaryItem } from "../../philsPluginLibrary";
export interface AuthorUserSummaryItemProps extends Partial<React.ComponentProps<types.UserSummaryItem>> {
authors: PluginAuthor[];
}
export const AuthorUserSummaryItem = (props: AuthorUserSummaryItemProps) => {
const [users, setUsers] = useState<Partial<User>[]>([]);
useEffect(() => {
(async () => {
props.authors.forEach(author =>
UserUtils.getUser(`${author.id}`)
.then(user => setUsers(users => [...users, user]))
.catch(() => setUsers(users => [...users, createDummyUser({
username: author.name,
id: `${author.id}`,
bot: true,
})]))
);
})();
}, []);
return (
<UserSummaryItem
users={users as User[]}
guildId={undefined}
renderIcon={false}
showDefaultAvatarsForNullUsers
showUserPopout
{...props}
/>
);
};

View file

@ -0,0 +1,53 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { Text } from "@webpack/common";
import React from "react";
import { Author, Contributor } from "../types";
import { openURL } from "../utils";
import { AuthorUserSummaryItem } from "./AuthorSummaryItem";
export interface ContributorAuthorSummaryProps {
author?: Author;
contributors?: Contributor[];
}
export const ContributorAuthorSummary = ({ author, contributors }: ContributorAuthorSummaryProps) => {
return (
<Flex style={{ gap: "0.7em" }}>
{author &&
<Flex style={{ justifyContent: "center", alignItems: "center", gap: "0.5em" }}>
<Text variant="text-sm/normal" style={{ color: "var(--text-muted)" }}>
Author: <a onClick={() => author.github && openURL(author.github)}>{`${author.name}`}</a>
</Text>
<AuthorUserSummaryItem authors={[author]} />
</Flex>
}
{(contributors && contributors.length > 0) &&
<Flex style={{ justifyContent: "center", alignItems: "center", gap: "0.5em" }}>
<Text variant="text-sm/normal" style={{ color: "var(--text-muted)" }}>
Contributors:
</Text>
<AuthorUserSummaryItem authors={contributors} />
</Flex>
}
</Flex>
);
};

View file

@ -0,0 +1,37 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button } from "@webpack/common";
import React from "react";
import { IconTooltipButton } from ".";
export const CopyButton = (props: typeof Button["defaultProps"]) => {
return (
<IconTooltipButton
color={Button.Colors.PRIMARY}
tooltipText="Copy Profile"
icon={
<svg width="18" height="18" viewBox="0 0 24 24">
<path d="M7.024 3.75c0-.966.784-1.75 1.75-1.75H20.25c.966 0 1.75.784 1.75 1.75v11.498a1.75 1.75 0 0 1-1.75 1.75H8.774a1.75 1.75 0 0 1-1.75-1.75Zm1.75-.25a.25.25 0 0 0-.25.25v11.498c0 .139.112.25.25.25H20.25a.25.25 0 0 0 .25-.25V3.75a.25.25 0 0 0-.25-.25Z" fill="currentColor"></path>
<path d="M1.995 10.749a1.75 1.75 0 0 1 1.75-1.751H5.25a.75.75 0 1 1 0 1.5H3.745a.25.25 0 0 0-.25.25L3.5 20.25c0 .138.111.25.25.25h9.5a.25.25 0 0 0 .25-.25v-1.51a.75.75 0 1 1 1.5 0v1.51A1.75 1.75 0 0 1 13.25 22h-9.5A1.75 1.75 0 0 1 2 20.25l-.005-9.501Z" fill="currentColor"></path>
</svg>
}
{...props} />
);
};

View file

@ -0,0 +1,39 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button } from "@webpack/common";
import * as types from "@webpack/types";
import React from "react";
import { IconTooltipButton } from ".";
export const DeleteButton = (props: types.Button["defaultProps"]) => {
return (
<IconTooltipButton
color={Button.Colors.RED}
tooltipText="Delete Profile"
icon={
<svg width="18" height="18" viewBox="0 0 24 24">
<path d="M16 1.75V3h5.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H8V1.75C8 .784 8.784 0 9.75 0h4.5C15.216 0 16 .784 16 1.75Zm-6.5 0V3h5V1.75a.25.25 0 0 0-.25-.25h-4.5a.25.25 0 0 0-.25.25ZM4.997 6.178a.75.75 0 1 0-1.493.144L4.916 20.92a1.75 1.75 0 0 0 1.742 1.58h10.684a1.75 1.75 0 0 0 1.742-1.581l1.413-14.597a.75.75 0 0 0-1.494-.144l-1.412 14.596a.25.25 0 0 1-.249.226H6.658a.25.25 0 0 1-.249-.226L4.997 6.178Z" fill="currentColor"></path>
<path d="M9.206 7.501a.75.75 0 0 1 .793.705l.5 8.5A.75.75 0 1 1 9 16.794l-.5-8.5a.75.75 0 0 1 .705-.793Zm6.293.793A.75.75 0 1 0 14 8.206l-.5 8.5a.75.75 0 0 0 1.498.088l.5-8.5Z" fill="currentColor"></path>
</svg>
}
{...props} />
);
};

View file

@ -0,0 +1,44 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { Button, Tooltip } from "@webpack/common";
import React from "react";
export interface IconTooltipButtonProps {
tooltipText?: string;
icon?: JSX.Element;
}
export const IconTooltipButton = (props: typeof Button["defaultProps"] & IconTooltipButtonProps) => {
return (
<Tooltip text={props.tooltipText}>
{tooltipProps => <Button
size={Button.Sizes.ICON}
{...props as any}
style={{ aspectRatio: 1, maxHeight: "32px", boxSizing: "border-box", ...props.style }}
>
<Flex style={{ justifyContent: "center", alignItems: "center", width: 24, height: 24 }}>
{props.icon}
</Flex>
<span {...tooltipProps} style={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }} />
</Button>}
</Tooltip >
);
};

View file

@ -0,0 +1,37 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button } from "@webpack/common";
import React from "react";
import { IconTooltipButton } from ".";
export const NewButton = (props: typeof Button["defaultProps"]) => {
return (
<IconTooltipButton
color={Button.Colors.PRIMARY}
tooltipText="New Profile"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
}
{...props} />
);
};

View file

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Button } from "@webpack/common";
import React from "react";
import { IconTooltipButton } from ".";
export const SaveButton = (props: typeof Button["defaultProps"]) => {
return (
<IconTooltipButton
color={Button.Colors.GREEN}
tooltipText="Save Profile"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" >
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
}
{...props} />
);
};

View file

@ -0,0 +1,23 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./CopyButton";
export * from "./DeleteButton";
export * from "./IconTooltipButton";
export * from "./NewButton";
export * from "./SaveButton";

View file

@ -0,0 +1,23 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./AuthorSummaryItem";
export * from "./buttons";
export * from "./ContributorAuthorSummary";
export * from "./settingsModal";
export * from "./settingsPanel";

View file

@ -0,0 +1,77 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot } from "@utils/modal";
import { Button, Text } from "@webpack/common";
import React from "react";
import { Author, Contributor } from "../../types";
import { ContributorAuthorSummary } from "../ContributorAuthorSummary";
export interface SettingsModalProps extends React.ComponentProps<typeof ModalRoot> {
title?: string;
onClose: () => void;
onDone?: () => void;
footerContent?: JSX.Element;
closeButtonName?: string;
author?: Author,
contributors?: Contributor[];
}
export const SettingsModal = (props: SettingsModalProps) => {
const doneButton =
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.BRAND}
onClick={props.onDone}
>
{props.closeButtonName ?? "Done"}
</Button>;
return (
<ModalRoot {...props}>
<ModalHeader separator={false}>
{props.title && <Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{props.title}</Text>}
<div style={{ marginLeft: "auto" }}>
<ModalCloseButton onClick={props.onClose} />
</div>
</ModalHeader>
<ModalContent style={{ marginBottom: "1em", display: "flex", flexDirection: "column", gap: "1em" }}>
{props.children}
</ModalContent>
<ModalFooter>
<Flex style={{ width: "100%" }}>
<div style={{ flex: 1, display: "flex" }}>
{(props.author || props.contributors && props.contributors.length > 0) &&
<Flex style={{ justifyContent: "flex-start", alignItems: "center", flex: 1 }}>
<ContributorAuthorSummary
author={props.author}
contributors={props.contributors} />
</Flex>
}
{props.footerContent}
</div>
<div style={{ marginLeft: "auto" }}>{doneButton}</div>
</Flex>
</ModalFooter>
</ModalRoot >
);
};

View file

@ -0,0 +1,83 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Switch } from "@components/Switch";
import { Card, Forms } from "@webpack/common";
import React from "react";
export interface SettingsModalItemProps extends Pick<React.ComponentProps<"div">,
| "children"> {
title?: string;
switchEnabled?: boolean;
switchProps?: React.ComponentProps<typeof Switch>;
flex?: number;
cardProps?: React.ComponentProps<typeof Card>;
}
export const SettingsModalCard = ({ children, title, switchProps, switchEnabled, flex, cardProps }: SettingsModalItemProps) => {
return (
<Card
{...cardProps}
style={{
padding: "1em",
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
flex: flex ?? 1,
...(cardProps?.style ? cardProps.style : {})
}}>
{title && <Forms.FormTitle tag="h5" style={{ margin: 0 }}>{title}</Forms.FormTitle>}
<div style={{
display: "flex",
gap: "1em",
height: "100%",
justifyContent: "center",
alignItems: "center",
paddingTop: "0.6em",
}}>
{children &&
<div style={{
display: "flex",
alignItems: "flex-end",
gap: "1em",
flex: 1
}}>
{children}
</div>
}
{switchEnabled &&
<div style={{
display: "flex",
height: "100%",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}>
<Forms.FormTitle tag="h5">Status</Forms.FormTitle>
<Switch
checked={false}
onChange={() => void 0}
disabled={false}
{...switchProps}
/>
</div>
}
</div>
</Card >
);
};

View file

@ -0,0 +1,39 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@webpack/common";
import React from "react";
export interface SettingsModalCardItemProps extends Pick<React.ComponentProps<"div">,
| "children"> {
title?: string;
}
export const SettingsModalCardItem = ({ children, title }: SettingsModalCardItemProps) => {
return (
<div style={{
display: "flex",
flexDirection: "column",
gap: "0.4em",
width: "100%"
}}>
{title && <Forms.FormTitle tag="h5" style={{ margin: 0 }}>{title}</Forms.FormTitle>}
{children}
</div>
);
};

View file

@ -0,0 +1,31 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import React from "react";
export interface SettingsModalRowProps extends Pick<React.ComponentProps<"div">,
| "children"
| "style"> {
gap?: string;
}
export const SettingsModalCardRow = ({ children, style, gap }: SettingsModalRowProps) => {
return (
<div style={{ display: "flex", gap: gap ?? "1em", ...style }}>{children}</div>
);
};

View file

@ -0,0 +1,113 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Flex } from "@components/Flex";
import { Select, TextInput, useEffect, useState } from "@webpack/common";
import { PluginSettings, ProfilableStore } from "../../../philsPluginLibrary";
import { CopyButton, DeleteButton, NewButton, SaveButton } from "../buttons";
import { SettingsModalCard } from "./SettingsModalCard";
import { SettingsModalCardItem } from "./SettingsModalCardItem";
export interface SettingsModalProfilesCardProps<T extends PluginSettings = {}> extends React.ComponentProps<typeof SettingsModalCard> {
profileableStore: ProfilableStore<T, any>;
onSaveStateChanged: (isSaving: boolean) => void;
}
export const SettingsModalProfilesCard = <T extends PluginSettings = {},>(props: SettingsModalProfilesCardProps<T>) => {
const { profileableStore: { use } } = props;
const {
currentProfile,
setCurrentProfile,
deleteProfile,
getCurrentProfile,
getDefaultProfiles,
getProfile,
getProfiles,
saveProfile,
isCurrentProfileADefaultProfile
} = use();
const { name } = currentProfile;
const [isSaving, setIsSaving] = useState(false);
const [profileNameInput, setProfileNameInput] = useState<string>("");
useEffect(() => {
props.onSaveStateChanged(isSaving);
}, [isSaving]);
const onSaveProfile = () => {
if (!isSaving) {
setIsSaving(true);
} else {
if (profileNameInput.length && !getDefaultProfiles().some(value => value.name === profileNameInput)) {
saveProfile({ ...getCurrentProfile(), name: profileNameInput });
setCurrentProfile(getProfile(profileNameInput) || { name: "" });
setIsSaving(false);
}
}
};
const onCopyProfile = () => {
setCurrentProfile({ ...getCurrentProfile(), name: "" });
};
const onNewProfile = () => {
setCurrentProfile({ name: "" });
};
const onDeleteProfile = () => {
deleteProfile(currentProfile);
};
return (
<SettingsModalCard
title="Profile"
{...props}>
<SettingsModalCardItem>
<Flex style={{ alignItems: "center" }}>
<div style={{ flex: 1 }}>
{isSaving
? <TextInput
style={{ width: "100%" }}
placeholder="Insert name"
value={profileNameInput}
onChange={setProfileNameInput} />
: <Select
isSelected={value => name === value}
options={getProfiles(true).map(profile => ({
label: profile.name,
value: profile.name
}))}
select={value => setCurrentProfile(getProfile(value) || { name: "" })}
serialize={() => ""} />}
</div>
<Flex style={{ gap: "0.8em" }}>
<SaveButton onClick={onSaveProfile} />
<NewButton onClick={onNewProfile} disabled={isSaving} />
<CopyButton onClick={onCopyProfile} disabled={isSaving} />
<DeleteButton onClick={onDeleteProfile} disabled={isSaving || isCurrentProfileADefaultProfile() || !currentProfile.name.length} />
</Flex>
</Flex>
</SettingsModalCardItem>
</SettingsModalCard>
);
};

View file

@ -0,0 +1,23 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./SettingsModal";
export * from "./SettingsModalCard";
export * from "./SettingsModalCardItem";
export * from "./SettingsModalCardRow";
export * from "./SettingsModalProfilesCard";

View file

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React } from "@webpack/common";
import { panelClasses } from "../../discordModules";
export interface SettingsPanelProps {
children: React.ComponentProps<"div">["children"];
}
export const SettingsPanel = ({ children }: SettingsPanelProps) => {
return (
<div
className={panelClasses.container}>
{children}
</div>
);
};

View file

@ -0,0 +1,40 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classes } from "@utils/misc";
import { Button } from "@webpack/common";
import React from "react";
import { panelClasses } from "../../../philsPluginLibrary";
export type IconComponent = <T extends { className: string; }>(props: T) => JSX.Element;
export interface SettingsPanelButtonProps extends Partial<React.ComponentProps<typeof Button>> {
icon?: IconComponent;
}
export const SettingsPanelButton = (props: SettingsPanelButtonProps) => {
return (
<Button
size={Button.Sizes.SMALL}
className={classes(panelClasses.button, panelClasses.buttonColor)}
innerClassName={classes(panelClasses.buttonContents)}
wrapperClassName={classes(panelClasses.button)}
children={props.icon && <props.icon className={classes(panelClasses.buttonIcon)} />}
{...props} />
);
};

View file

@ -0,0 +1,37 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classes } from "@utils/misc";
import React from "react";
import { panelClasses } from "../../discordModules";
export interface SettingsPanelRowProps {
children: React.ComponentProps<"div">["children"];
}
export const SettingsPanelRow = ({ children }: SettingsPanelRowProps) => {
return (
<div
className={classes(panelClasses.actionButtons)}
style={{ padding: 0 }}
>
{children}
</div>
);
};

View file

@ -0,0 +1,34 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Tooltip } from "@webpack/common";
import React from "react";
import { SettingsPanelButton, SettingsPanelButtonProps } from "./SettingsPanelButton";
export interface SettingsPanelTooltipButtonProps extends SettingsPanelButtonProps {
tooltipProps: Omit<React.ComponentProps<typeof Tooltip>, "children">;
}
export const SettingsPanelTooltipButton = (props: SettingsPanelTooltipButtonProps) => {
return (
<Tooltip {...props.tooltipProps}>
{tooltipProps => <SettingsPanelButton {...tooltipProps} {...props} />}
</Tooltip>
);
};

View file

@ -0,0 +1,22 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./SettingsPanel";
export * from "./SettingsPanelButton";
export * from "./SettingsPanelRow";
export * from "./SettingsPanelTooltipButton";

View file

@ -0,0 +1,30 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import * as types from "../types/constants";
export const PluginInfo: types.PluginInfo = {
PLUGIN_NAME: "PhilsPluginLibrary",
DESCRIPTION: "A library for phil's plugins",
AUTHOR: {
...Devs.philhk,
github: "https://github.com/philhk"
},
} as const;

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./constants";

View file

@ -0,0 +1,25 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { findByPropsLazy } from "@webpack";
import * as types from "../types";
export const panelClasses: types.PanelClasses = findByPropsLazy("button", "buttonContents", "buttonColor");
// waitFor(filters.byProps("button", "buttonContents", "buttonColor"), result => panelClasses = result);

View file

@ -0,0 +1,24 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent } from "@utils/react";
import { findByCode } from "@webpack";
import { types } from "../";
export const UserSummaryItem = LazyComponent<React.ComponentProps<types.UserSummaryItem>>(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));

View file

@ -0,0 +1,22 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./classes";
export * from "./components";
export * from "./modules";
export * from "./stores";

View file

@ -0,0 +1,25 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { filters, waitFor } from "@webpack";
import * as types from "../types";
export let utils: types.Utils;
waitFor(filters.byProps("getPidFromDesktopSource"), result => utils = result);

View file

@ -0,0 +1,25 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { waitForStore } from "webpack/common/internal";
import * as types from "../types";
export let MediaEngineStore: types.MediaEngineStore;
waitForStore("MediaEngineStore", store => MediaEngineStore = store);

View file

@ -0,0 +1,86 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import EventEmitter from "events";
import TypedEmitter from "typed-emitter";
export type TypedEmitterEvents<J extends TypedEmitter<any>> = J extends TypedEmitter<
infer N
>
? N
: never;
export interface EmitterEvent {
emitter: TypedEmitter<any> | EventEmitter;
event: any;
fn: (...args: any[]) => any;
plugin?: string;
}
export class Emitter {
private static events: EmitterEvent[] = [];
public static addListener<
T extends TypedEmitter<any>,
U extends keyof TypedEmitterEvents<T>,
V extends TypedEmitterEvents<T>[U]
>(
emitter: T,
type: keyof Pick<EventEmitter, "on" | "once">,
event: U,
fn: V,
plugin?: string
): () => void;
public static addListener(
emitter: EventEmitter,
type: keyof Pick<EventEmitter, "on" | "once">,
event: string,
fn: (...args: any[]) => void,
plugin?: string
): () => void {
emitter[type](event, fn);
const emitterEvenet: EmitterEvent = {
emitter,
event,
fn,
plugin: plugin
};
this.events.push(emitterEvenet);
return () => this.removeListener(emitterEvenet);
}
public static removeListener(emitterEvent: EmitterEvent) {
emitterEvent.emitter.removeListener(emitterEvent.event, emitterEvent.fn);
this.events = this.events.filter(
emitterEvent_ => emitterEvent_ !== emitterEvent
);
}
public static removeAllListeners(plugin?: string) {
if (!plugin) {
this.events.forEach(emitterEvent =>
this.removeListener(emitterEvent)
);
} else
this.events.forEach(emitterEvent =>
plugin === emitterEvent.plugin && this.removeListener(emitterEvent)
);
}
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./emitter";

View file

@ -0,0 +1,142 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const ScreenshareSettingsIcon =
(props: React.ComponentProps<"svg">) =>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1000"
height="1000"
viewBox="0 0 1000 1000"
{...props}
>
<defs>
<mask id="screenshareSettingsIconMask">
<path
fill="currentColor"
fillRule="evenodd"
d="M2 4.5c0-1.103.897-2 2-2h16c1.103 0 2 .897 2 2v11c0 1.104-.897 2-2 2h-7v2h4v2H7v-2h4v-2H4c-1.103 0-2-.896-2-2v-11zm11.2 9.838V11.6c-3.336 0-5.532 1.063-7.2 3.4.672-3.338 2.532-6.662 7.2-7.338V5L18 9.662l-4.8 4.675z"
transform="matrix(43.2813 0 0 43.3063 567.187 588.59) translate(-12 -12)"
vectorEffect="non-scaling-stroke"
></path>
<path
fill="#000"
strokeWidth="0"
// Circle
d="M132.5 67.5c0 35.899-29.101 65-65 65-35.898 0-65-29.101-65-65 0-35.898 29.102-65 65-65 35.899 0 65 29.102 65 65z"
transform="translate(229.14 230.807) scale(4.9157) translate(-67.5 -67.5)"
vectorEffect="non-scaling-stroke"
></path>
</mask>
</defs>
<rect width="100%" height="100%" fill="#fff" mask="url(#screenshareSettingsIconMask)"></rect>
<path
fill="currentColor"
fillRule="evenodd"
strokeWidth="0"
// Settings Icon
d="M19.738 10H22v4h-2.261a7.952 7.952 0 01-1.174 2.564L20 18l-2 2-1.435-1.436A7.946 7.946 0 0114 19.738V22h-4v-2.262a7.94 7.94 0 01-2.564-1.174L6 20l-2-2 1.436-1.436A7.911 7.911 0 014.262 14H2v-4h2.262a7.9 7.9 0 011.174-2.564L4 6l2-2 1.436 1.436A7.9 7.9 0 0110 4.262V2h4v2.261a7.967 7.967 0 012.565 1.174L18 3.999l2 2-1.436 1.437A7.93 7.93 0 0119.738 10zM12 16a4 4 0 100-8 4 4 0 000 8z"
transform="translate(229.812 230.81) scale(23.0217) translate(-12 -12)"
vectorEffect="non-scaling-stroke"
></path>
</svg>;
export const CameraSettingsIcon =
(props: React.ComponentProps<"svg">) =>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1000"
height="1000"
viewBox="0 0 1000 1000"
{...props}
>
<defs>
<mask id="m">
<path
fill="currentColor"
fillRule="evenodd"
d="M21.526 8.149a1 1 0 00-.973-.044L18 9.382V7c0-1.103-.897-2-2-2H4c-1.103 0-2 .897-2 2v10c0 1.104.897 2 2 2h12c1.103 0 2-.896 2-2v-2.382l2.553 1.276a.992.992 0 00.973-.043c.294-.183.474-.504.474-.851V9c0-.347-.18-.668-.474-.851z"
transform="translate(586.527 617.666) scale(41.3472) translate(-12 -12)"
vectorEffect="non-scaling-stroke"
></path>
<path
fill="#000"
strokeWidth="0"
// Circle
d="M132.5 67.5c0 35.899-29.101 65-65 65-35.898 0-65-29.101-65-65 0-35.898 29.102-65 65-65 35.899 0 65 29.102 65 65z"
transform="translate(229.14 230.807) scale(4.9157) translate(-67.5 -67.5)"
vectorEffect="non-scaling-stroke"
></path>
</mask>
</defs>
<rect width="100%" height="100%" fill="#fff" mask="url(#m)"></rect>
<path
fill="currentColor"
fillRule="evenodd"
strokeWidth="0"
// Settings Icon
d="M19.738 10H22v4h-2.261a7.952 7.952 0 01-1.174 2.564L20 18l-2 2-1.435-1.436A7.946 7.946 0 0114 19.738V22h-4v-2.262a7.94 7.94 0 01-2.564-1.174L6 20l-2-2 1.436-1.436A7.911 7.911 0 014.262 14H2v-4h2.262a7.9 7.9 0 011.174-2.564L4 6l2-2 1.436 1.436A7.9 7.9 0 0110 4.262V2h4v2.261a7.967 7.967 0 012.565 1.174L18 3.999l2 2-1.436 1.437A7.93 7.93 0 0119.738 10zM12 16a4 4 0 100-8 4 4 0 000 8z"
transform="translate(229.812 230.81) scale(23.0217) translate(-12 -12)"
vectorEffect="non-scaling-stroke"
></path>
</svg>;
export const MicrophoneSettingsIcon =
(props: React.ComponentProps<"svg">) =>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1000"
height="1000"
viewBox="0 0 1000 1000"
{...props}
>
<defs>
<mask id="microphoneSettingsIcon">
<path
fill="currentColor"
fillRule="evenodd"
d="M14.99 11c0 1.66-1.33 3-2.99 3-1.66 0-3-1.34-3-3V5c0-1.66 1.34-3 3-3s3 1.34 3 3l-.01 6zM12 16.1c2.76 0 5.3-2.1 5.3-5.1H19c0 3.42-2.72 6.24-6 6.72V21h-2v-3.28c-3.28-.49-6-3.31-6-6.72h1.7c0 3 2.54 5.1 5.3 5.1zM12 4c-.8 0-1 .667-1 1v6c0 .333.2 1 1 1s1-.667 1-1V5c0-.333-.2-1-1-1z"
transform="translate(689.616 574.556) scale(48.6222) translate(0 -.417) scale(.8333) translate(-12 -11.5)"
></path>
<path
fill="currentColor"
fillRule="evenodd"
d="M14.99 11c0 1.66-1.33 3-2.99 3-1.66 0-3-1.34-3-3V5c0-1.66 1.34-3 3-3s3 1.34 3 3l-.01 6zM12 16.1c2.76 0 5.3-2.1 5.3-5.1H19c0 3.42-2.72 6.24-6 6.72V22h-2v-4.28c-3.28-.49-6-3.31-6-6.72h1.7c0 3 2.54 5.1 5.3 5.1z"
transform="translate(689.616 574.556) scale(48.6222) scale(.8333) translate(-12 -12)"
></path>
<path
fill="#000"
strokeWidth="0"
// Circle
d="M132.5 67.5c0 35.899-29.101 65-65 65-35.898 0-65-29.101-65-65 0-35.898 29.102-65 65-65 35.899 0 65 29.102 65 65z"
transform="translate(229.14 230.807) scale(4.9157) translate(-67.5 -67.5)"
vectorEffect="non-scaling-stroke"
></path>
</mask>
</defs>
<rect width="100%" height="100%" fill="#fff" mask="url(#microphoneSettingsIcon)"></rect>
<path
fill="currentColor"
fillRule="evenodd"
strokeWidth="0"
// Settings Icon
d="M19.738 10H22v4h-2.261a7.952 7.952 0 01-1.174 2.564L20 18l-2 2-1.435-1.436A7.946 7.946 0 0114 19.738V22h-4v-2.262a7.94 7.94 0 01-2.564-1.174L6 20l-2-2 1.436-1.436A7.911 7.911 0 014.262 14H2v-4h2.262a7.9 7.9 0 011.174-2.564L4 6l2-2 1.436 1.436A7.9 7.9 0 0110 4.262V2h4v2.261a7.967 7.967 0 012.565 1.174L18 3.999l2 2-1.436 1.437A7.93 7.93 0 0119.738 10zM12 16a4 4 0 100-8 4 4 0 000 8z"
transform="translate(229.812 230.81) scale(23.0217) translate(-12 -12)"
vectorEffect="non-scaling-stroke"
></path>
</svg>;

View file

@ -0,0 +1,50 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { replacedUserPanelComponent } from "./patches";
export default definePlugin({
name: "PhilsPluginLibrary",
description: "A library for phil's plugins",
authors: [Devs.philhk],
replacedUserPanelComponent: replacedUserPanelComponent,
patches: [
{
find: "Messages.ACCOUNT_A11Y_LABEL",
replacement: {
match: /(?<=function)( .{0,8}(?={).)(.{0,1000}isFullscreenInContext\(\).+?\)]}\))(})/,
replace: "$1return $self.replacedUserPanelComponent(function(){$2}, this, arguments)$3"
}
}
],
});
export * from "./components";
export * from "./discordModules";
export * from "./emitter";
export * from "./icons";
export * from "./patchers";
export * from "./patches";
export * from "./store";
export * as types from "./types";
export * from "./utils";

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./patcher";

View file

@ -0,0 +1,29 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export abstract class Patcher {
protected unpatchFunctions: (() => any)[] = [];
public abstract patch(): this;
public abstract unpatch(): this;
protected _unpatch(): this {
this.unpatchFunctions.forEach(fn => fn());
this.unpatchFunctions = [];
return this;
}
}

View file

@ -0,0 +1,88 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { lodash } from "@webpack/common";
import { MicrophoneProfile, MicrophoneStore } from "../../betterMicrophone.desktop/stores";
import { ProfilableStore, replaceObjectValuesIfExist, types } from "../../philsPluginLibrary";
export function getDefaultAudioTransportationOptions(connection: types.Connection) {
return {
audioEncoder: {
...connection.getCodecOptions("opus").audioEncoder,
},
encodingVoiceBitRate: 64000
};
}
export function getReplaceableAudioTransportationOptions(connection: types.Connection, get: ProfilableStore<MicrophoneStore, MicrophoneProfile>["get"]) {
const { currentProfile } = get();
const {
channels,
channelsEnabled,
freq,
freqEnabled,
pacsize,
pacsizeEnabled,
rate,
rateEnabled,
voiceBitrate,
voiceBitrateEnabled
} = currentProfile;
return {
...(voiceBitrateEnabled && voiceBitrate
? {
encodingVoiceBitRate: voiceBitrate * 1000
}
: {}
),
audioEncoder: {
...connection.getCodecOptions("opus").audioEncoder,
...(rateEnabled && rate ? { rate } : {}),
...(pacsizeEnabled && pacsize ? { pacsize } : {}),
...(freqEnabled && freq ? { freq } : {}),
...(channelsEnabled && channels ? { channels } : {})
}
};
}
export function patchConnectionAudioTransportOptions(
connection: types.Connection,
get: ProfilableStore<MicrophoneStore, MicrophoneProfile>["get"],
logger?: Logger
) {
const oldSetTransportOptions = connection.conn.setTransportOptions;
connection.conn.setTransportOptions = function (this: any, options: Record<string, any>) {
replaceObjectValuesIfExist(options, getReplaceableAudioTransportationOptions(connection, get));
return Reflect.apply(oldSetTransportOptions, this, [options]);
};
const forceUpdateTransportationOptions = () => {
const transportOptions = lodash.merge({ ...getDefaultAudioTransportationOptions(connection) }, getReplaceableAudioTransportationOptions(connection, get));
logger?.info("Overridden Transport Options", transportOptions);
oldSetTransportOptions(transportOptions);
};
return { oldSetTransportOptions, forceUpdateTransportationOptions };
}

View file

@ -0,0 +1,21 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./audio";
export * from "./userPanel";
export * from "./video";

View file

@ -0,0 +1,107 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React } from "@webpack/common";
import { SettingsPanel } from "../components";
import { IconComponent, SettingsPanelButton } from "../components/settingsPanel/SettingsPanelButton";
import { SettingsPanelRow } from "../components/settingsPanel/SettingsPanelRow";
import { SettingsPanelTooltipButton } from "../components/settingsPanel/SettingsPanelTooltipButton";
export interface PanelButton {
name: string,
tooltipText?: string,
icon?: IconComponent;
onClick?: () => void;
}
const settingsPanelButtonsSubscriptions = new Set<React.DispatchWithoutAction>();
export const settingsPanelButtons: PanelButton[] = new Proxy<PanelButton[]>([], {
set: (target, p, newValue) => {
target[p] = newValue;
settingsPanelButtonsSubscriptions.forEach(fn => fn());
return true;
},
});
export const useButtons = () => {
const [, forceUpdate] = React.useReducer(() => ({}), {});
React.useEffect(() => {
settingsPanelButtonsSubscriptions.add(forceUpdate);
return () => void settingsPanelButtonsSubscriptions.delete(() => forceUpdate);
}, []);
return settingsPanelButtons;
};
export const ButtonsSettingsPanel = () => {
const rawPanelButtons = useButtons();
const convertRawPanelButtons = (buttons: PanelButton[]) => {
const settingsPanelButtonsClone = [...buttons].sort();
const groupedButtons: JSX.Element[][] = [];
while (settingsPanelButtonsClone.length) {
const splicedButtons =
settingsPanelButtonsClone
.splice(0, 3)
.map(({ icon, tooltipText, onClick }) =>
tooltipText
? <SettingsPanelTooltipButton tooltipProps={{ text: tooltipText }} icon={icon} onClick={onClick} />
: <SettingsPanelButton icon={icon} onClick={onClick} />
);
groupedButtons.push(splicedButtons);
}
return groupedButtons;
};
return rawPanelButtons.length > 0
? <SettingsPanel>
{...convertRawPanelButtons(rawPanelButtons).map(value => <SettingsPanelRow children={value} />)}
</SettingsPanel>
: <>
</>;
};
export function replacedUserPanelComponent(oldComponent: (...args: any[]) => any, thisContext: any, functionArguments: any) {
const componentResult: JSX.Element = Reflect.apply(oldComponent, thisContext, functionArguments);
if (!componentResult?.props) return componentResult;
const { children } = componentResult.props;
const userPanel = children.at(-2);
const userPanelChildren = userPanel.props.children;
userPanelChildren.splice(userPanelChildren.length - 1, 0,
<ButtonsSettingsPanel />
);
return componentResult;
}
export function addSettingsPanelButton(settings: PanelButton) {
settingsPanelButtons.push(settings);
}
export function removeSettingsPanelButton(name: string) {
settingsPanelButtons.splice(0, settingsPanelButtons.length, ...settingsPanelButtons.filter(value => value.name !== name));
}

View file

@ -0,0 +1,253 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { lodash } from "@webpack/common";
import { ScreenshareProfile, ScreenshareStore } from "../../betterScreenshare.dev/stores";
import { ProfilableStore, replaceObjectValuesIfExist, types, utils } from "../../philsPluginLibrary";
export function getDefaultVideoTransportationOptions(connection: types.Connection) {
return {
...connection.videoQualityManager.applyQualityConstraints({}).constraints,
videoEncoder: {
...connection.getCodecOptions("", "H264", "stream").videoEncoder
},
streamParameters: connection.videoStreamParameters[0],
keyframeInterval: 0,
};
}
export function getDefaultVideoDesktopSourceOptions(connection: types.Connection) {
const [type, sourceId] = connection.goLiveSourceIdentifier?.split(":") ?? ["screen", 0];
return {
hdrCaptureMode: "never",
allowScreenCaptureKit: true,
useQuartzCapturer: true,
useGraphicsCapture: true,
useVideoHook: true,
sourceId: sourceId,
type: type
};
}
export function getStreamParameters(connection: types.Connection, get: ProfilableStore<ScreenshareStore, ScreenshareProfile>["get"]) {
const { currentProfile } = get();
const {
framerate,
framerateEnabled,
height,
resolutionEnabled,
videoBitrate,
videoBitrateEnabled,
width,
} = currentProfile;
const { bitrateMax, capture } = connection.applyQualityConstraints({}).quality;
return {
...connection.videoStreamParameters[0],
...(videoBitrateEnabled && videoBitrate
? {
maxBitrate: videoBitrate * 1000,
}
: {
maxBitrate: bitrateMax
}
),
...((resolutionEnabled && width && height)
? {
maxResolution: {
height: height,
width: width,
type: "fixed"
},
maxPixelCount: width * height
}
: {
maxResolution: !capture.height || !capture.width ? {
height: capture.height,
width: capture.width,
type: "source"
} : {
height: capture.height,
width: capture.width,
type: "fixed"
}
}
),
...(framerateEnabled && framerate
? {
maxFrameRate: framerate,
}
: {
maxFrameRate: capture.framerate
}
),
active: true,
};
}
export function getReplaceableVideoTransportationOptions(connection: types.Connection, get: ProfilableStore<ScreenshareStore, ScreenshareProfile>["get"]) {
const { currentProfile, audioSource, audioSourceEnabled } = get();
const {
framerate,
framerateEnabled,
height,
keyframeInterval,
keyframeIntervalEnabled,
resolutionEnabled,
videoBitrate,
videoBitrateEnabled,
videoCodec,
videoCodecEnabled,
width,
} = currentProfile;
return {
...(videoBitrateEnabled && videoBitrate
? {
encodingVideoBitRate: videoBitrate * 1000,
encodingVideoMinBitRate: videoBitrate * 1000,
encodingVideoMaxBitRate: videoBitrate * 1000,
callBitRate: videoBitrate * 1000,
callMinBitRate: videoBitrate * 1000,
callMaxBitRate: videoBitrate * 1000
}
: {}
),
...((resolutionEnabled && width && height)
? {
encodingVideoHeight: height,
encodingVideoWidth: width,
remoteSinkWantsPixelCount: height * width
}
: {}
),
...(framerateEnabled && framerate
? {
encodingVideoFrameRate: framerate,
remoteSinkWantsMaxFramerate: framerate
}
: {}
),
...(keyframeIntervalEnabled && keyframeInterval
? {
keyframeInterval: keyframeInterval
}
: {}
),
...(videoCodecEnabled && videoCodec
? {
videoEncoder: connection.getCodecOptions("", videoCodec, "stream").videoEncoder
}
: {}
),
...(audioSourceEnabled && audioSource && utils.getPidFromDesktopSource(audioSource)
? {
soundsharePid: utils.getPidFromDesktopSource(audioSource),
soundshareEventDriven: true,
soundshareLoopback: true
}
: {}
),
streamParameters: getStreamParameters(connection, get)
};
}
export function getReplaceableVideoDesktopSourceOptions(get: ProfilableStore<ScreenshareStore, ScreenshareProfile>["get"]) {
const { currentProfile } = get();
const {
hdrEnabled,
} = currentProfile;
return {
...(hdrEnabled
? {
hdrCaptureMode: "always"
}
: {}
),
};
}
export function patchConnectionVideoSetDesktopSourceWithOptions(
connection: types.Connection,
get: ProfilableStore<ScreenshareStore, ScreenshareProfile>["get"],
logger?: Logger
) {
const oldSetDesktopSourceWithOptions = connection.conn.setDesktopSourceWithOptions;
connection.conn.setDesktopSourceWithOptions = function (this: any, options: Record<string, any>) {
const replaceableDesktopSourceOptions = getReplaceableVideoDesktopSourceOptions(get);
replaceObjectValuesIfExist(options, replaceableDesktopSourceOptions);
logger?.info("Overridden Desktop Source Options", options);
return Reflect.apply(oldSetDesktopSourceWithOptions, this, [options]);
};
const forceUpdateDesktopSourceOptions = () => {
const desktopSourceOptions = lodash.merge({ ...getDefaultVideoDesktopSourceOptions(connection) }, getReplaceableVideoDesktopSourceOptions(get));
logger?.info("Force Updated Desktop Source Options", desktopSourceOptions);
oldSetDesktopSourceWithOptions(desktopSourceOptions);
};
return {
oldSetDesktopSourceWithOptions,
forceUpdateDesktopSourceOptions
};
}
export function patchConnectionVideoTransportOptions(
connection: types.Connection,
get: ProfilableStore<ScreenshareStore, ScreenshareProfile>["get"],
logger?: Logger
) {
const oldSetTransportOptions = connection.conn.setTransportOptions;
connection.conn.setTransportOptions = function (this: any, options: Record<string, any>) {
const replaceableTransportOptions = getReplaceableVideoTransportationOptions(connection, get);
if (options.streamParameters)
connection.videoStreamParameters = Array.isArray(options.streamParameters) ? options.streamParameters : [options.streamParameters];
replaceObjectValuesIfExist(options, replaceableTransportOptions);
logger?.info("Overridden Transport Options", options);
return Reflect.apply(oldSetTransportOptions, this, [options]);
};
const forceUpdateTransportationOptions = () => {
const transportOptions = lodash.merge({ ...getDefaultVideoTransportationOptions(connection) }, getReplaceableVideoTransportationOptions(connection, get));
logger?.info("Force Updated Transport Options", transportOptions);
oldSetTransportOptions(transportOptions);
};
return {
oldSetTransportOptions,
forceUpdateTransportationOptions,
};
}

View file

@ -0,0 +1,20 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./profileable";
export * from "./store";

View file

@ -0,0 +1,85 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginInitializer, PluginSettings, PluginStore } from "./store";
export interface ProfileableProfile {
name: string;
}
export interface ProfileableSettings<T extends PluginSettings = {}, B extends ProfileableProfile & T = T & ProfileableProfile> {
currentProfile: B;
profiles: B[];
setCurrentProfile: (f: ((currentProfile: B) => B | undefined) | B | undefined) => void;
getCurrentProfile: () => B;
duplicateProfile: (profile: string | B, name: string) => void;
deleteProfile: (profile: string | B) => void;
saveProfile: (profile: B) => void;
getProfile: (profile: string) => B | undefined;
getProfiles: (defaultProfiles: boolean) => B[],
isCurrentProfileADefaultProfile: () => boolean;
getDefaultProfiles: () => B[];
}
export type ProfilableStore<
T extends PluginSettings = {},
S extends PluginSettings = {}
> = PluginStore<T & ProfileableSettings<S>>;
export type ProfilableMiddleware<
T extends PluginSettings = {},
S extends PluginSettings = {},
B = T & ProfileableSettings<S>
> = PluginInitializer<T & ProfileableSettings<S>, B>;
export type ProfilableInitializer<
T extends PluginSettings = {},
S extends PluginSettings = {}
> = ProfilableMiddleware<T, S, T & Partial<ProfileableSettings<S>>>;
export function profileable<
T extends PluginSettings = {},
S extends PluginSettings = {}
>(f: ProfilableInitializer<T, S>, defaultProfile: ProfileableProfile & S, defaultProfiles: (ProfileableProfile & S)[] = []): ProfilableMiddleware<T, S> {
return (set, get) => ({
currentProfile: defaultProfile,
profiles: [],
getCurrentProfile: () => get().currentProfile,
getProfile: profile => [...get().profiles, ...(defaultProfiles ?? [])].find(p => p.name === profile),
deleteProfile: profile => get().profiles = get().profiles.filter(p => typeof profile === "string" ? p.name !== profile : p.name !== profile.name),
duplicateProfile: (profile, name) => {
const foundProfile = get().profiles.find(p => typeof profile === "string" ? p.name === profile : p.name === profile.name);
if (foundProfile) {
foundProfile.name = name;
get().profiles.push(foundProfile);
}
},
setCurrentProfile: f => {
const currProfile = get().currentProfile;
get().currentProfile = (typeof f === "function" ? f(currProfile) ?? currProfile : f ?? currProfile);
},
saveProfile: profile => {
get().deleteProfile(profile.name);
get().profiles.push(profile);
},
isCurrentProfileADefaultProfile: () => defaultProfiles.some(profile => get().currentProfile.name === profile.name),
getDefaultProfiles: () => defaultProfiles,
getProfiles: defaultProfiles => [...get().profiles, ...(defaultProfiles ? get().getDefaultProfiles() : [])],
...f(set as any, get as any)
});
}

View file

@ -0,0 +1,102 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings, useSettings } from "@api/Settings";
export type PluginSettings = {
[key: string]: any;
};
export type PluginUse<Z extends PluginSettings> = () => Z;
export type PluginGet<Z extends PluginSettings> = () => Z;
export type PluginSet<Z extends PluginSettings> = (s: Z | ((settings: Z) => Z | undefined)) => Z;
export type PluginInitializer<Z extends PluginSettings, T = Z> = (set: PluginSet<Z>, get: PluginGet<Z>) => T;
export interface PluginStore<Z extends PluginSettings> {
use: PluginUse<Z>,
get: PluginGet<Z>,
set: PluginSet<Z>;
}
function createObjectProxy<T extends object>(obj1: T, onUpdate: (updatedObject: T) => void): T {
const handler: ProxyHandler<T> = {
set(target, property, value, receiver) {
const success = Reflect.set(target, property, value, receiver);
const nestedObj = target[property];
if (typeof nestedObj === "object") {
target[property] = createObjectProxy(nestedObj, () => { onUpdate(obj1); }); // On update will call itself until the top level object
}
onUpdate(obj1); // This will recursively call on nested objects
return success;
}
};
return new Proxy(obj1, handler);
}
const startupStates = {};
const settingStorage = new Map();
export function createPluginStore<Z extends PluginSettings = {}>(pluginName: string, storeName: string, f: PluginInitializer<Z>): PluginStore<Z> {
if (!Settings.plugins[pluginName])
throw new Error("The specified plugin does not exist");
if (!Settings.plugins[pluginName].stores)
Settings.plugins[pluginName].stores = {};
if (!Settings.plugins[pluginName].stores[storeName]) // Just incase the store doesn't exist we create it here (otherwise we crash)
Settings.plugins[pluginName].stores[storeName] = {};
const get: PluginGet<Z> = () => {
const storeSettings = settingStorage.get(storeName);
if (!startupStates[storeName]) { // We do this so that we can load all the saved data without the proxy attempting to overwrite it
const startupInfo = Settings.plugins[pluginName].stores[storeName];
Object.keys(startupInfo).forEach(prop => storeSettings[prop] = startupInfo[prop]);
startupStates[storeName] = true;
}
return storeSettings;
};
const set: PluginSet<Z> = (s: ((settings: Z) => Z | undefined) | Z) =>
(typeof s === "function" ? s(get()) : s) || get();
const use: PluginUse<Z> = () => { useSettings().plugins[pluginName].stores[storeName]; return get(); }; // useSettings is called to update renderer (after settings change)
const initialSettings: Z = f(set, get);
const proxiedSettings = createObjectProxy(initialSettings as unknown, updateCallback); // Setup our proxy that allows us connections to the datastore
function updateCallback(updatedObject: any) {
if (!startupStates[storeName]) return; // Wait for the startup information to overwrite the blank proxy
Settings.plugins[pluginName].stores[storeName] = JSON.parse(JSON.stringify(updatedObject));
}
for (const key of Object.keys(initialSettings)) { proxiedSettings[key] = initialSettings[key]; } // Set them so the nested objects also become proxies
settingStorage.set(storeName, proxiedSettings);
updateCallback(initialSettings);
return {
use,
get,
set
};
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./styles";

View file

@ -0,0 +1,25 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const Styles = {
infoCard: {
padding: "1em",
width: "100%",
boxSizing: "border-box"
},
} as const satisfies Record<string, React.CSSProperties>;

View file

@ -0,0 +1,46 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface Resolution {
width: number;
height: number;
}
export interface Framerate {
framerate: number;
}
export interface Bitrate {
min: number;
target: number;
max: number;
}
export type DeepPartial<T> = T extends Function
? T
: T extends Array<infer InferredArrayMember>
? DeepPartialArray<InferredArrayMember>
: T extends object
? DeepPartialObject<T>
: T | undefined;
interface DeepPartialArray<T> extends Array<DeepPartial<T>> { }
type DeepPartialObject<T> = {
[key in keyof T]?: DeepPartial<T[key]>;
};

View file

@ -0,0 +1,31 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginAuthor } from "@utils/types";
export type Author = PluginAuthor & { github?: string; };
export type Contributor = Author;
export interface PluginInfo {
[key: string]: any;
PLUGIN_NAME: string,
DESCRIPTION: string,
AUTHOR: PluginAuthor & { github?: string; },
CONTRIBUTORS?: Record<string, PluginAuthor & { github?: string; }>,
README?: string;
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./panelClasses";

View file

@ -0,0 +1,53 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface PanelClasses {
container: string;
inner: string;
channel: string;
statusWithPopout: string;
hotspot: string;
customStatusContainer: string;
noiseCancellationPopout: string;
noiseCancellationTooltip: string;
krispLogo: string;
krispLink: string;
micTestButton: string;
beta: string;
connection: string;
voiceUsers: string;
actionButtons: string;
button: string;
buttonColor: string;
buttonActive: string;
fauxDisabled: string;
buttonDeveloperActivityShelf: string;
active: string;
buttonContents: string;
buttonIcon: string;
withText: string;
voicePanelIntroductionHeader: string;
voicePanelIntroductionText: string;
voicePanelIntroductionButton: string;
voicePanelIntroductionWrapper: string;
wrapper: string;
viewAsRolesWarning: string;
viewAsRolesWarningText: string;
viewAsRolesWarningButton: string;
disabled: string;
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./userSummaryItem";

View file

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { User } from "discord-types/general";
import type { ComponentType } from "react";
export interface UserSummaryItemProps {
guildId?: string;
className?: string;
users?: User[];
renderUser?: (...props: any[]) => any;
renderMoreUsers?: (...props: any[]) => any;
max?: number;
showUserPopout?: boolean;
renderIcon?: boolean;
showDefaultAvatarsForNullUsers?: boolean;
size?: number;
}
export type UserSummaryItem = ComponentType<UserSummaryItemProps>;

View file

@ -0,0 +1,22 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./classes";
export * from "./components";
export * from "./modules";
export * from "./stores";

View file

@ -0,0 +1,56 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface Conn {
destroy: (...args: any[]) => any;
setTransportOptions: (options: Record<string, any>) => any;
setSelfMute: (...args: any[]) => any;
setSelfDeafen: (...args: any[]) => any;
mergeUsers: (...args: any[]) => any;
destroyUser: (...args: any[]) => any;
setLocalVolume: (...args: any[]) => any;
setLocalMute: (...args: any[]) => any;
setLocalPan: (...args: any[]) => any;
setDisableLocalVideo: (...args: any[]) => any;
setMinimumOutputDelay: (...args: any[]) => any;
getEncryptionModes: (...args: any[]) => any;
configureConnectionRetries: (...args: any[]) => any;
setOnSpeakingCallback: (...args: any[]) => any;
setOnSpeakingWhileMutedCallback: (...args: any[]) => any;
setPingInterval: (...args: any[]) => any;
setPingCallback: (...args: any[]) => any;
setPingTimeoutCallback: (...args: any[]) => any;
setRemoteUserSpeakingStatus: (...args: any[]) => any;
setRemoteUserCanHavePriority: (...args: any[]) => any;
setOnVideoCallback: (...args: any[]) => any;
setVideoBroadcast: (...args: any[]) => any;
setDesktopSource: (...args: any[]) => any;
setDesktopSourceWithOptions: (...args: any[]) => any;
clearDesktopSource: (...args: any[]) => any;
setDesktopSourceStatusCallback: (...args: any[]) => any;
setOnDesktopSourceEnded: (...args: any[]) => any;
setOnSoundshare: (...args: any[]) => any;
setOnSoundshareEnded: (...args: any[]) => any;
setOnSoundshareFailed: (...args: any[]) => any;
setPTTActive: (...args: any[]) => any;
getStats: (...args: any[]) => any;
getFilteredStats: (...args: any[]) => any;
startReplay: (...args: any[]) => any;
startSamplesPlayback: (...args: any[]) => any;
stopSamplesPlayback: (...args: any[]) => any;
}

View file

@ -0,0 +1,387 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import TypedEmitter from "typed-emitter";
import { Framerate, Resolution } from "../../../types";
import { Conn, FramerateReducer, VideoQualityManager } from "./";
export const ConnectionEvent = {
SPEAKING: "speaking",
MUTE: "mute",
NEW_LISTENER: "newListener",
DESTORY: "destroy",
CONNECTED: "connected",
SILENCE: "silence",
DESKTOP_SOURCE_END: "desktopsourceend",
SOUNDSHARE_ATTACHED: "soundshareattached",
SOUNDSHARE_FAILED: "soundsharefailed",
SOUNDSHARE_SPEAKING: "soundsharespeaking",
SOUNDSHARE_TRACE: "soundsharetrace",
INTERACTION_REQUIRED: "interactionrequired",
VIDEOHOOK_INITIALIZED: "videohook-initialize",
SCREENSHARE_FAILED: "screenshare-finish",
NOISE_CANCELLER_ERROR: "noisecancellererror",
VOICE_ACTIVITY_DETECTOR_ERROR: "voiceactivitydetectorerror",
VIDEO_STATE: "video-state",
VIDEO: "video",
FIRST_FRAME: "first-frame",
ERROR: "error",
CONNECTION_STATE_CHANGE: "connectionstatechange",
PING: "ping",
PING_TIMEOUT: "pingtimeout",
OUTBOUND_LOSSRATE: "outboundlossrate",
LOCAL_VIDEO_DISABLED: "local-video-disabled",
STATS: "stats",
} as const;
export type ConnectionEvent = typeof ConnectionEvent;
export type ConnectionEvents = {
[ConnectionEvent.SPEAKING]: (...args: any[]) => any;
[ConnectionEvent.MUTE]: (...args: any[]) => any;
[ConnectionEvent.NEW_LISTENER]: (...args: any[]) => any;
[ConnectionEvent.DESTORY]: (...args: any[]) => any;
[ConnectionEvent.CONNECTED]: (...args: any[]) => any;
[ConnectionEvent.SILENCE]: (...args: any[]) => any;
[ConnectionEvent.DESKTOP_SOURCE_END]: (...args: any[]) => any;
[ConnectionEvent.SOUNDSHARE_ATTACHED]: (...args: any[]) => any;
[ConnectionEvent.SOUNDSHARE_FAILED]: (...args: any[]) => any;
[ConnectionEvent.SOUNDSHARE_SPEAKING]: (...args: any[]) => any;
[ConnectionEvent.SOUNDSHARE_TRACE]: (...args: any[]) => any;
[ConnectionEvent.INTERACTION_REQUIRED]: (...args: any[]) => any;
[ConnectionEvent.VIDEOHOOK_INITIALIZED]: (...args: any[]) => any;
[ConnectionEvent.SCREENSHARE_FAILED]: (...args: any[]) => any;
[ConnectionEvent.NOISE_CANCELLER_ERROR]: (...args: any[]) => any;
[ConnectionEvent.VOICE_ACTIVITY_DETECTOR_ERROR]: (...args: any[]) => any;
[ConnectionEvent.VIDEO_STATE]: (...args: any[]) => any;
[ConnectionEvent.VIDEO]: (...args: any[]) => any;
[ConnectionEvent.FIRST_FRAME]: (...args: any[]) => any;
[ConnectionEvent.ERROR]: (...args: any[]) => any;
[ConnectionEvent.CONNECTION_STATE_CHANGE]: (...args: any[]) => any;
[ConnectionEvent.PING]: (...args: any[]) => any;
[ConnectionEvent.PING_TIMEOUT]: (...args: any[]) => any;
[ConnectionEvent.OUTBOUND_LOSSRATE]: (...args: any[]) => any;
[ConnectionEvent.LOCAL_VIDEO_DISABLED]: (...args: any[]) => any;
[ConnectionEvent.STATS]: (...args: any[]) => any;
};
export type Connection = TypedEmitter<ConnectionEvents> &
Connection__ &
Connection_ & {
streamUserId: string,
goLiveSourceIdentifier?: string;
emitter: TypedEmitter<ConnectionEvents>;
mediaEngineConnectionId: string;
destroyed: boolean;
audioSSRC: number;
selfDeaf: boolean;
localMutes: LocalMutes;
disabledLocalVideos: LocalMutes;
localVolumes: LocalVolumes;
isActiveOutputSinksEnabled: boolean;
activeOutputSinks: LocalMutes;
videoSupported: boolean;
useElectronVideo: boolean;
voiceBitrate: number;
remoteSinkWantsMaxFramerate: number;
wantsPriority: Set<any>;
localSpeakingFlags: LocalSpeakingFlags;
videoReady: boolean;
videoStreamParameters: VideoStreamParameter[];
remoteVideoSinkWants: VideoSinkWants;
localVideoSinkWants: VideoSinkWants;
connectionState: string;
experimentFlags: string[];
context: string;
ids: Ids;
selfMute: boolean;
selfVideo: boolean;
forceAudioNormal: boolean;
forceAudioPriority: boolean;
codecs: Codec[];
desktopDegradationPreference: number;
sourceDesktopDegradationPreference: number;
videoDegradationPreference: number;
localPans: LocalMutes;
remoteAudioSSRCs: LocalMutes;
remoteVideoSSRCs: LocalMutes;
inputMode: string;
vadThreshold: number;
vadAutoThreshold: boolean;
vadUseKrisp: boolean;
vadLeading: number;
vadTrailing: number;
pttReleaseDelay: number;
soundshareActive: boolean;
soundshareId?: any;
soundshareSentSpeakingEvent: boolean;
echoCancellation: boolean;
noiseSuppression: boolean;
automaticGainControl: boolean;
noiseCancellation: boolean;
experimentalEncoders: boolean;
hardwareH264: boolean;
attenuationFactor: number;
attenuateWhileSpeakingSelf: boolean;
attenuateWhileSpeakingOthers: boolean;
qos: boolean;
minimumJitterBufferLevel: number;
postponeDecodeLevel: number;
reconnectInterval: number;
keyframeInterval: number;
conn: Conn;
stats: Stats;
framerateReducer: FramerateReducer;
videoQualityManager: VideoQualityManager;
handleSpeakingNative: (...args: any[]) => any;
handleSpeakingFlags: (...args: any[]) => any;
handleSpeakingWhileMuted: (...args: any[]) => any;
handlePing: (...args: any[]) => any;
handlePingTimeout: (...args: any[]) => any;
handleVideo: (...args: any[]) => any;
handleFirstFrame: (...args: any[]) => any;
handleNoInput: (...args: any[]) => any;
handleDesktopSourceEnded: (...args: any[]) => any;
handleSoundshare: (...args: any[]) => any;
handleSoundshareFailed: (...args: any[]) => any;
handleSoundshareEnded: (...args: any[]) => any;
handleNewListenerNative: (...args: any[]) => any;
handleStats: (...args: any[]) => any;
__proto__: Connection_;
};
interface Connection_ {
initialize: (...args: any[]) => any;
destroy: (...args: any[]) => any;
setCodecs: (audioCodec: string, videoCodec: string, context: string) => any;
getStats: (...args: any[]) => any;
createUser: (...args: any[]) => any;
destroyUser: (...args: any[]) => any;
setSelfMute: (...args: any[]) => any;
setSelfDeaf: (...args: any[]) => any;
setSoundshareSource: (id: number, loopback: boolean) => void;
setLocalMute: (...args: any[]) => any;
setLocalVideoDisabled: (...args: any[]) => any;
setMinimumJitterBufferLevel: (...args: any[]) => any;
setPostponeDecodeLevel: (...args: any[]) => any;
setClipRecordSsrc: (...args: any[]) => any;
getLocalVolume: (...args: any[]) => any;
setLocalVolume: (...args: any[]) => any;
setLocalPan: (...args: any[]) => any;
isAttenuating: (...args: any[]) => any;
setAttenuation: (...args: any[]) => any;
setCanHavePriority: (...args: any[]) => any;
setBitRate: (...args: any[]) => any;
setVoiceBitRate: (target: number) => void;
setCameraBitRate: (target: number, min: number, max: number) => void;
setEchoCancellation: (...args: any[]) => any;
setNoiseSuppression: (...args: any[]) => any;
setAutomaticGainControl: (...args: any[]) => any;
setNoiseCancellation: (...args: any[]) => any;
setExperimentalEncoders: (...args: any[]) => any;
setHardwareH264: (...args: any[]) => any;
setQoS: (...args: any[]) => any;
setInputMode: (...args: any[]) => any;
setSilenceThreshold: (...args: any[]) => any;
setForceAudioInput: (...args: any[]) => any;
setSpeakingFlags: (...args: any[]) => any;
clearAllSpeaking: (...args: any[]) => any;
setEncryption: (...args: any[]) => any;
setReconnectInterval: (...args: any[]) => any;
setKeyframeInterval: (keyframeInterval: number) => void;
setVideoBroadcast: (...args: any[]) => any;
setDesktopSource: (
source: string | null,
options?: DesktopSourceOptions
) => void;
clearDesktopSource: (...args: any[]) => any;
setDesktopSourceStatusCallback: (...args: any[]) => any;
hasDesktopSource: (...args: any[]) => any;
setDesktopEncodingOptions: (
width: number,
height: number,
framerate: number
) => void;
setSDP: (...args: any[]) => any;
setRemoteVideoSinkWants: (...args: any[]) => any;
setLocalVideoSinkWants: (...args: any[]) => any;
startSamplesPlayback: (...args: any[]) => any;
stopSamplesPlayback: (...args: any[]) => any;
startSamplesLocalPlayback: (...args: any[]) => any;
stopAllSamplesLocalPlayback: (...args: any[]) => any;
stopSamplesLocalPlayback: (...args: any[]) => any;
updateVideoQualityCore: (...args: any[]) => any;
setStreamParameters: (...args: any[]) => any;
applyVideoTransportOptions: (...args: any[]) => any;
chooseEncryptionMode: (...args: any[]) => any;
getUserOptions: (...args: any[]) => any;
createInputModeOptions: (...args: any[]) => any;
getAttenuationOptions: (...args: any[]) => any;
getCodecParams: (...args: any[]) => any;
getCodecOptions: (...args: any[]) => any;
getConnectionTransportOptions: (...args: any[]) => any;
setStream: (...args: any[]) => any;
getUserIdBySsrc: (...args: any[]) => any;
setRtcLogEphemeralKey: (...args: any[]) => any;
setRtcLogMarker: (...args: any[]) => any;
__proto__: Connection__;
}
interface Connection__ {
destroy: (...args: any[]) => any;
getLocalMute: (...args: any[]) => any;
getLocalVideoDisabled: (...args: any[]) => any;
setLocalVideoDisabled: (...args: any[]) => any;
getHasActiveVideoOutputSink: (...args: any[]) => any;
setHasActiveVideoOutputSink: (...args: any[]) => any;
getActiveOutputSinkTrackingEnabled: (...args: any[]) => any;
setUseElectronVideo: (...args: any[]) => any;
setClipRecordSsrc: (...args: any[]) => any;
getStreamParameters: (...args: any[]) => any;
setExperimentFlag: (...args: any[]) => any;
setConnectionState: (...args: any[]) => any;
updateVideoQuality: (...args: any[]) => any;
applyVideoQualityMode: (...args: any[]) => any;
overwriteQualityForTesting: (args: {
encode: Resolution & Framerate;
capture: Resolution & Framerate;
bitrateMin: number;
bitrateMax: number;
bitrateTarget: number;
}) => any;
applyQualityConstraints: (...args: any[]) => any;
pickProperties: (...args: any[]) => any;
initializeStreamParameters: (...args: any[]) => any;
getLocalWant: (...args: any[]) => any;
emitStats: (...args: any[]) => any;
__proto__: TypedEmitter<ConnectionEvents>;
}
export interface Stats {
mediaEngineConnectionId: string;
transport: Transport;
camera?: any;
rtp: Rtp;
}
export interface Rtp {
inbound: LocalMutes;
outbound: Outbound[];
}
export interface Outbound {
type: string;
ssrc: number;
sinkWant: string;
codec: OutboundCodec;
bytesSent: number;
packetsSent: number;
packetsLost: number;
fractionLost: number;
audioLevel: number;
audioDetected: number;
framesCaptured: number;
framesRendered: number;
noiseCancellerProcessTime: number;
}
export interface OutboundCodec {
id: number;
name: string;
}
export interface Transport {
availableOutgoingBitrate: number;
ping: number;
decryptionFailures: number;
routingFailures: number;
localAddress: string;
pacerDelay: number;
receiverReports: any[];
receiverBitrateEstimate: number;
outboundBitrateEstimate: number;
inboundBitrateEstimate: number;
bytesSent: number;
}
export interface Codec {
type: string;
name: string;
priority: number;
payloadType: number;
rtxPayloadType?: number;
encode?: boolean;
decode?: boolean;
}
export interface Ids {
userId: string;
channelId: string;
guildId: string;
}
export interface VideoSinkWants {
any: number;
}
export interface VideoStreamParameter {
type: string;
active: boolean;
rid: string;
ssrc: number;
rtxSsrc: number;
quality: number;
maxBitrate: number;
maxFrameRate: number;
maxResolution: MaxResolution;
maxPixelCount: number;
}
export type MaxResolution = Partial<Resolution> & {
type: "fixed" | "source";
};
export interface LocalSpeakingFlags {
[key: string]: number;
}
export interface LocalVolumes {
[key: string]: number;
}
export interface LocalMutes {
[key: string]: any;
}
export const HdrCaptureMode = {
NEVER: "never",
ALWAYS: "always",
PERMITTED_DEVICES_ONLY: "permittedDevicesOnly",
} as const;
export type HdrCaptureMode = typeof HdrCaptureMode;
export interface DesktopSourceOptions extends Partial<Resolution> {
fps?: number;
useVideoHook?: boolean;
useGraphicsCapture?: boolean;
useQuartzCapturer?: boolean;
allowScreenCaptureKit?: boolean;
hdrCaptureMode?: HdrCaptureMode[keyof HdrCaptureMode];
}

View file

@ -0,0 +1,36 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Connection, VideoQualityManager } from "./";
export type FramerateReducer = FramerateReducer_ & {
connection: Connection;
sinkWants: VideoQualityManager;
framerateReductionTimeout?: number;
handleSelfMute: (...args: any[]) => any;
handleSpeaking: (...args: any[]) => any;
__proto__: FramerateReducer_;
};
export interface FramerateReducer_ {
destroy: (...args: any[]) => any;
destroyFramerateScaleFactorTimers: (...args: any[]) => any;
initialize: (...args: any[]) => any;
updateRemoteWantsFramerate: (...args: any[]) => any;
userSpeakingChange: (...args: any[]) => any;
}

View file

@ -0,0 +1,24 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./conn";
export * from "./connection";
export * from "./framerateReducer";
export * from "./mediaEngine";
export * from "./utils";
export * from "./videoQualityManager";

View file

@ -0,0 +1,148 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import TypedEmitter from "typed-emitter";
import { Connection } from "./";
export const MediaEngineEvent = {
REMOVE_LISTENER: "removeListener",
NEW_LISTENER: "newListener",
DESTORY: "destroy",
CONNECTION: "connection",
DEVICE_CHANGE: "devicechange",
VOLUME_CHANGE: "volumechange",
DESKTOP_SOURCE_END: "desktopsourceend",
AUDIO_PERMISSION: "audio-permission",
VIDEO_PERMISSION: "video-permission",
WATCHDOG_TIMEOUT: "watchdogtimeout",
VIDEO_INPUT_INITIALIZED: "video-input-initialized",
CONNECTION_STATS: "connection-stats",
} as const;
export type MediaEngineEvents = {
[MediaEngineEvent.REMOVE_LISTENER]: (...args: any[]) => any;
[MediaEngineEvent.NEW_LISTENER]: (...args: any[]) => any;
[MediaEngineEvent.DESTORY]: (...args: any[]) => any;
[MediaEngineEvent.CONNECTION]: (connection: Connection) => void;
[MediaEngineEvent.DEVICE_CHANGE]: (...args: any[]) => any;
[MediaEngineEvent.VOLUME_CHANGE]: (...args: any[]) => any;
[MediaEngineEvent.DESKTOP_SOURCE_END]: (...args: any[]) => any;
[MediaEngineEvent.AUDIO_PERMISSION]: (...args: any[]) => any;
[MediaEngineEvent.VIDEO_PERMISSION]: (...args: any[]) => any;
[MediaEngineEvent.WATCHDOG_TIMEOUT]: (...args: any[]) => any;
[MediaEngineEvent.VIDEO_INPUT_INITIALIZED]: (...args: any[]) => any;
[MediaEngineEvent.CONNECTION_STATS]: (...args: any[]) => any;
[MediaEngineEvent.REMOVE_LISTENER]: (...args: any[]) => any;
};
export type MediaEngine = TypedEmitter<MediaEngineEvents> &
MediaEngine_ & {
Video: (...args: any[]) => any;
Camera: (...args: any[]) => any;
handleDeviceChange: (...args: any[]) => any;
handleVolumeChange: (...args: any[]) => any;
handleVoiceActivity: (...args: any[]) => any;
handleActiveSinksChange: (...args: any[]) => any;
handleNewListener: (...args: any[]) => any;
handleRemoveListener: (...args: any[]) => any;
handleVideoInputInitialization: (...args: any[]) => any;
emitter: TypedEmitter<MediaEngineEvents>;
videoInputDeviceId: string;
lastVoiceActivity: number;
audioSubsystem: string;
audioLayer: string;
loopback: boolean;
deviceChangeGeneration: number;
consecutiveWatchdogFailures: number;
codecSurvey?: any;
connections: Set<Connection>;
__proto__: MediaEngine_;
};
export interface MediaEngine_ {
destroy: (...args: any[]) => any;
interact: (...args: any[]) => any;
supported: (...args: any[]) => any;
supports: (...args: any[]) => any;
connect: (...args: any[]) => any;
shouldConnectionBroadcastVideo: (...args: any[]) => any;
eachConnection: (callback: (connection: Connection) => void) => void;
enable: (...args: any[]) => any;
setInputVolume: (...args: any[]) => any;
setOutputVolume: (...args: any[]) => any;
getAudioInputDevices: (...args: any[]) => any;
setAudioInputDevice: (...args: any[]) => any;
getAudioOutputDevices: (...args: any[]) => any;
setAudioOutputDevice: (...args: any[]) => any;
getVideoInputDevices: (...args: any[]) => any;
setVideoInputDevice: (...args: any[]) => any;
getSupportedVideoCodecs: (...args: any[]) => any;
getCodecCapabilities: (callback: (codecs: string) => void) => void;
setDesktopSource: (...args: any[]) => any;
setSoundshareSource: (...args: any[]) => any;
getDesktopSource: (...args: any[]) => any;
getDesktopSources: (...args: any[]) => any;
getScreenPreviews: (...args: any[]) => any;
setClipBufferLength: (...args: any[]) => any;
saveClip: (...args: any[]) => any;
updateClipMetadata: (...args: any[]) => any;
exportClip: (...args: any[]) => any;
getWindowPreviews: (
width: number,
height: number
) => Promise<WindowPreview[]>;
setAudioSubsystem: (...args: any[]) => any;
getAudioSubsystem: (...args: any[]) => any;
getAudioLayer: (...args: any[]) => any;
getDebugLogging: (...args: any[]) => any;
setDebugLogging: (...args: any[]) => any;
setExperimentalAdm: (...args: any[]) => any;
setLoopback: (...args: any[]) => any;
getLoopback: (...args: any[]) => any;
setH264Enabled: (...args: any[]) => any;
setAv1Enabled: (...args: any[]) => any;
getCodecSurvey: () => Promise<string>;
writeAudioDebugState: (...args: any[]) => any;
startAecDump: (...args: any[]) => any;
stopAecDump: (...args: any[]) => any;
setAecDump: (...args: any[]) => any;
rankRtcRegions: (...args: any[]) => any;
getSoundshareStatus: (...args: any[]) => any;
enableSoundshare: (...args: any[]) => any;
createReplayConnection: (...args: any[]) => any;
setUseDirectVideo: (...args: any[]) => any;
setMaxSyncDelayOverride: (...args: any[]) => any;
applyMediaFilterSettings: (...args: any[]) => any;
startLocalAudioRecording: (...args: any[]) => any;
stopLocalAudioRecording: (...args: any[]) => any;
watchdogTick: (...args: any[]) => any;
__proto__: TypedEmitter<MediaEngineEvents>;
}
export interface CodecCapabilities {
codec: string;
decode: boolean;
encode: boolean;
}
export interface WindowPreview {
id: string;
url: string;
name: string;
}

View file

@ -0,0 +1,103 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface Utils {
requireModule: (...args: any[]) => any;
ensureModule: (...args: any[]) => any;
getCrashReporterMetadata: (...args: any[]) => any;
getSetting: (...args: any[]) => any;
beforeUnload: (...args: any[]) => any;
inputEventRegister: (...args: any[]) => any;
inputEventUnregister: (...args: any[]) => any;
setOnInputEventCallback: (...args: any[]) => any;
setFocused: (...args: any[]) => any;
getIdleMilliseconds: (...args: any[]) => any;
setObservedGamesCallback: (...args: any[]) => any;
setCandidateGamesCallback: (...args: any[]) => any;
clearCandidateGamesCallback: (...args: any[]) => any;
setGameCandidateOverrides: (...args: any[]) => any;
detectPid: (...args: any[]) => any;
undetectPid: (...args: any[]) => any;
shouldDisplayNotifications: (...args: any[]) => any;
getVoiceEngine: (...args: any[]) => any;
getDiscordUtils: (...args: any[]) => any;
isSystemDarkMode: (...args: any[]) => any;
getGameUtils: (...args: any[]) => any;
getCloudSync: (...args: any[]) => any;
getDispatch: (...args: any[]) => any;
setBadge: (...args: any[]) => any;
setSystemTrayIcon: (...args: any[]) => any;
setThumbarButtons: (...args: any[]) => any;
bounceDock: (...args: any[]) => any;
setSystemTrayApplications: (...args: any[]) => any;
architecture: (...args: any[]) => any;
moduleVersions: (...args: any[]) => any;
copy: (...args: any[]) => any;
copyImage: (...args: any[]) => any;
saveImage: (...args: any[]) => any;
saveFile: (...args: any[]) => any;
canCopyImage: (...args: any[]) => any;
cut: (...args: any[]) => any;
paste: (...args: any[]) => any;
readClipboard: (...args: any[]) => any;
on: (...args: any[]) => any;
invoke: (...args: any[]) => any;
send: (...args: any[]) => any;
flashFrame: (...args: any[]) => any;
minimize: (...args: any[]) => any;
restore: (...args: any[]) => any;
maximize: (...args: any[]) => any;
focus: (...args: any[]) => any;
blur: (...args: any[]) => any;
fullscreen: (...args: any[]) => any;
close: (...args: any[]) => any;
setAlwaysOnTop: (...args: any[]) => any;
isAlwaysOnTop: (...args: any[]) => any;
purgeMemory: (...args: any[]) => any;
updateCrashReporter: (...args: any[]) => any;
flushDNSCache: (...args: any[]) => any;
supportsFeature: (...args: any[]) => any;
getEnableHardwareAcceleration: (...args: any[]) => any;
setEnableHardwareAcceleration: (...args: any[]) => any;
getGPUDriverVersions: (...args: any[]) => any;
setZoomFactor: (...args: any[]) => any;
setBackgroundThrottling: (...args: any[]) => any;
getPidFromDesktopSource: (...args: any[]) => any;
getDesktopSourceFromPid: (...args: any[]) => any;
generateSessionFromPid: (...args: any[]) => any;
getAudioPid: (...args: any[]) => any;
setForegroundProcess: (...args: any[]) => any;
getDiscordMemoryUsage: (...args: any[]) => any;
showOpenDialog: (...args: any[]) => any;
flushStorageData: (...args: any[]) => any;
flushCookies: (...args: any[]) => any;
setCrashInformation: (...args: any[]) => any;
blockDisplaySleep: (...args: any[]) => any;
unblockDisplaySleep: (...args: any[]) => any;
cleanupDisplaySleep: (...args: any[]) => any;
relaunch: (...args: any[]) => any;
makeChunkedRequest: (...args: any[]) => any;
submitLiveCrashReport: (...args: any[]) => any;
crash: (...args: any[]) => any;
setApplicationBackgroundColor: (...args: any[]) => any;
asyncify: (...args: any[]) => any;
releaseChannel: string,
canBootstrapNewUpdater: boolean;
buildNumber: number;
version: number[];
}

View file

@ -0,0 +1,81 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Bitrate, Framerate, Resolution } from "../../";
import { Connection } from "./";
export type VideoQualityManager = VideoQualityManager_ & {
connection: Connection;
contextType: string;
isMuted: boolean;
isStreamContext: boolean;
ladder: Ladder;
options: Options;
qualityOverwrite: QualityOverwrite;
__proto__: VideoQualityManager_;
};
export interface VideoQualityManager_ {
applyQualityConstraints: (...args: any[]) => any;
getDesktopQuality: (...args: any[]) => any;
getQuality: (...args: any[]) => any;
getVideoQuality: (...args: any[]) => any;
setQuality: (...args: any[]) => any;
}
export interface QualityOverwrite {
bitrateMax?: number;
bitrateMin?: number;
bitrateTarget?: number;
capture?: Resolution & Framerate;
encode?: Resolution & Framerate;
}
export interface Options {
videoBudget: VideoBudget;
videoCapture: VideoBudget;
videoBitrate: VideoBitrate;
desktopBitrate: DesktopBitrate;
videoBitrateFloor: number;
}
export type DesktopBitrate = Bitrate;
export type VideoBitrate = Omit<Bitrate, "target">;
export type VideoBudget = Resolution & Framerate;
export interface Ladder {
pixelBudget: number;
ladder: {
[key: number]: LadderValue;
};
orderedLadder: OrderedLadder[];
}
export interface OrderedLadder extends Resolution, Framerate {
pixelCount: number;
wantValue: number;
budgetPortion: number;
mutedFramerate: Framerate;
}
export interface LadderValue extends Resolution, Framerate {
budgetPortion: number;
mutedFramerate: Framerate;
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./mediaEngineStore";

View file

@ -0,0 +1,119 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { types } from "../../..";
export type MediaEngineStore = MediaEngineStore__ &
MediaEngineStore_ & {
__proto__: MediaEngineStore_;
};
export interface MediaEngineStore_ {
initialize: (...args: any[]) => any;
supports: (...args: any[]) => any;
supportsInApp: (...args: any[]) => any;
isSupported: (...args: any[]) => any;
isExperimentalEncodersSupported: (...args: any[]) => any;
isNoiseSuppressionSupported: (...args: any[]) => any;
isNoiseCancellationSupported: (...args: any[]) => any;
isNoiseCancellationError: (...args: any[]) => any;
isAutomaticGainControlSupported: (...args: any[]) => any;
isAdvancedVoiceActivitySupported: (...args: any[]) => any;
isAecDumpSupported: (...args: any[]) => any;
isSimulcastSupported: (...args: any[]) => any;
getAecDump: (...args: any[]) => any;
getMediaEngine: () => types.MediaEngine;
getVideoComponent: (...args: any[]) => any;
getCameraComponent: (...args: any[]) => any;
isEnabled: (...args: any[]) => any;
isMute: (...args: any[]) => any;
isDeaf: (...args: any[]) => any;
hasContext: (...args: any[]) => any;
isSelfMutedTemporarily: (...args: any[]) => any;
isSelfMute: (...args: any[]) => any;
isHardwareMute: (...args: any[]) => any;
isSelfDeaf: (...args: any[]) => any;
isVideoEnabled: (...args: any[]) => any;
isVideoAvailable: (...args: any[]) => any;
isScreenSharing: (...args: any[]) => any;
isSoundSharing: (...args: any[]) => any;
isLocalMute: (...args: any[]) => any;
supportsDisableLocalVideo: (...args: any[]) => any;
isLocalVideoDisabled: (...args: any[]) => any;
isLocalVideoAutoDisabled: (...args: any[]) => any;
isMediaFilterSettingLoading: (...args: any[]) => any;
isNativeAudioPermissionReady: (...args: any[]) => any;
getDesktopSource: (...args: any[]) => any;
getDesktopSourceContext: (...args: any[]) => any;
getLocalPan: (...args: any[]) => any;
getLocalVolume: (...args: any[]) => any;
getInputVolume: (...args: any[]) => any;
getOutputVolume: (...args: any[]) => any;
getMode: (...args: any[]) => any;
getModeOptions: (...args: any[]) => any;
getShortcuts: (...args: any[]) => any;
getInputDeviceId: (...args: any[]) => any;
getOutputDeviceId: (...args: any[]) => any;
getVideoDeviceId: (...args: any[]) => any;
getInputDevices: (...args: any[]) => any;
getOutputDevices: (...args: any[]) => any;
getVideoDevices: (...args: any[]) => any;
getEchoCancellation: (...args: any[]) => any;
getLoopback: (...args: any[]) => any;
getNoiseSuppression: (...args: any[]) => any;
getAutomaticGainControl: (...args: any[]) => any;
getNoiseCancellation: (...args: any[]) => any;
getExperimentalEncoders: (...args: any[]) => any;
getHardwareH264: (...args: any[]) => any;
getEnableSilenceWarning: (...args: any[]) => any;
getDebugLogging: (...args: any[]) => any;
getQoS: (...args: any[]) => any;
getAttenuation: (...args: any[]) => any;
getAttenuateWhileSpeakingSelf: (...args: any[]) => any;
getAttenuateWhileSpeakingOthers: (...args: any[]) => any;
getAudioSubsystem: (...args: any[]) => any;
getSettings: (...args: any[]) => any;
getState: (...args: any[]) => any;
getInputDetected: (...args: any[]) => any;
getNoInputDetectedNotice: (...args: any[]) => any;
getPacketDelay: (...args: any[]) => any;
setCanHavePriority: (...args: any[]) => any;
isInteractionRequired: (...args: any[]) => any;
getVideoHook: (...args: any[]) => any;
getExperimentalSoundshare: (...args: any[]) => any;
supportsExperimentalSoundshare: (...args: any[]) => any;
getOpenH264: (...args: any[]) => any;
getAv1Enabled: (...args: any[]) => any;
getEverSpeakingWhileMuted: (...args: any[]) => any;
getSoundshareEnabled: (...args: any[]) => any;
supportsEnableSoundshare: (...args: any[]) => any;
getVideoStreamParameters: (...args: any[]) => any;
__proto__: MediaEngineStore__;
}
export interface MediaEngineStore__ {
registerActionHandlers: (...args: any[]) => any;
getName: (...args: any[]) => any;
initializeIfNeeded: (...args: any[]) => any;
initialize: (...args: any[]) => any;
syncWith: (...args: any[]) => any;
waitFor: (...args: any[]) => any;
emitChange: (...args: any[]) => any;
getDispatchToken: (...args: any[]) => any;
mustEmitChanges: (...args: any[]) => any;
}

Some files were not shown because too many files have changed in this diff Show more