merge: dev

This commit is contained in:
Lewis Crichton 2024-01-19 17:00:00 +00:00
commit 269ed18fcc
No known key found for this signature in database
20 changed files with 464 additions and 216 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.6.6", "version": "1.6.7",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {

View file

@ -23,6 +23,7 @@ import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { CogWheel, DeleteIcon, PluginIcon } from "@components/Icons"; import { CogWheel, DeleteIcon, PluginIcon } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import PluginModal from "@components/PluginSettings/PluginModal";
import { AddonCard } from "@components/VencordSettings/AddonCard"; import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared"; import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
@ -366,6 +367,21 @@ function ThemesTab() {
> >
Edit QuickCSS Edit QuickCSS
</Button> </Button>
{Vencord.Settings.plugins.ClientTheme.enabled && (
<Button
onClick={() => openModal(modalProps => (
<PluginModal
{...modalProps}
plugin={Vencord.Plugins.plugins.ClientTheme}
onRestartNeeded={() => { }}
/>
))}
size={Button.Sizes.SMALL}
>
Edit ClientTheme
</Button>
)}
</> </>
</Card> </Card>

View file

@ -83,10 +83,10 @@ function VencordSettings() {
title: "Use Windows' native title bar instead of Discord's custom one", title: "Use Windows' native title bar instead of Discord's custom one",
note: "Requires a full restart" note: "Requires a full restart"
}), }),
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && { !IS_WEB && {
key: "transparent", key: "transparent",
title: "Enable window transparency", title: "Enable window transparency.",
note: "Requires a full restart" note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart"
}, },
!IS_WEB && isWindows && { !IS_WEB && isWindows && {
key: "winCtrlQ", key: "winCtrlQ",

View file

@ -79,8 +79,7 @@ if (!IS_VANILLA) {
delete options.frame; delete options.frame;
} }
// This causes electron to freeze / white screen for some people if (settings.transparent) {
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
options.transparent = true; options.transparent = true;
options.backgroundColor = "#00000000"; options.backgroundColor = "#00000000";
} }

View file

@ -1,94 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 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 } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const enum Methods {
Random,
Consistent,
Timestamp,
}
const tarExtMatcher = /\.tar\.\w+$/;
export default definePlugin({
name: "AnonymiseFileNames",
authors: [Devs.obscurity],
description: "Anonymise uploaded file names",
patches: [
{
find: "instantBatchUpload:function",
replacement: {
match: /uploadFiles:(.{1,2}),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
},
},
],
options: {
method: {
description: "Anonymising method",
type: OptionType.SELECT,
options: [
{ label: "Random Characters", value: Methods.Random, default: true },
{ label: "Consistent", value: Methods.Consistent },
{ label: "Timestamp (4chan-like)", value: Methods.Timestamp },
],
},
randomisedLength: {
description: "Random characters length",
type: OptionType.NUMBER,
default: 7,
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random,
},
consistent: {
description: "Consistent filename",
type: OptionType.STRING,
default: "image",
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent,
},
},
anonymise(file: string) {
let name = "image";
const tarMatch = tarExtMatcher.exec(file);
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
switch (Settings.plugins.AnonymiseFileNames.method) {
case Methods.Random:
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
name = Array.from(
{ length: Settings.plugins.AnonymiseFileNames.randomisedLength },
() => chars[Math.floor(Math.random() * chars.length)]
).join("");
break;
case Methods.Consistent:
name = Settings.plugins.AnonymiseFileNames.consistent;
break;
case Methods.Timestamp:
// UNIX timestamp in nanos, i could not find a better dependency-less way
name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`;
break;
}
return name + ext;
},
});

View file

@ -0,0 +1,130 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 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 { Upload } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
type AnonUpload = Upload & { anonymise?: boolean; };
const ActionBarIcon = findByCodeLazy(".actionBarIcon)");
const UploadDraft = findByPropsLazy("popFirstFile", "update");
const enum Methods {
Random,
Consistent,
Timestamp,
}
const tarExtMatcher = /\.tar\.\w+$/;
const settings = definePluginSettings({
anonymiseByDefault: {
description: "Whether to anonymise file names by default",
type: OptionType.BOOLEAN,
default: true,
},
method: {
description: "Anonymising method",
type: OptionType.SELECT,
options: [
{ label: "Random Characters", value: Methods.Random, default: true },
{ label: "Consistent", value: Methods.Consistent },
{ label: "Timestamp", value: Methods.Timestamp },
],
},
randomisedLength: {
description: "Random characters length",
type: OptionType.NUMBER,
default: 7,
disabled: () => settings.store.method !== Methods.Random,
},
consistent: {
description: "Consistent filename",
type: OptionType.STRING,
default: "image",
disabled: () => settings.store.method !== Methods.Consistent,
},
});
export default definePlugin({
name: "AnonymiseFileNames",
authors: [Devs.obscurity],
description: "Anonymise uploaded file names",
patches: [
{
find: "instantBatchUpload:function",
replacement: {
match: /uploadFiles:(.{1,2}),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
},
},
{
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
replacement: {
match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
},
},
],
settings,
renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => {
const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault;
return (
<ActionBarIcon
tooltip={anonymise ? "Using anonymous file name" : "Using normal file name"}
onClick={() => {
upload.anonymise = !anonymise;
UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders
}}
>
{anonymise
? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M17.06 13C15.2 13 13.64 14.33 13.24 16.1C12.29 15.69 11.42 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C19.23 21 21 19.21 21 17C21 14.79 19.23 13 17.06 13M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17S5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17S8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17S15.5 14.14 17.06 14.14C18.62 14.14 19.88 15.42 19.88 17S18.61 19.86 17.06 19.86M22 10.5H2V12H22V10.5M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z" /></svg>
: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{ transform: "scale(-1,1)" }}><path fill="currentColor" d="M22.11 21.46L2.39 1.73L1.11 3L6.31 8.2L6 9H7.11L8.61 10.5H2V12H10.11L13.5 15.37C13.38 15.61 13.3 15.85 13.24 16.1C12.29 15.69 11.41 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C17.66 21 18.22 20.86 18.72 20.61L20.84 22.73L22.11 21.46M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17C4.13 15.42 5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17C9.75 18.58 8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17C14.25 16.74 14.29 16.5 14.36 16.25L17.84 19.73C17.59 19.81 17.34 19.86 17.06 19.86M22 12H15.2L13.7 10.5H22V12M17.06 13C19.23 13 21 14.79 21 17C21 17.25 20.97 17.5 20.93 17.73L19.84 16.64C19.68 15.34 18.66 14.32 17.38 14.17L16.29 13.09C16.54 13.03 16.8 13 17.06 13M12.2 9L7.72 4.5L8.43 2.68C8.63 2.17 9.19 1.89 9.72 2.04L9.77 2.05L12 2.79L14.22 2.05C14.75 1.88 15.32 2.14 15.54 2.63L15.56 2.68L18 9H12.2Z" /></svg>
}
</ActionBarIcon>
);
}, { noop: true }),
anonymise(upload: AnonUpload) {
if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename;
const file = upload.filename;
const tarMatch = tarExtMatcher.exec(file);
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
switch (settings.store.method) {
case Methods.Random:
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length: settings.store.randomisedLength },
() => chars[Math.floor(Math.random() * chars.length)]
).join("") + ext;
case Methods.Consistent:
return settings.store.consistent + ext;
case Methods.Timestamp:
return Date.now() + ext;
}
},
});

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "BetterGifPicker",
description: "Makes the gif picker open the favourite category by default",
authors: [Devs.Samwich],
patches: [
{
find: ".GIFPickerResultTypes.SEARCH",
replacement: [{
match: "this.state={resultType:null}",
replace: 'this.state={resultType:"Favorites"}'
}]
}
]
});

View file

@ -153,5 +153,6 @@ export const defaultRules = [
"utm_term", "utm_term",
"si@open.spotify.com", "si@open.spotify.com",
"igshid", "igshid",
"igsh",
"share_id@reddit.com", "share_id@reddit.com",
]; ];

View file

@ -19,6 +19,16 @@
border: thin solid var(--background-modifier-accent) !important; border: thin solid var(--background-modifier-accent) !important;
} }
.client-theme-warning { .client-theme-warning * {
color: var(--text-danger); color: var(--text-danger);
} }
.client-theme-contrast-warning {
background-color: var(--background-primary);
padding: 0.5rem;
border-radius: .5rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}

View file

@ -8,19 +8,19 @@ import "./clientTheme.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getTheme, Theme } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms } from "@webpack/common"; import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const colorPresets = [ const colorPresets = [
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
"#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
"#3C2E42", "#422938" "#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d",
"#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb",
]; ];
function onPickColor(color: number) { function onPickColor(color: number) {
@ -30,9 +30,35 @@ function onPickColor(color: number) {
updateColorVars(hexColor); updateColorVars(hexColor);
} }
const { saveClientTheme } = findByPropsLazy("saveClientTheme");
function setTheme(theme: string) {
saveClientTheme({ theme });
}
const ThemeStore = findStoreLazy("ThemeStore");
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() { function ThemeSettings() {
const lightnessWarning = hexToLightness(settings.store.color) > 45; const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme);
const lightModeWarning = getTheme() === Theme.Light; const isLightTheme = theme === "light";
const oppositeTheme = isLightTheme ? "dark" : "light";
const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset);
const nitroThemeEnabled = nitroTheme !== undefined;
const selectedLuminance = relativeLuminance(settings.store.color);
let contrastWarning = false, fixableContrast = true;
if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12)
contrastWarning = true;
if (selectedLuminance < 0.26 && selectedLuminance > 0.12)
fixableContrast = false;
// light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels
if (isLightTheme && selectedLuminance > 0.65) {
contrastWarning = true;
fixableContrast = false;
}
return ( return (
<div className="client-theme-settings"> <div className="client-theme-settings">
@ -48,15 +74,18 @@ function ThemeSettings() {
suggestedColors={colorPresets} suggestedColors={colorPresets}
/> />
</div> </div>
{lightnessWarning || lightModeWarning {(contrastWarning || nitroThemeEnabled) && (<>
? <div> <Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} /> <div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
<Forms.FormText className="client-theme-warning">Your theme won't look good:</Forms.FormText> <div className="client-theme-warning">
{lightnessWarning && <Forms.FormText className="client-theme-warning">Selected color is very light</Forms.FormText>} <Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
{lightModeWarning && <Forms.FormText className="client-theme-warning">Light mode isn't supported</Forms.FormText>} {contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
</div>
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
</div> </div>
: null </>)}
}
</div> </div>
); );
} }
@ -87,9 +116,12 @@ export default definePlugin({
settings, settings,
startAt: StartAt.DOMContentLoaded, startAt: StartAt.DOMContentLoaded,
start() { async start() {
updateColorVars(settings.store.color); updateColorVars(settings.store.color);
generateColorOffsets();
const styles = await getStyles();
generateColorOffsets(styles);
generateLightModeFixes(styles);
}, },
stop() { stop() {
@ -98,56 +130,86 @@ export default definePlugin({
} }
}); });
const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g; const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g;
const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g;
async function generateColorOffsets() { // generates variables per theme by:
// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable)
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); // - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600)
const variableLightness = {} as Record<string, number>; function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp, centerVariable: string): string {
return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1)
// Search all stylesheets for color variables
for (const styleLinkNode of styleLinkNodes) {
const cssLink = styleLinkNode.getAttribute("href");
if (!cssLink) continue;
const res = await fetch(cssLink);
const cssString = await res.text();
// Get lightness values of --primary variables >=500
let variableMatch = variableRegex.exec(cssString);
while (variableMatch !== null) {
const [, variable, lightness] = variableMatch;
variableLightness[variable] = parseFloat(lightness);
variableMatch = variableRegex.exec(cssString);
}
}
// Generate offsets
const lightnessOffsets = Object.entries(variableLightness)
.map(([key, lightness]) => { .map(([key, lightness]) => {
const lightnessOffset = lightness - variableLightness["--primary-600-hsl"]; const lightnessOffset = lightness - variableLightness[centerVariable];
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
}) })
.join("\n"); .join("\n");
}
const style = document.createElement("style");
style.setAttribute("id", "clientThemeOffsets"); function generateColorOffsets(styles) {
style.textContent = `:root:root { const variableLightness = {} as Record<string, number>;
${lightnessOffsets}
}`; // Get lightness values of --primary variables
document.head.appendChild(style); let variableMatch = variableRegex.exec(styles);
while (variableMatch !== null) {
const [, variable, lightness] = variableMatch;
variableLightness[variable] = parseFloat(lightness);
variableMatch = variableRegex.exec(styles);
}
createStyleSheet("clientThemeOffsets", [
`.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
`.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
].join("\n\n"));
}
function generateLightModeFixes(styles) {
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
// get light capturing groups that mention --white-500
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();
const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m;
const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m;
// find all capturing groups that assign background or background-color directly to w500
const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n");
const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n");
// create css to reassign them to --primary-100
const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`;
const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`;
const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m;
const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m;
// get all global variables used for backgrounds
const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500
.map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[]
const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500
// create css to reassign every var
const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`;
createStyleSheet("clientThemeLightModeFixes", [
reassignBackgrounds,
reassignBackgroundColors,
reassignVariables,
].join("\n\n"));
}
function captureOne(str, regex) {
const result = str.match(regex);
return (result === null) ? null : result[1];
}
function mapReject(arr, mapFunc, rejectFunc = _.isNull) {
return _.reject(arr.map(mapFunc), rejectFunc);
} }
function updateColorVars(color: string) { function updateColorVars(color: string) {
const { hue, saturation, lightness } = hexToHSL(color); const { hue, saturation, lightness } = hexToHSL(color);
let style = document.getElementById("clientThemeVars"); let style = document.getElementById("clientThemeVars");
if (!style) { if (!style)
style = document.createElement("style"); style = createStyleSheet("clientThemeVars");
style.setAttribute("id", "clientThemeVars");
document.head.appendChild(style);
}
style.textContent = `:root { style.textContent = `:root {
--theme-h: ${hue}; --theme-h: ${hue};
@ -156,6 +218,28 @@ function updateColorVars(color: string) {
}`; }`;
} }
function createStyleSheet(id, content = "") {
const style = document.createElement("style");
style.setAttribute("id", id);
style.textContent = content.split("\n").map(line => line.trim()).join("\n");
document.body.appendChild(style);
return style;
}
// returns all of discord's native styles in a single string
async function getStyles(): Promise<string> {
let out = "";
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
for (const styleLinkNode of styleLinkNodes) {
const cssLink = styleLinkNode.getAttribute("href");
if (!cssLink) continue;
const res = await fetch(cssLink);
out += await res.text();
}
return out;
}
// https://css-tricks.com/converting-color-spaces-in-javascript/ // https://css-tricks.com/converting-color-spaces-in-javascript/
function hexToHSL(hexCode: string) { function hexToHSL(hexCode: string) {
// Hex => RGB normalized to 0-1 // Hex => RGB normalized to 0-1
@ -198,17 +282,14 @@ function hexToHSL(hexCode: string) {
return { hue, saturation, lightness }; return { hue, saturation, lightness };
} }
// Minimized math just for lightness, lowers lag when changing colors // https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
function hexToLightness(hexCode: string) { function relativeLuminance(hexCode: string) {
// Hex => RGB normalized to 0-1 const normalize = (x: number) =>
const r = parseInt(hexCode.substring(0, 2), 16) / 255; x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
const cMax = Math.max(r, g, b); const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255);
const cMin = Math.min(r, g, b); const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255);
const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255);
const lightness = 100 * ((cMax + cMin) / 2); return r * 0.2126 + g * 0.7152 + b * 0.0722;
return lightness;
} }

View file

@ -0,0 +1,5 @@
# FixYoutubeEmbeds
Bypasses youtube videos being blocked from display on Discord (for example by UMG)
![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be)

View file

@ -0,0 +1,14 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "FixYoutubeEmbeds",
description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)",
authors: [Devs.coolelectronics]
});

View file

@ -0,0 +1,26 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { getSettings } from "main/ipcMain";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
if (frame.url.startsWith("https://www.youtube.com/")) {
const settings = getSettings().plugins?.FixYoutubeEmbeds;
if (!settings?.enabled) return;
frame.executeJavaScript(`
new MutationObserver(() => {
let err = document.querySelector(".ytp-error-content-wrap-subreason span")?.textContent;
if (err && err.includes("blocked it from display")) window.location.reload()
}).observe(document.body, { childList: true, subtree:true });
`);
}
});
});
});

View file

@ -14,7 +14,7 @@ import { ReviewDBAuth } from "./entities";
const DATA_STORE_KEY = "rdb-auth"; const DATA_STORE_KEY = "rdb-auth";
const OAuth = findByPropsLazy("OAuth2AuthorizeModal"); const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
export let Auth: ReviewDBAuth = {}; export let Auth: ReviewDBAuth = {};
@ -46,7 +46,7 @@ export async function updateAuth(newAuth: ReviewDBAuth) {
export function authorize(callback?: any) { export function authorize(callback?: any) {
openModal(props => openModal(props =>
<OAuth.OAuth2AuthorizeModal <OAuth2AuthorizeModal
{...props} {...props}
scopes={["identify"]} scopes={["identify"]}
responseType="code" responseType="code"
@ -64,7 +64,7 @@ export function authorize(callback?: any) {
const { token, success } = await res.json(); const { token, success } = await res.json();
if (success) { if (success) {
updateAuth({ token }); updateAuth({ token });
showToast("Successfully logged in!"); showToast("Successfully logged in!", Toasts.Type.SUCCESS);
callback?.(); callback?.();
} else if (res.status === 1) { } else if (res.status === 1) {
showToast("An Error occurred while logging in.", Toasts.Type.FAILURE); showToast("An Error occurred while logging in.", Toasts.Type.FAILURE);

View file

@ -32,7 +32,7 @@ import { BlockButton, DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge"; import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => { export default LazyComponent(() => {
// this is terrible, blame ven // this is terrible, blame mantika
const p = filters.byProps; const p = filters.byProps;
const [ const [
{ cozyMessage, buttons, message, buttonsInner, groupStart }, { cozyMessage, buttons, message, buttonsInner, groupStart },

View file

@ -28,8 +28,8 @@ import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent"; import ReviewComponent from "./ReviewComponent";
const Slate = findByPropsLazy("Editor", "Transforms"); const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
const InputTypes = findByPropsLazy("ChatInputTypes"); const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default);
@ -122,7 +122,7 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch():
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
const { token } = Auth; const { token } = Auth;
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const inputType = InputTypes.ChatInputTypes.FORM; const inputType = ChatInputTypes.FORM;
inputType.disableAutoFocus = true; inputType.disableAutoFocus = true;
const channel = { const channel = {
@ -172,10 +172,9 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: {
refetch(); refetch();
const slateEditor = editorRef.current.ref.current.getSlateEditor(); const slateEditor = editorRef.current.ref.current.getSlateEditor();
const { Editor, Transform } = Slate;
// clear editor // clear editor
Transform.delete(slateEditor, { Transforms.delete(slateEditor, {
at: { at: {
anchor: Editor.start(slateEditor, []), anchor: Editor.start(slateEditor, []),
focus: Editor.end(slateEditor, []), focus: Editor.end(slateEditor, []),

View file

@ -19,10 +19,10 @@
import { showToast, Toasts } from "@webpack/common"; import { showToast, Toasts } from "@webpack/common";
import { Auth, authorize, getToken, updateAuth } from "./auth"; import { Auth, authorize, getToken, updateAuth } from "./auth";
import { Review, ReviewDBCurrentUser, ReviewDBUser } from "./entities"; import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities";
import { settings } from "./settings"; import { settings } from "./settings";
const API_URL = "https://manti.vendicated.dev"; const API_URL = "https://manti.vendicated.dev/api/reviewdb";
export const REVIEWS_PER_PAGE = 50; export const REVIEWS_PER_PAGE = 50;
@ -45,13 +45,13 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
flags: String(flags), flags: String(flags),
offset: String(offset) offset: String(offset)
}); });
const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`); const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`);
const res = (req.status === 200) const res = (req.status === 200)
? await req.json() as Response ? await req.json() as Response
: { : {
success: false, success: false,
message: "An Error occured while fetching reviews. Please try again later.", message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.",
reviews: [], reviews: [],
updated: false, updated: false,
hasNextPage: false, hasNextPage: false,
@ -65,14 +65,15 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
reviews: [ reviews: [
{ {
id: 0, id: 0,
comment: "An Error occured while fetching reviews. Please try again later.", comment: res.message,
star: 0, star: 0,
timestamp: 0, timestamp: 0,
type: ReviewType.System,
sender: { sender: {
id: 0, id: 0,
username: "Error", username: "ReviewDB",
profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128", profilePhoto: "https://cdn.discordapp.com/avatars/1134864775000629298/3f87ad315b32ee464d84f1270c8d1b37.png?size=256&format=webp&quality=lossless",
discordID: "0", discordID: "1134864775000629298",
badges: [] badges: []
} }
} }
@ -92,7 +93,7 @@ export async function addReview(review: any): Promise<Response | null> {
return null; return null;
} }
return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, { return fetch(API_URL + `/users/${review.userid}/reviews`, {
method: "PUT", method: "PUT",
body: JSON.stringify(review), body: JSON.stringify(review),
headers: { headers: {
@ -107,7 +108,7 @@ export async function addReview(review: any): Promise<Response | null> {
} }
export async function deleteReview(id: number): Promise<Response> { export async function deleteReview(id: number): Promise<Response> {
return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { return fetch(API_URL + `/users/${id}/reviews`, {
method: "DELETE", method: "DELETE",
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
@ -121,7 +122,7 @@ export async function deleteReview(id: number): Promise<Response> {
} }
export async function reportReview(id: number) { export async function reportReview(id: number) {
const res = await fetch(API_URL + "/api/reviewdb/reports", { const res = await fetch(API_URL + "/reports", {
method: "PUT", method: "PUT",
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
@ -137,7 +138,7 @@ export async function reportReview(id: number) {
} }
async function patchBlock(action: "block" | "unblock", userId: string) { async function patchBlock(action: "block" | "unblock", userId: string) {
const res = await fetch(API_URL + "/api/reviewdb/blocks", { const res = await fetch(API_URL + "/blocks", {
method: "PATCH", method: "PATCH",
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
@ -168,7 +169,7 @@ export const blockUser = (userId: string) => patchBlock("block", userId);
export const unblockUser = (userId: string) => patchBlock("unblock", userId); export const unblockUser = (userId: string) => patchBlock("unblock", userId);
export async function fetchBlocks(): Promise<ReviewDBUser[]> { export async function fetchBlocks(): Promise<ReviewDBUser[]> {
const res = await fetch(API_URL + "/api/reviewdb/blocks", { const res = await fetch(API_URL + "/blocks", {
method: "GET", method: "GET",
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
@ -181,14 +182,14 @@ export async function fetchBlocks(): Promise<ReviewDBUser[]> {
} }
export function getCurrentUserInfo(token: string): Promise<ReviewDBCurrentUser> { export function getCurrentUserInfo(token: string): Promise<ReviewDBCurrentUser> {
return fetch(API_URL + "/api/reviewdb/users", { return fetch(API_URL + "/users", {
body: JSON.stringify({ token }), body: JSON.stringify({ token }),
method: "POST", method: "POST",
}).then(r => r.json()); }).then(r => r.json());
} }
export async function readNotification(id: number) { export async function readNotification(id: number) {
return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, { return fetch(API_URL + `/notifications?id=${id}`, {
method: "PATCH", method: "PATCH",
headers: { headers: {
"Authorization": await getToken() || "", "Authorization": await getToken() || "",

View file

@ -72,10 +72,6 @@ export default definePlugin({
{ {
find: 'tutorialId:"whos-online', find: 'tutorialId:"whos-online',
replacement: [ replacement: [
{
match: /\i.roleIcon,\.\.\.\i/,
replace: "$&,color:$self.roleGroupColor(arguments[0])"
},
{ {
match: /null,\i," — ",\i\]/, match: /null,\i," — ",\i\]/,
replace: "null,$self.roleGroupColor(arguments[0])]" replace: "null,$self.roleGroupColor(arguments[0])]"
@ -83,6 +79,16 @@ export default definePlugin({
], ],
predicate: () => settings.store.memberList, predicate: () => settings.store.memberList,
}, },
{
find: ".Messages.THREAD_BROWSER_PRIVATE",
replacement: [
{
match: /children:\[\i," — ",\i\]/,
replace: "children:[$self.roleGroupColor(arguments[0])]"
},
],
predicate: () => settings.store.memberList,
},
{ {
find: "renderPrioritySpeaker", find: "renderPrioritySpeaker",
replacement: [ replacement: [

View file

@ -46,6 +46,23 @@ const settings = definePluginSettings({
} }
}); });
const MEDIA_PROXY_URL = "https://media.discordapp.net";
const CDN_URL = "https://cdn.discordapp.com";
function fixImageUrl(urlString: string, explodeWebp: boolean) {
const url = new URL(urlString);
if (url.origin === CDN_URL) return urlString;
if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname;
url.searchParams.delete("width");
url.searchParams.delete("height");
url.searchParams.set("quality", "lossless");
if (explodeWebp && url.searchParams.get("format") === "webp")
url.searchParams.set("format", "png");
return url.toString();
}
export default definePlugin({ export default definePlugin({
name: "WebContextMenus", name: "WebContextMenus",
description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)", description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)",
@ -182,34 +199,40 @@ export default definePlugin({
], ],
async copyImage(url: string) { async copyImage(url: string) {
if (IS_VESKTOP && VesktopNative.clipboard) { url = fixImageUrl(url, true);
const data = await fetch(url).then(r => r.arrayBuffer());
VesktopNative.clipboard.copyImage(data, url); let imageData = await fetch(url).then(r => r.blob());
return; if (imageData.type !== "image/png") {
const bitmap = await createImageBitmap(imageData);
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext("2d")!.drawImage(bitmap, 0, 0);
await new Promise<void>(done => {
canvas.toBlob(data => {
imageData = data!;
done();
}, "image/png");
});
} }
// Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png if (IS_VESKTOP && VesktopNative.clipboard) {
// via canvas first VesktopNative.clipboard.copyImage(await imageData.arrayBuffer(), url);
const img = new Image(); return;
img.onload = () => { } else {
const canvas = document.createElement("canvas"); navigator.clipboard.write([
canvas.width = img.naturalWidth; new ClipboardItem({
canvas.height = img.naturalHeight; "image/png": imageData
canvas.getContext("2d")!.drawImage(img, 0, 0); })
]);
canvas.toBlob(data => { }
navigator.clipboard.write([
new ClipboardItem({
"image/png": data!
})
]);
}, "image/png");
};
img.crossOrigin = "anonymous";
img.src = url;
}, },
async saveImage(url: string) { async saveImage(url: string) {
url = fixImageUrl(url, false);
const data = await fetchImage(url); const data = await fetchImage(url);
if (!data) return; if (!data) return;

View file

@ -403,6 +403,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Grzesiek11", name: "Grzesiek11",
id: 368475654662127616n, id: 368475654662127616n,
}, },
Samwich: {
name: "Samwich",
id: 976176454511509554n,
},
coolelectronics: {
name: "coolelectronics",
id: 696392247205298207n,
}
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly