Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
961b9b2cad
10 changed files with 593 additions and 62 deletions
|
|
@ -11,7 +11,6 @@
|
|||
html,
|
||||
body,
|
||||
#container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
|
@ -19,6 +18,29 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#title-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (display-mode: window-controls-overlay) {
|
||||
#title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 14px;
|
||||
font-family: Consolas, Courier New, monospace;
|
||||
line-height: 1;
|
||||
height: env(titlebar-area-height, 0px);
|
||||
app-region: drag;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
|
|
@ -62,6 +84,7 @@
|
|||
style="background: #1e1e1e; color: #ffffff; border: 1px solid transparent; padding: 5px 10px; border-radius: 4px; transition: background 0.3s, border 0.3s;">Redo</button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="title-bar">Equicord QuickCSS Editor</div>
|
||||
<div id="container"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/loader.js"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { isTruthy } from "@utils/guards";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { PluginTarget } from "@utils/pluginTargets";
|
||||
import { useAwaiter, useCleanupEffect, useIntersection } from "@utils/react";
|
||||
import { PluginTag, PluginTags } from "@utils/types";
|
||||
import { Alerts, ConfirmModal, lodash, openModal, Parser, React, SearchableSelect, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useRef, useState } from "@webpack/common";
|
||||
|
|
@ -109,13 +110,14 @@ const enum SearchStatus {
|
|||
API_PLUGINS
|
||||
}
|
||||
|
||||
export const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "equibop" | "desktop" | "dev", string> = {
|
||||
export const ExcludedReasons: Record<PluginTarget, string> = {
|
||||
desktop: "Discord Desktop app or Vesktop/Equibop",
|
||||
discordDesktop: "Discord Desktop app",
|
||||
vesktop: "Vesktop/Equibop apps",
|
||||
equibop: "Vesktop/Equibop apps",
|
||||
web: "Vesktop/Equibop apps & Discord web",
|
||||
dev: "Developer version of Equicord"
|
||||
dev: "Developer version of Equicord",
|
||||
browser: "Web Browser version of Equicord"
|
||||
};
|
||||
|
||||
function ExcludedPluginsList({ search }: { search: string; }) {
|
||||
|
|
|
|||
24
src/globals.d.ts
vendored
24
src/globals.d.ts
vendored
|
|
@ -66,6 +66,30 @@ declare global {
|
|||
export var unsafeWindow: any;
|
||||
|
||||
interface Window extends Record<PropertyKey, any> { }
|
||||
|
||||
interface MediaTrackConstraintSet {
|
||||
restrictOwnAudio?: boolean;
|
||||
suppressLocalAudioPlayback?: boolean;
|
||||
}
|
||||
|
||||
|
||||
type CaptureController = {
|
||||
decreaseZoomLevel(): Promise<undefined>;
|
||||
forwardWheel(): Promise<undefined>;
|
||||
getSupportedZoomLevels(): number[];
|
||||
increaseZoomLevel(): Promise<undefined>;
|
||||
resetZoomLevel(): Promise<undefined>;
|
||||
setFocusBehavior(): undefined;
|
||||
};
|
||||
|
||||
interface DisplayMediaStreamOptions {
|
||||
surfaceSwitching?: "include" | "exclude";
|
||||
selfBrowserSurface?: "include" | "exclude";
|
||||
systemAudio?: "include" | "exclude";
|
||||
monitorTypeSurfaces?: "include" | "exclude";
|
||||
controller?: CaptureController;
|
||||
preferCurrentTab?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
|
|
|
|||
4
src/modules.d.ts
vendored
4
src/modules.d.ts
vendored
|
|
@ -17,13 +17,15 @@
|
|||
*/
|
||||
|
||||
declare module "~plugins" {
|
||||
import { PluginTarget } from "@utils/pluginTargets";
|
||||
const plugins: Record<string, import("./utils/types").Plugin>;
|
||||
|
||||
export default plugins;
|
||||
export const PluginMeta: Record<string, {
|
||||
folderName: string;
|
||||
userPlugin: boolean;
|
||||
}>;
|
||||
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vesktop" | "equibop" | "desktop" | "dev">;
|
||||
export const ExcludedPlugins: Record<string, PluginTarget>;
|
||||
}
|
||||
|
||||
declare module "~git-hash" {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@
|
|||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.vc-member-list-decorators-display-names {
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal
|
||||
[class*="withDisplayNameStyles"] [class^="name"] {
|
||||
flex: initial;
|
||||
}
|
||||
|
|
@ -34,81 +34,107 @@ function fallbackToYoutubeThumbnail(originUrl: string | undefined): string | und
|
|||
return match ? `https://i.ytimg.com/vi/${match[5]}/maxresdefault.jpg` : undefined;
|
||||
}
|
||||
|
||||
async function fetchCoverArt(releaseGroupMBID: string, originUrl?: string): Promise<string | undefined> {
|
||||
if (!releaseGroupMBID) return fallbackToYoutubeThumbnail(originUrl);
|
||||
|
||||
if (coverArtCache.has(releaseGroupMBID)) {
|
||||
return coverArtCache.get(releaseGroupMBID);
|
||||
async function fetchCoverArtArchive(id: string, path: string) {
|
||||
if (coverArtCache.has(id)) {
|
||||
return coverArtCache.get(id);
|
||||
}
|
||||
|
||||
const res = await fetch(`https://coverartarchive.org/release-group/${releaseGroupMBID}`);
|
||||
if (!res.ok) return fallbackToYoutubeThumbnail(originUrl);
|
||||
|
||||
const url = await res.json()
|
||||
.then(json => json.images[0].thumbnails.large ?? fallbackToYoutubeThumbnail(originUrl));
|
||||
coverArtCache.set(releaseGroupMBID, url);
|
||||
const url = await fetch(`https://coverartarchive.org${path}`)
|
||||
.then(res =>
|
||||
res.ok
|
||||
? res.json()
|
||||
: Promise.reject(new Error(`${res.status} ${res.statusText}`))
|
||||
)
|
||||
.then(json => json.images[0].thumbnails.large);
|
||||
|
||||
coverArtCache.set(id, url);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function getUrls(additionalInfo: Record<string, string> | undefined, trackName: string, artistName: string, releaseName: string): Promise<Partial<TrackData>> {
|
||||
async function fetchCoverArt(releaseMBID: string, releaseGroupMBID: string, originUrl?: string): Promise<string | undefined> {
|
||||
return (
|
||||
(releaseMBID && await fetchCoverArtArchive(releaseMBID, `/release/${releaseMBID}`).catch(() => null)) ||
|
||||
(releaseGroupMBID && await fetchCoverArtArchive(releaseGroupMBID, `/release-group/${releaseGroupMBID}`).catch(() => null)) ||
|
||||
fallbackToYoutubeThumbnail(originUrl)
|
||||
);
|
||||
}
|
||||
|
||||
async function tryLookup(query: string): Promise<Record<string, any> | undefined> {
|
||||
const params = new URLSearchParams({
|
||||
fmt: "json",
|
||||
limit: "1"
|
||||
});
|
||||
return await fetch("https://musicbrainz.org/ws/2/recording/?" + params + "&query=" + query, {
|
||||
headers: { "User-Agent": VENCORD_USER_AGENT }
|
||||
})
|
||||
.then(res => res.ok ? res.json() : Promise.reject(new Error(`${res.status} ${res.statusText}`)))
|
||||
.then(json => json.recordings?.[0]);
|
||||
}
|
||||
|
||||
async function getUrls(
|
||||
additionalInfo: Record<string, string> | undefined,
|
||||
trackName: string,
|
||||
artistName: string,
|
||||
releaseName: string
|
||||
): Promise<Partial<TrackData>> {
|
||||
// Well tagged music will have MBIDs which we can use directly. These are optional but highly recommended in ListenBrainz scrobbles.
|
||||
// If your music doesn't have these, it's highly recommended to use https://picard.musicbrainz.org/ to automatically add them
|
||||
if (additionalInfo?.recording_mbid) {
|
||||
const { release_group_mbid, release_mbid, recording_mbid, artist_mbids, origin_url } = additionalInfo;
|
||||
|
||||
return {
|
||||
imageURL: await fetchCoverArt(release_group_mbid, origin_url),
|
||||
trackURL: recording_mbid ? url(`/track/${recording_mbid}`) : undefined,
|
||||
albumURL: release_group_mbid
|
||||
? url(`/release-group/${release_group_mbid}`)
|
||||
: release_mbid
|
||||
imageURL: await fetchCoverArt(release_mbid, release_group_mbid, origin_url),
|
||||
trackURL: url(`/track/${recording_mbid}`),
|
||||
albumURL: release_mbid
|
||||
? url(`/release/${release_mbid}`)
|
||||
: release_group_mbid
|
||||
? url(`/release-group/${release_group_mbid}`)
|
||||
: undefined,
|
||||
artistURL: artist_mbids?.length ? url(`/artist/${artist_mbids[0]}`) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// If no MBIDs are present, try to search for the track on MusicBrainz
|
||||
// If no MBIDs are present, try searching MusicBrainz: first by ISRC (if present), then by name.
|
||||
const nameQuery = encodeURIComponent(
|
||||
`artist:"${artistName}" AND recording:"${trackName}"${releaseName ? ` AND release:"${releaseName}"` : ""}`
|
||||
).replace(/[!()*\-~]/g, "\\$&");
|
||||
|
||||
let rawQuery = `artist:"${artistName}" AND recording:"${trackName}"`;
|
||||
if (releaseName)
|
||||
rawQuery += ` AND album:"${releaseName}"`;
|
||||
const query = encodeURIComponent(rawQuery);
|
||||
const queries = additionalInfo?.isrc
|
||||
? [encodeURIComponent(`isrc:${additionalInfo.isrc}`), nameQuery]
|
||||
: [nameQuery];
|
||||
|
||||
for (const query of queries) {
|
||||
if (metadataCache.has(query)) {
|
||||
return metadataCache.get(query) ?? {};
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
fmt: "json",
|
||||
limit: "1"
|
||||
});
|
||||
const metadata = await tryLookup(query);
|
||||
if (!metadata) continue;
|
||||
|
||||
const metadata = await fetch("https://musicbrainz.org/ws/2/recording/?" + params + "&query=" + query, {
|
||||
headers: { "User-Agent": VENCORD_USER_AGENT }
|
||||
})
|
||||
.then(res => res.ok ? res.json() : Promise.reject(new Error(`${res.status} ${res.statusText}`)))
|
||||
.then(json => json.recordings?.[0]);
|
||||
const artist = metadata["artist-credit"]?.[0]?.artist;
|
||||
const release = metadata.releases?.find((release: { title: string; }) => release.title === releaseName) || metadata.releases?.[0];
|
||||
|
||||
if (!metadata) {
|
||||
const data = additionalInfo?.origin_url ? { imageURL: fallbackToYoutubeThumbnail(additionalInfo.origin_url) } : {};
|
||||
const data: Partial<TrackData> = {
|
||||
imageURL: await fetchCoverArt(release?.id, release?.["release-group"]?.id, additionalInfo?.origin_url),
|
||||
trackURL: url(`/track/${metadata.id}/`),
|
||||
albumURL: release?.id
|
||||
? url(`/release/${release.id}/`)
|
||||
: release?.["release-group"]?.id
|
||||
? url(`/release-group/${release["release-group"].id}/`)
|
||||
: undefined,
|
||||
artistURL: artist?.id ? url(`/artist/${artist.id}/`) : undefined,
|
||||
album: additionalInfo?.release_name ?? release?.title ?? "Unknown",
|
||||
};
|
||||
metadataCache.set(query, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
const artist = metadata["artist-credit"]?.[0]?.artist;
|
||||
const release = metadata.releases?.[0];
|
||||
const fallback: Partial<TrackData> = additionalInfo?.origin_url
|
||||
? { imageURL: fallbackToYoutubeThumbnail(additionalInfo.origin_url) }
|
||||
: {};
|
||||
metadataCache.set(queries.at(-1)!, fallback);
|
||||
|
||||
const data: Partial<TrackData> = {
|
||||
imageURL: await fetchCoverArt(release?.["release-group"]?.id, additionalInfo?.origin_url),
|
||||
trackURL: url(`/track/${metadata.id}/`),
|
||||
albumURL: release?.id ? url(`/release/${release.id}/`) : release?.["release-group"]?.id ? url(`/release-group/${release["release-group"].id}/`) : undefined,
|
||||
artistURL: artist?.id ? url(`/artist/${artist.id}/`) : undefined,
|
||||
};
|
||||
metadataCache.set(query, data);
|
||||
|
||||
return data;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export const ListenBrainzScrobbler: ScrobblerBackend = {
|
||||
|
|
|
|||
|
|
@ -3,21 +3,36 @@
|
|||
color-scheme: only light;
|
||||
}
|
||||
|
||||
@media all and not (display-mode: browser) {
|
||||
@media all and not ((display-mode: browser) or (display-mode: fullscreen)) {
|
||||
:root {
|
||||
/* force title bar to be same as WCO bar height, regardless of zoom, device pixel ratio or OS, is only set when WCO is defined, otherwise sets to 0 */
|
||||
--custom-app-top-bar-height: env(titlebar-area-height, 0px) !important;
|
||||
}
|
||||
|
||||
/* position trailing titlebar elements alongside WCO */
|
||||
[class*="bar_"] [class*="trailing_"] {
|
||||
[data-fullscreen] > [class*="bar_"] [class*="trailing_"] {
|
||||
padding-right: calc(100vw - env(titlebar-area-width, 100%) - env(titlebar-area-x, 0px));
|
||||
}
|
||||
|
||||
/* popout windows like screenshare popout, these properties don't work outside WCO so this is safe */
|
||||
[data-popout-root="true"] {
|
||||
[class*="headerWrapper_"] {
|
||||
app-region: drag;
|
||||
|
||||
/* position trailing toolbar elements alongside WCO */
|
||||
[class*="toolbar_"] {
|
||||
padding-right: calc(100vw - env(titlebar-area-width, 100%) - env(titlebar-area-x, 0px));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* force hide trailing icons when standalone so there's no ugly pixel overflow */
|
||||
@media all and (display-mode: standalone) {
|
||||
[class*="bar_"] [class*="trailing_"] {
|
||||
[data-fullscreen] > [class*="bar_"] {
|
||||
[class*="trailing_"],
|
||||
[class*="leading"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/plugins/webScreenShare.browser/index.tsx
Normal file
320
src/plugins/webScreenShare.browser/index.tsx
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* 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 { getUserSettingLazy } from "@api/UserSettings";
|
||||
import {
|
||||
Divider,
|
||||
Span,
|
||||
} from "@components/index";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Checkbox, closeModal, Modal, openModal, Text, } from "@webpack/common";
|
||||
|
||||
import managedStyle from "./styles.css?managed";
|
||||
|
||||
const cl = classNameFactory("vc-screen-picker-");
|
||||
|
||||
class NotAllowedError extends Error {
|
||||
name = "NotAllowedError";
|
||||
}
|
||||
|
||||
const logger = new Logger("VencordScreenShare");
|
||||
|
||||
const getDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
|
||||
|
||||
function openScreenSharePicker(options: DisplayMediaStreamOptions) {
|
||||
return new Promise<MediaStream>((resolve, reject) => {
|
||||
const key = openModal(
|
||||
props => (
|
||||
<ModalComponent
|
||||
modalProps={props}
|
||||
submit={resolve}
|
||||
options={options}
|
||||
close={() => {
|
||||
props.onClose();
|
||||
reject(new NotAllowedError("Permission denied by user"));
|
||||
}}
|
||||
/>
|
||||
),
|
||||
{
|
||||
onCloseRequest() {
|
||||
closeModal(key);
|
||||
reject(new NotAllowedError("Permission denied by user"));
|
||||
},
|
||||
onCloseCallback() {
|
||||
reject(new NotAllowedError("Permission denied by user"));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function OptionRadio<Settings extends object, Key extends keyof Settings, Options extends ReadonlyArray<string>>(props: {
|
||||
options: Options;
|
||||
labels?: Array<string>;
|
||||
settings: Settings;
|
||||
settingsKey: Key;
|
||||
onChange: (option: Options[number]) => void;
|
||||
}) {
|
||||
const { options, settings, settingsKey, labels, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className={cl("padding")}>
|
||||
<div className={cl("option-radios")}>
|
||||
{options.map((option, idx) => (
|
||||
<label className={cl("option-radio")} data-checked={settings[settingsKey] === option} key={option}>
|
||||
<Span weight="bold">{labels?.[idx] ?? option}</Span>
|
||||
<input
|
||||
type="radio"
|
||||
name={settingsKey.toString()}
|
||||
value={option}
|
||||
checked={settings[settingsKey] === option}
|
||||
onChange={() => onChange(option)}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModalComponent({ modalProps, submit, close, options }: {
|
||||
modalProps: any;
|
||||
submit: (data: Promise<MediaStream>) => void;
|
||||
close: () => void;
|
||||
options: DisplayMediaStreamOptions;
|
||||
}) {
|
||||
const liveSettings = settings.use();
|
||||
const disableStreamPreviewsValue = disableStreamPreviews.useSetting();
|
||||
|
||||
async function stream() {
|
||||
try {
|
||||
const frameRate = Number(liveSettings.frameRate);
|
||||
const height = Number(liveSettings.resolution);
|
||||
|
||||
// const conn = [...MediaEngineStore.getMediaEngine().connections].find(
|
||||
// connection => connection.userId === UserStore.getCurrentUser().id
|
||||
// );
|
||||
|
||||
const videoOptions = typeof options?.video !== "boolean" && !!options.video ? options.video : {};
|
||||
const audioOptions = typeof options?.audio !== "boolean" && !!options.audio ? options.audio : {};
|
||||
// allow browser to constrain itself
|
||||
delete videoOptions.width;
|
||||
|
||||
submit(
|
||||
getDisplayMedia({
|
||||
video: {
|
||||
...videoOptions,
|
||||
frameRate,
|
||||
height,
|
||||
},
|
||||
audio: {
|
||||
...audioOptions,
|
||||
restrictOwnAudio: true,
|
||||
},
|
||||
surfaceSwitching: "include",
|
||||
systemAudio: liveSettings.systemAudio ? "exclude" : "include"
|
||||
}).then(t => {
|
||||
// w3c says this can fail, they don't say why, so just to be safe
|
||||
try {
|
||||
const video = t.getVideoTracks()?.[0];
|
||||
if (video) video.contentHint = liveSettings.contentHint;
|
||||
} catch { }
|
||||
return t;
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error while submitting stream.", error);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cl("modal")}>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
|
||||
size="lg"
|
||||
actionBarInput={
|
||||
<div className={cl("summary")}>
|
||||
<Text variant="text-md/semibold" color="text-strong" className={cl("source-or-preset-name")}>{liveSettings.contentHint === "motion" ? "Gaming" : liveSettings.contentHint === "detail" ? "Screenshare" : "Custom"}</Text>
|
||||
<Text variant="text-xs/medium" color="text-muted" className={cl("summary-detail")}>
|
||||
<span>{liveSettings.contentHint === "motion" ? "Smoother video" : liveSettings.contentHint === "detail" ? "Cleaner text" : "User preset"}</span>
|
||||
<span className={cl("ellipsis")}>•</span>
|
||||
<span>{liveSettings.resolution}p</span>
|
||||
<span className={cl("ellipsis")}>•</span>
|
||||
<span>{liveSettings.frameRate}fps</span>
|
||||
{liveSettings.systemAudio ? <span className={cl("ellipsis")}>•</span> : ""}
|
||||
{liveSettings.systemAudio ? <span>Stream Muted</span> : ""}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
variant: "primary",
|
||||
text: "Stream",
|
||||
onClick: stream
|
||||
}
|
||||
]}
|
||||
>
|
||||
|
||||
<div>
|
||||
<div className={cl("flex", "padding")}>
|
||||
<section className={cl("quality-section")}>
|
||||
<Text tag="h2" variant="heading-md/semibold" color="text-strong">Resolution</Text>
|
||||
<OptionRadio
|
||||
options={StreamResolution}
|
||||
settings={liveSettings}
|
||||
settingsKey="resolution"
|
||||
onChange={value => (liveSettings.resolution = value)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className={cl("quality-section")}>
|
||||
<Text tag="h2" variant="heading-md/semibold" color="text-strong">Frame Rate</Text>
|
||||
<OptionRadio
|
||||
options={StreamFps}
|
||||
settings={liveSettings}
|
||||
settingsKey="frameRate"
|
||||
onChange={value => (liveSettings.frameRate = value)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<div>
|
||||
<Text tag="h2" variant="heading-md/semibold" color="text-strong">Stream Mode</Text>
|
||||
<div>
|
||||
<OptionRadio
|
||||
options={StreamContentHint}
|
||||
labels={["Smoother video", "Cleaner text", "Custom"]}
|
||||
settings={liveSettings}
|
||||
settingsKey="contentHint"
|
||||
onChange={option => (liveSettings.contentHint = option)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={cl("padding", "pointer")}>
|
||||
<Checkbox
|
||||
value={!!liveSettings.systemAudio}
|
||||
onChange={(_e, value) => (liveSettings.systemAudio = value)}
|
||||
shape="box"
|
||||
reverse={true}>
|
||||
<div className={cl("control-content")}>
|
||||
<Text tag="h2" variant="heading-md/semibold" color="text-strong">Mute Stream Audio</Text>
|
||||
<Text variant="text-sm/normal" color="text-subtle">Prevents system audio from being included in your stream.</Text>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className={cl("padding", "pointer")}>
|
||||
<Checkbox
|
||||
value={!disableStreamPreviewsValue}
|
||||
onChange={(_e, value) => disableStreamPreviews.updateSetting(() => !value)}
|
||||
shape="box"
|
||||
reverse={true}>
|
||||
<div className={cl("control-content")}>
|
||||
<Text tag="h2" variant="heading-md/semibold" color="text-strong">Show Stream Previews</Text>
|
||||
<Text variant="text-sm/normal" color="text-subtle">Allows others to see a preview of your stream before they join.</Text>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// this is kinda shit and it really should be done via videoQualityManager
|
||||
const StreamResolution = ["480", "720", "1080", "1440", "2160"] as const;
|
||||
const StreamFps = ["15", "30", "60", "120"] as const;
|
||||
const StreamContentHint = ["motion", "detail", ""] as const;
|
||||
|
||||
const settings = definePluginSettings({
|
||||
resolution: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Resolution",
|
||||
hidden: true,
|
||||
options: StreamResolution.map(res => ({ label: res, value: res, default: res === "1080" }))
|
||||
},
|
||||
frameRate: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Frame Rate",
|
||||
hidden: true,
|
||||
options: StreamFps.map(fps => ({ label: fps, value: fps, default: fps === "60" }))
|
||||
},
|
||||
contentHint: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Content Hint",
|
||||
hidden: true,
|
||||
options: StreamContentHint.map(hint => ({ label: hint, value: hint, default: hint === "motion" }))
|
||||
},
|
||||
systemAudio: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Mute system audio",
|
||||
hidden: true
|
||||
}
|
||||
});
|
||||
|
||||
const disableStreamPreviews = getUserSettingLazy<boolean>("voiceAndVideo", "disableStreamPreviews")!;
|
||||
|
||||
export default definePlugin({
|
||||
name: "WebScreenShare",
|
||||
authors: [Devs.ThaUnknown],
|
||||
description: "Adds a screenshare options menu. Allows for changing resolution, framerate, encoding hints, and system audio settings.",
|
||||
tags: ["Voice", "Utility"],
|
||||
enabledByDefault: true,
|
||||
settings,
|
||||
managedStyle,
|
||||
|
||||
start() {
|
||||
navigator.mediaDevices.getDisplayMedia = openScreenSharePicker;
|
||||
},
|
||||
stop() {
|
||||
navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
|
||||
},
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "this.getDefaultGoliveQuality()",
|
||||
replacement: {
|
||||
match: /this\.getDefaultGoliveQuality\(\)/,
|
||||
replace: "$self.getGoliveMaxQuality($&)"
|
||||
}
|
||||
}
|
||||
],
|
||||
// this is a maximum, not a true default, this is later constrained properly in GDM anyways
|
||||
// and this way we don't pre-emptively neuter the max stream quality
|
||||
getGoliveMaxQuality(opts: any) {
|
||||
const framerate = 120;
|
||||
const height = 2160;
|
||||
const width = 3840;
|
||||
|
||||
Object.assign(opts, {
|
||||
bitrateMin: 500000,
|
||||
bitrateMax: 8000000,
|
||||
bitrateTarget: 600000
|
||||
});
|
||||
if (opts?.encode) {
|
||||
Object.assign(opts.encode, {
|
||||
framerate,
|
||||
width,
|
||||
height,
|
||||
pixelCount: height * width
|
||||
});
|
||||
}
|
||||
Object.assign(opts.capture, {
|
||||
framerate,
|
||||
width,
|
||||
height,
|
||||
pixelCount: height * width
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
});
|
||||
114
src/plugins/webScreenShare.browser/styles.css
Normal file
114
src/plugins/webScreenShare.browser/styles.css
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
.vc-screen-picker-modal {
|
||||
display: contents;
|
||||
|
||||
header,
|
||||
[class^="bodySpacerTop_"],
|
||||
[class^="bodySpacerBottom_"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-screen-picker-option-radios {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
|
||||
.vc-screen-picker-option-radio {
|
||||
text-align: center;
|
||||
padding: 0.3em;
|
||||
cursor: pointer;
|
||||
transition: 50ms ease-in;
|
||||
transition-property: background-color, color, border-color, opacity;
|
||||
background-color: var(--control-secondary-background-default);
|
||||
border-top: 1px solid var(--control-primary-border-default);
|
||||
border-bottom: 1px solid var(--control-primary-border-default);
|
||||
overflow: clip;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--control-secondary-background-hover);
|
||||
color: var(--control-secondary-text-hover);
|
||||
border-color: var(--control-primary-border-hover);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--radius-xs, 4px) 0 0 var(--radius-xs, 4px);
|
||||
border-left: 1px solid var(--control-primary-border-default);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 var(--radius-xs, 4px) var(--radius-xs, 4px) 0;
|
||||
border-right: 1px solid var(--control-primary-border-default);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-screen-picker-option-radio[data-checked="true"] {
|
||||
background-color: var(--control-primary-background-default);
|
||||
color: var(--control-primary-text-default);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--control-primary-background-hover);
|
||||
color: var(--control-primary-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-screen-picker-padding {
|
||||
padding: var(--space-sm, 12px) 0;
|
||||
}
|
||||
|
||||
.vc-screen-picker-flex {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vc-screen-picker-quality-section {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.vc-screen-picker-pointer label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-screen-picker-control-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: var(--space-4);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vc-screen-picker-summary {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
|
||||
.vc-screen-picker-source-or-preset-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.vc-screen-picker-summary-detail {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--space-8);
|
||||
|
||||
.vc-screen-picker-ellipsis {
|
||||
color: var(--background-mod-subtle);
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/utils/pluginTargets.ts
Normal file
8
src/utils/pluginTargets.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const PluginTargets = ["web", "browser", "discordDesktop", "vesktop", "desktop", "dev"] as const;
|
||||
export type PluginTarget = typeof PluginTargets[number];
|
||||
Loading…
Add table
Reference in a new issue