Compare commits
14 Commits
v1.4.7
...
feat/start
Author | SHA1 | Date | |
---|---|---|---|
|
9bc373d87a | ||
|
fb7e1c64fd | ||
|
1128e8f3ad | ||
|
a73d09a2f0 | ||
|
cf7c4d63b6 | ||
|
a95311ef2c | ||
|
dd23f9802c | ||
|
f23ddf4cae | ||
|
4222c7fd9f | ||
|
09f65b401e | ||
|
7364776715 | ||
|
3a5b70d410 | ||
|
e08d49edac | ||
|
17abbd3e3e |
5
src/plugins/dearrow/README.md
Normal file
5
src/plugins/dearrow/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Dearrow
|
||||
|
||||
Makes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/)
|
||||
|
||||
https://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283
|
161
src/plugins/dearrow/index.tsx
Normal file
161
src/plugins/dearrow/index.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
import type { Component } from "react";
|
||||
|
||||
interface Props {
|
||||
embed: {
|
||||
rawTitle: string;
|
||||
provider?: {
|
||||
name: string;
|
||||
};
|
||||
thumbnail: {
|
||||
proxyURL: string;
|
||||
};
|
||||
video: {
|
||||
url: string;
|
||||
};
|
||||
|
||||
dearrow: {
|
||||
enabled: boolean;
|
||||
oldTitle?: string;
|
||||
oldThumb?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
||||
|
||||
async function embedDidMount(this: Component<Props>) {
|
||||
try {
|
||||
const { embed } = this.props;
|
||||
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||
|
||||
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||
if (!videoId) return;
|
||||
|
||||
const res = await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`);
|
||||
if (!res.ok) return;
|
||||
|
||||
const { titles, thumbnails } = await res.json();
|
||||
|
||||
const hasTitle = titles[0]?.votes >= 0;
|
||||
const hasThumb = thumbnails[0]?.votes >= 0;
|
||||
|
||||
if (!hasTitle && !hasThumb) return;
|
||||
|
||||
embed.dearrow = {
|
||||
enabled: true
|
||||
};
|
||||
|
||||
if (titles[0]?.votes >= 0) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
embed.rawTitle = titles[0].title;
|
||||
}
|
||||
|
||||
if (thumbnails[0]?.votes >= 0) {
|
||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||
}
|
||||
|
||||
this.forceUpdate();
|
||||
} catch (err) {
|
||||
new Logger("Dearrow").error("Failed to dearrow embed", err);
|
||||
}
|
||||
}
|
||||
|
||||
function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||
const { embed } = component.props;
|
||||
if (!embed?.dearrow) return null;
|
||||
|
||||
return (
|
||||
<Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<button
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||
onClick={() => {
|
||||
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||
embed.dearrow.enabled = !enabled;
|
||||
if (oldTitle) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
embed.rawTitle = oldTitle;
|
||||
}
|
||||
if (oldThumb) {
|
||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||
embed.thumbnail.proxyURL = oldThumb;
|
||||
}
|
||||
|
||||
component.forceUpdate();
|
||||
}}
|
||||
>
|
||||
{/* Dearrow Icon, taken from https://dearrow.ajay.app/logo.svg (and optimised) */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 36 36"
|
||||
aria-label="Toggle Dearrow"
|
||||
>
|
||||
<path
|
||||
fill="#1213BD"
|
||||
d="M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z"
|
||||
/>
|
||||
<path
|
||||
fill="#88c9f9"
|
||||
d="m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z"
|
||||
/>
|
||||
<path
|
||||
fill="#0a62a5"
|
||||
d="m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Dearrow",
|
||||
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
embedDidMount,
|
||||
renderButton(component: Component<Props>) {
|
||||
return (
|
||||
<ErrorBoundary noop>
|
||||
<DearrowButton component={component} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
},
|
||||
|
||||
patches: [{
|
||||
find: "this.renderInlineMediaEmbed",
|
||||
replacement: [
|
||||
// patch componentDidMount to replace embed thumbnail and title
|
||||
{
|
||||
match: /(\i).render=function.{0,50}\i\.embed/,
|
||||
replace: "$1.componentDidMount=$self.embedDidMount,$&"
|
||||
},
|
||||
|
||||
// add dearrow button
|
||||
{
|
||||
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||
replace: "children:[$self.renderButton(this),"
|
||||
}
|
||||
]
|
||||
}],
|
||||
});
|
12
src/plugins/dearrow/styles.css
Normal file
12
src/plugins/dearrow/styles.css
Normal file
@ -0,0 +1,12 @@
|
||||
.vc-dearrow-toggle-off svg {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.vc-dearrow-toggle-on, .vc-dearrow-toggle-off {
|
||||
all: unset;
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
}
|
@ -212,15 +212,15 @@ export default definePlugin({
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canStreamHighQuality:function",
|
||||
find: "canUseHighVideoUploadQuality:function",
|
||||
predicate: () => settings.store.enableStreamQualityBypass,
|
||||
replacement: [
|
||||
"canUseHighVideoUploadQuality",
|
||||
"canStreamHighQuality",
|
||||
"canStreamMidQuality"
|
||||
// TODO: Remove the last two when they get removed from stable
|
||||
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
|
||||
].map(func => {
|
||||
return {
|
||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
||||
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
|
||||
replace: "$&return true;"
|
||||
};
|
||||
})
|
||||
|
6
src/plugins/favEmojiFirst/README.md
Normal file
6
src/plugins/favEmojiFirst/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
# FavoriteEmojiFirst
|
||||
|
||||
Puts your favorite emoji first in the emoji autocomplete.
|
||||
|
||||
![FavEmojis](https://i.imgur.com/mEFCoZG.png)
|
||||
![Example](https://i.imgur.com/wY3Tc43.png)
|
5
src/plugins/favGifSearch/README.md
Normal file
5
src/plugins/favGifSearch/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# FavoriteGifSearch
|
||||
|
||||
Adds a search bar to favorite gifs.
|
||||
|
||||
![Screenshot](https://i.imgur.com/Bcgb7PD.png)
|
@ -87,7 +87,7 @@ export const settings = definePluginSettings({
|
||||
export default definePlugin({
|
||||
name: "FavoriteGifSearch",
|
||||
authors: [Devs.Aria],
|
||||
description: "Adds a search bar for favorite gifs",
|
||||
description: "Adds a search bar to favorite gifs.",
|
||||
|
||||
patches: [
|
||||
{
|
6
src/plugins/imageZoom/README.md
Normal file
6
src/plugins/imageZoom/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
# ImageZoom
|
||||
|
||||
Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size
|
||||
|
||||
![Example](https://i.imgur.com/VJdo4aq.png)
|
||||
![ContextMenu](https://i.imgur.com/0oaRM2s.png)
|
@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
||||
ContextMenu.close();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuCheckboxItem
|
||||
id="vc-nearest-neighbour"
|
||||
label="Nearset Neighbour"
|
||||
checked={settings.store.nearestNeighbour}
|
||||
action={() => {
|
||||
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
||||
ContextMenu.close();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuControlItem
|
||||
id="vc-zoom"
|
||||
label="Zoom"
|
||||
|
@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
|
||||
import { decrypt } from "../index";
|
||||
|
||||
export function DecModal(props: any) {
|
||||
const secret: string = props?.message?.content;
|
||||
const encryptedMessage: string = props?.message?.content;
|
||||
const [password, setPassword] = React.useState("password");
|
||||
|
||||
return (
|
||||
@ -38,9 +38,9 @@ export function DecModal(props: any) {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
||||
<TextInput defaultValue={secret} disabled={true}></TextInput>
|
||||
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
|
||||
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
onChange={setPassword}
|
||||
@ -51,7 +51,7 @@ export function DecModal(props: any) {
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={() => {
|
||||
const toSend = decrypt(secret, password, true);
|
||||
const toSend = decrypt(encryptedMessage, password, true);
|
||||
if (!toSend || !props?.message) return;
|
||||
// @ts-expect-error
|
||||
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
||||
|
@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
|
||||
return steggo.hide(secret + "\u200b", password, cover);
|
||||
}
|
||||
|
||||
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
|
||||
const decrypted = steggo.reveal(secret, password);
|
||||
export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
|
||||
const decrypted = steggo.reveal(encrypted, password);
|
||||
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,14 @@ const enum ActivityFlag {
|
||||
INSTANCE = 1 << 0,
|
||||
}
|
||||
|
||||
const enum NameFormat {
|
||||
StatusName = "status-name",
|
||||
ArtistFirst = "artist-first",
|
||||
SongFirst = "song-first",
|
||||
ArtistOnly = "artist",
|
||||
SongOnly = "song"
|
||||
}
|
||||
|
||||
const applicationId = "1108588077900898414";
|
||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||
|
||||
@ -117,10 +125,37 @@ const settings = definePluginSettings({
|
||||
default: true,
|
||||
},
|
||||
statusName: {
|
||||
description: "text shown in status",
|
||||
description: "custom status text",
|
||||
type: OptionType.STRING,
|
||||
default: "some music",
|
||||
},
|
||||
nameFormat: {
|
||||
description: "Show name of song and artist in status name",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Use custom status name",
|
||||
value: NameFormat.StatusName,
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Use format 'artist - song'",
|
||||
value: NameFormat.ArtistFirst
|
||||
},
|
||||
{
|
||||
label: "Use format 'song - artist'",
|
||||
value: NameFormat.SongFirst
|
||||
},
|
||||
{
|
||||
label: "Use artist name only",
|
||||
value: NameFormat.ArtistOnly
|
||||
},
|
||||
{
|
||||
label: "Use song name only",
|
||||
value: NameFormat.SongOnly
|
||||
}
|
||||
],
|
||||
},
|
||||
useListeningStatus: {
|
||||
description: 'show "Listening to" status instead of "Playing"',
|
||||
type: OptionType.BOOLEAN,
|
||||
@ -140,13 +175,13 @@ const settings = definePluginSettings({
|
||||
value: "placeholder"
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "LastFMRichPresence",
|
||||
description: "Little plugin for Last.fm rich presence",
|
||||
authors: [Devs.dzshn, Devs.RuiNtD],
|
||||
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
<>
|
||||
@ -267,9 +302,24 @@ export default definePlugin({
|
||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||
});
|
||||
|
||||
const statusName = (() => {
|
||||
switch (settings.store.nameFormat) {
|
||||
case NameFormat.ArtistFirst:
|
||||
return trackData.artist + " - " + trackData.name;
|
||||
case NameFormat.SongFirst:
|
||||
return trackData.name + " - " + trackData.artist;
|
||||
case NameFormat.ArtistOnly:
|
||||
return trackData.artist;
|
||||
case NameFormat.SongOnly:
|
||||
return trackData.name;
|
||||
default:
|
||||
return settings.store.statusName;
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
application_id: applicationId,
|
||||
name: settings.store.statusName,
|
||||
name: statusName,
|
||||
|
||||
details: trackData.name,
|
||||
state: trackData.artist,
|
||||
|
@ -326,14 +326,14 @@ export default definePlugin({
|
||||
{
|
||||
// Attachment renderer
|
||||
// Module 96063
|
||||
find: "[\"className\",\"attachment\",\"inlineMedia\"",
|
||||
find: "().removeAttachmentHoverButton",
|
||||
replacement: [
|
||||
{
|
||||
match: /((\w)\.className,\w=\2\.attachment),/,
|
||||
replace: "$1,deleted=$2.attachment?.deleted,"
|
||||
},
|
||||
{
|
||||
match: /\["className","attachment","inlineMedia".+?className:/,
|
||||
match: /\["className","attachment".+?className:/,
|
||||
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
||||
}
|
||||
]
|
||||
|
@ -29,8 +29,8 @@ export default definePlugin({
|
||||
{
|
||||
find: ".onRemoveAttachment,",
|
||||
replacement: {
|
||||
match: /\.nonMediaAttachment.{0,10}children:\[(\i),/,
|
||||
replace: "$&$1&&$self.renderPiPButton(),"
|
||||
match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
|
||||
replace: "$&$1&&$2&&$self.renderPiPButton(),"
|
||||
},
|
||||
},
|
||||
],
|
||||
|
5
src/plugins/previewMessage/README.md
Normal file
5
src/plugins/previewMessage/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# PreviewMessage
|
||||
|
||||
Lets you preview your message before sending it.
|
||||
|
||||
![Example](https://i.imgur.com/etqbkzu.png)
|
@ -16,37 +16,96 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { sendBotMessage } from "@api/Commands";
|
||||
import { generateId, sendBotMessage } from "@api/Commands";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||
import { MessageAttachment } from "discord-types/general";
|
||||
|
||||
interface Props {
|
||||
type: {
|
||||
analyticsName: string;
|
||||
isEmpty: boolean;
|
||||
attachments: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const UploadStore = findByPropsLazy("getUploads");
|
||||
|
||||
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
||||
|
||||
|
||||
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
||||
new Promise(res => {
|
||||
const img = new Image();
|
||||
img.onload = () =>
|
||||
res({ width: img.width, height: img.height });
|
||||
|
||||
img.onerror = () =>
|
||||
res(null);
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
|
||||
|
||||
const getAttachments = async (channelId: string) =>
|
||||
await Promise.all(
|
||||
UploadStore.getUploads(channelId, DraftType.ChannelMessage)
|
||||
.map(async (upload: any) => {
|
||||
const { isImage, filename, spoiler, item: { file } } = upload;
|
||||
const url = URL.createObjectURL(file);
|
||||
const attachment: MessageAttachment = {
|
||||
id: generateId(),
|
||||
filename: spoiler ? "SPOILER_" + filename : filename,
|
||||
// weird eh? if i give it the normal content type the preview doenst work
|
||||
content_type: undefined,
|
||||
size: await upload.getSize(),
|
||||
spoiler,
|
||||
// discord adds query params to the url, so we need to add a hash to prevent that
|
||||
url: url + "#",
|
||||
proxy_url: url + "#",
|
||||
};
|
||||
|
||||
if (isImage) {
|
||||
const box = await getImageBox(url);
|
||||
if (!box) return attachment;
|
||||
|
||||
attachment.width = box.width;
|
||||
attachment.height = box.height;
|
||||
}
|
||||
|
||||
return attachment;
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
export function PreviewButton(chatBoxProps: Props) {
|
||||
const { isEmpty, attachments } = chatBoxProps.type;
|
||||
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
|
||||
|
||||
if (chatBoxProps.type.analyticsName !== "normal") return null;
|
||||
if (!draft) return null;
|
||||
|
||||
const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0;
|
||||
const hasContent = !isEmpty && draft?.length > 0;
|
||||
|
||||
if (!hasContent && !hasAttachments) return null;
|
||||
|
||||
return (
|
||||
<Tooltip text="Preview Message">
|
||||
{tooltipProps => (
|
||||
<Button
|
||||
{...tooltipProps}
|
||||
onClick={() =>
|
||||
onClick={async () =>
|
||||
sendBotMessage(
|
||||
channelId,
|
||||
{
|
||||
content: getDraft(channelId),
|
||||
author: UserStore.getCurrentUser()
|
||||
author: UserStore.getCurrentUser(),
|
||||
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
||||
}
|
||||
)}
|
||||
size=""
|
||||
@ -66,7 +125,7 @@ export function PreviewButton(chatBoxProps: Props) {
|
||||
|
||||
export default definePlugin({
|
||||
name: "PreviewMessage",
|
||||
description: "Lets you preview your message before sending it",
|
||||
description: "Lets you preview your message before sending it.",
|
||||
authors: [Devs.Aria],
|
||||
patches: [
|
||||
{
|
5
src/plugins/searchReply/README.md
Normal file
5
src/plugins/searchReply/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# SearchReply
|
||||
|
||||
Adds a reply button to search results.
|
||||
|
||||
![Screenshot](https://i.imgur.com/SjIEHpw.png)
|
23
src/plugins/startupTimings/StartupTimingPage.css
Normal file
23
src/plugins/startupTimings/StartupTimingPage.css
Normal file
@ -0,0 +1,23 @@
|
||||
.vc-startuptimings-server-trace {
|
||||
color: var(--header-primary);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.vc-startuptimings-grid {
|
||||
color: var(--header-primary);
|
||||
display: grid;
|
||||
gap: 2px 10px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.vc-startuptimings-4-cols {
|
||||
grid-template-columns: repeat(3, auto) 1fr;
|
||||
}
|
||||
|
||||
.vc-startuptimings-3-cols {
|
||||
grid-template-columns: repeat(2, auto) 1fr;
|
||||
}
|
||||
|
||||
.vc-startuptimings-2-cols {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
@ -16,11 +16,35 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./StartupTimingPage.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Forms, React } from "@webpack/common";
|
||||
|
||||
|
||||
export const cl = classNameFactory("vc-startuptimings-");
|
||||
|
||||
interface ITTITrackerEvent {
|
||||
emoji: string;
|
||||
name: string;
|
||||
start: number;
|
||||
end: number;
|
||||
hasData(): boolean;
|
||||
}
|
||||
|
||||
interface ITTITracker {
|
||||
serializeTTITracker(): Record<string, string | number | boolean | null | undefined>;
|
||||
[event: string]: ITTITrackerEvent | string | boolean | null | any;
|
||||
}
|
||||
|
||||
/** Time-To-Interactive Tracker */
|
||||
const TTITracker: ITTITracker = findByPropsLazy("serializeTTITracker");
|
||||
|
||||
interface AppStartPerformance {
|
||||
prefix: string;
|
||||
logs: Log[];
|
||||
@ -91,11 +115,11 @@ function TimingSection({ title, logs, traceEnd }: TimingSectionProps) {
|
||||
<Forms.FormSection title={title} tag="h1">
|
||||
<code>
|
||||
{traceEnd && (
|
||||
<div style={{ color: "var(--header-primary)", marginBottom: 5, userSelect: "text" }}>
|
||||
<div className={cl("server-trace")} style={{ marginBottom: 5 }}>
|
||||
Trace ended at: {(new Date(traceEnd)).toTimeString()}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ color: "var(--header-primary)", display: "grid", gridTemplateColumns: "repeat(3, auto) 1fr", gap: "2px 10px", userSelect: "text" }}>
|
||||
<div className={classes(cl("grid"), cl("4-cols"))}>
|
||||
<span>Start</span>
|
||||
<span>Interval</span>
|
||||
<span>Delta</span>
|
||||
@ -119,7 +143,7 @@ function ServerTrace({ trace }: ServerTraceProps) {
|
||||
return (
|
||||
<Forms.FormSection title="Server Trace" tag="h2">
|
||||
<code>
|
||||
<Flex flexDirection="column" style={{ color: "var(--header-primary)", gap: 5, userSelect: "text" }}>
|
||||
<Flex flexDirection="column" className={cl("server-trace")} style={{ gap: 5 }}>
|
||||
{lines.map(line => (
|
||||
<span>{line}</span>
|
||||
))}
|
||||
@ -129,11 +153,70 @@ function ServerTrace({ trace }: ServerTraceProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function TTIAnalytics() {
|
||||
const analytics = TTITracker.serializeTTITracker();
|
||||
const filteredAnalytics = Object.entries(analytics).filter(([key, value]) => value != null && !/_start$|_end$/.test(key));
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Forms.FormSection title="TTI Analytics" tag="h2">
|
||||
<code>
|
||||
<div className={classes(cl("grid"), cl("2-cols"))}>
|
||||
{filteredAnalytics.map(([key, value]) => (
|
||||
<React.Fragment>
|
||||
<span><pre>{key}</pre></span>
|
||||
<span><pre>{`${value}`}</pre></span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</code>
|
||||
</Forms.FormSection>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
interface TTITimingsProps {
|
||||
records: [string, ITTITrackerEvent][];
|
||||
title: string;
|
||||
type: "registered" | "unregistered";
|
||||
}
|
||||
|
||||
function TTITimings({ records, title, type }: TTITimingsProps) {
|
||||
const isRegistered = type === "registered";
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Forms.FormSection title={title} tag="h2">
|
||||
<code>
|
||||
<div className={classes(cl("grid"), cl(isRegistered ? "3-cols" : "2-cols"))}>
|
||||
{isRegistered && <span>Duration</span>}
|
||||
<span>Key</span>
|
||||
<span style={{ marginBottom: 5 }}>Event</span>
|
||||
{records.map(([key, event]) => (
|
||||
<React.Fragment key={key}>
|
||||
{isRegistered && <span><pre>{event.end - event.start}ms</pre></span>}
|
||||
<span><pre>{key}</pre></span>
|
||||
<span><pre>{event.emoji} {event.name}</pre></span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</code>
|
||||
</Forms.FormSection>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function StartupTimingPage() {
|
||||
if (!AppStartPerformance?.logs) return <div>Loading...</div>;
|
||||
|
||||
const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace;
|
||||
|
||||
const registeredTTITimings: [string, ITTITrackerEvent][] = (Object.entries(TTITracker))
|
||||
.filter(([, value]) => value?.hasData?.());
|
||||
|
||||
const unregisteredTTITimings: [string, ITTITrackerEvent][] = (Object.entries(TTITracker))
|
||||
.filter(([, value]) => value?.hasData && !value.hasData());
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TimingSection
|
||||
@ -141,9 +224,26 @@ function StartupTimingPage() {
|
||||
logs={AppStartPerformance.logs}
|
||||
traceEnd={AppStartPerformance.endTime_}
|
||||
/>
|
||||
{/* Lazy Divider */}
|
||||
<div style={{ marginTop: 5 }}> </div>
|
||||
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||
|
||||
{serverTrace && <ServerTrace trace={serverTrace} />}
|
||||
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||
|
||||
<TTIAnalytics />
|
||||
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||
|
||||
<TTITimings
|
||||
title="Registered TTI Timings"
|
||||
records={registeredTTITimings}
|
||||
type="registered"
|
||||
/>
|
||||
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||
|
||||
<TTITimings
|
||||
title="Unregistered TTI Timings"
|
||||
records={unregisteredTTITimings}
|
||||
type="unregistered"
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -366,6 +366,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
RuukuLada: {
|
||||
name: "RuukuLada",
|
||||
id: 119705748346241027n,
|
||||
},
|
||||
blahajZip: {
|
||||
name: "blahaj.zip",
|
||||
id: 683954422241427471n,
|
||||
},
|
||||
archeruwu: {
|
||||
name: "archer_uwu",
|
||||
id: 160068695383736320n
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
|
9
src/webpack/common/types/menu.d.ts
vendored
9
src/webpack/common/types/menu.d.ts
vendored
@ -65,8 +65,13 @@ export interface Menu {
|
||||
id: string;
|
||||
interactive?: boolean;
|
||||
}>;
|
||||
// TODO: Type me
|
||||
MenuSliderControl: RC<any>;
|
||||
MenuSliderControl: RC<{
|
||||
minValue: number,
|
||||
maxValue: number,
|
||||
value: number,
|
||||
onChange(value: number): void,
|
||||
renderValue?(value: number): string,
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ContextMenuApi {
|
||||
|
Reference in New Issue
Block a user