Compare commits

..

14 Commits

Author SHA1 Message Date
Rie Takahashi
9bc373d87a fix: merge tti components, use css file 2023-09-17 20:45:04 +01:00
Rie Takahashi
fb7e1c64fd fix missing $ in regex 2023-09-17 19:42:59 +01:00
Rie Takahashi
1128e8f3ad feat(startup timings): add Time-To-Interactive info 2023-09-17 19:37:03 +01:00
Syncx
a73d09a2f0 PreviewMessage: Add attachments (& misc changes) (#1715) 2023-09-12 23:14:17 +02:00
AutumnVN
cf7c4d63b6 pictureInPicture: don't show PiP button on normal file (#1725) 2023-09-12 23:11:53 +02:00
Archer
a95311ef2c lastfm: Add setting for artist name and song title only (#1726)
Co-authored-by: V <vendicated@riseup.net>
2023-09-12 23:11:25 +02:00
lovenginx
dd23f9802c InvisibleChat: fixup decryption modal (#1720) 2023-09-12 23:04:50 +02:00
Nuckyz
f23ddf4cae oopsies 2023-09-12 05:10:44 -03:00
Nuckyz
4222c7fd9f Fix broken FakeNitro patch on canary 2023-09-12 05:01:18 -03:00
Nuckyz
09f65b401e Fix broken MessageLogger patches 2023-09-12 04:28:55 -03:00
V
7364776715 Dearrow: Fix button part 2 2023-09-09 19:51:30 +02:00
V
3a5b70d410 Dearrow: Fix button 2023-09-09 19:49:11 +02:00
V
e08d49edac New Plugin: Dearrow (#1723) 2023-09-09 19:17:50 +02:00
Mushrrom
17abbd3e3e LastFM: Add setting for using name + artist as activity name (#1713)
Co-authored-by: V <vendicated@riseup.net>
2023-09-09 04:22:41 +02:00
23 changed files with 492 additions and 33 deletions

View 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

View 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),"
}
]
}],
});

View 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;
}

View File

@ -212,15 +212,15 @@ export default definePlugin({
} }
}, },
{ {
find: "canStreamHighQuality:function", find: "canUseHighVideoUploadQuality:function",
predicate: () => settings.store.enableStreamQualityBypass, predicate: () => settings.store.enableStreamQualityBypass,
replacement: [ replacement: [
"canUseHighVideoUploadQuality", "canUseHighVideoUploadQuality",
"canStreamHighQuality", // TODO: Remove the last two when they get removed from stable
"canStreamMidQuality" "(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
].map(func => { ].map(func => {
return { return {
match: new RegExp(`${func}:function\\(\\i\\){`), match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
replace: "$&return true;" replace: "$&return true;"
}; };
}) })

View 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)

View File

@ -0,0 +1,5 @@
# FavoriteGifSearch
Adds a search bar to favorite gifs.
![Screenshot](https://i.imgur.com/Bcgb7PD.png)

View File

@ -87,7 +87,7 @@ export const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "FavoriteGifSearch", name: "FavoriteGifSearch",
authors: [Devs.Aria], authors: [Devs.Aria],
description: "Adds a search bar for favorite gifs", description: "Adds a search bar to favorite gifs.",
patches: [ patches: [
{ {

View 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)

View File

@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
ContextMenu.close(); 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 <Menu.MenuControlItem
id="vc-zoom" id="vc-zoom"
label="Zoom" label="Zoom"

View File

@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
import { decrypt } from "../index"; import { decrypt } from "../index";
export function DecModal(props: any) { export function DecModal(props: any) {
const secret: string = props?.message?.content; const encryptedMessage: string = props?.message?.content;
const [password, setPassword] = React.useState("password"); const [password, setPassword] = React.useState("password");
return ( return (
@ -38,9 +38,9 @@ export function DecModal(props: any) {
</ModalHeader> </ModalHeader>
<ModalContent> <ModalContent>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle> <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
<TextInput defaultValue={secret} disabled={true}></TextInput> <TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
<Forms.FormTitle tag="h5">Password</Forms.FormTitle> <Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
<TextInput <TextInput
style={{ marginBottom: "20px" }} style={{ marginBottom: "20px" }}
onChange={setPassword} onChange={setPassword}
@ -51,7 +51,7 @@ export function DecModal(props: any) {
<Button <Button
color={Button.Colors.GREEN} color={Button.Colors.GREEN}
onClick={() => { onClick={() => {
const toSend = decrypt(secret, password, true); const toSend = decrypt(encryptedMessage, password, true);
if (!toSend || !props?.message) return; if (!toSend || !props?.message) return;
// @ts-expect-error // @ts-expect-error
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend); Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);

View File

@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
return steggo.hide(secret + "\u200b", password, cover); return steggo.hide(secret + "\u200b", password, cover);
} }
export function decrypt(secret: string, password: string, removeIndicator: boolean): string { export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
const decrypted = steggo.reveal(secret, password); const decrypted = steggo.reveal(encrypted, password);
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted; return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
} }

View File

@ -72,6 +72,14 @@ const enum ActivityFlag {
INSTANCE = 1 << 0, INSTANCE = 1 << 0,
} }
const enum NameFormat {
StatusName = "status-name",
ArtistFirst = "artist-first",
SongFirst = "song-first",
ArtistOnly = "artist",
SongOnly = "song"
}
const applicationId = "1108588077900898414"; const applicationId = "1108588077900898414";
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
@ -117,10 +125,37 @@ const settings = definePluginSettings({
default: true, default: true,
}, },
statusName: { statusName: {
description: "text shown in status", description: "custom status text",
type: OptionType.STRING, type: OptionType.STRING,
default: "some music", 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: { useListeningStatus: {
description: 'show "Listening to" status instead of "Playing"', description: 'show "Listening to" status instead of "Playing"',
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -140,13 +175,13 @@ const settings = definePluginSettings({
value: "placeholder" value: "placeholder"
} }
], ],
} },
}); });
export default definePlugin({ export default definePlugin({
name: "LastFMRichPresence", name: "LastFMRichPresence",
description: "Little plugin for Last.fm rich presence", description: "Little plugin for Last.fm rich presence",
authors: [Devs.dzshn, Devs.RuiNtD], authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
settingsAboutComponent: () => ( settingsAboutComponent: () => (
<> <>
@ -267,9 +302,24 @@ export default definePlugin({
url: `https://www.last.fm/user/${settings.store.username}`, 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 { return {
application_id: applicationId, application_id: applicationId,
name: settings.store.statusName, name: statusName,
details: trackData.name, details: trackData.name,
state: trackData.artist, state: trackData.artist,

View File

@ -326,14 +326,14 @@ export default definePlugin({
{ {
// Attachment renderer // Attachment renderer
// Module 96063 // Module 96063
find: "[\"className\",\"attachment\",\"inlineMedia\"", find: "().removeAttachmentHoverButton",
replacement: [ replacement: [
{ {
match: /((\w)\.className,\w=\2\.attachment),/, match: /((\w)\.className,\w=\2\.attachment),/,
replace: "$1,deleted=$2.attachment?.deleted," replace: "$1,deleted=$2.attachment?.deleted,"
}, },
{ {
match: /\["className","attachment","inlineMedia".+?className:/, match: /\["className","attachment".+?className:/,
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +" replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
} }
] ]

View File

@ -29,8 +29,8 @@ export default definePlugin({
{ {
find: ".onRemoveAttachment,", find: ".onRemoveAttachment,",
replacement: { replacement: {
match: /\.nonMediaAttachment.{0,10}children:\[(\i),/, match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
replace: "$&$1&&$self.renderPiPButton()," replace: "$&$1&&$2&&$self.renderPiPButton(),"
}, },
}, },
], ],

View File

@ -0,0 +1,5 @@
# PreviewMessage
Lets you preview your message before sending it.
![Example](https://i.imgur.com/etqbkzu.png)

View File

@ -16,37 +16,96 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { MessageAttachment } from "discord-types/general";
interface Props { interface Props {
type: { type: {
analyticsName: string; analyticsName: string;
isEmpty: boolean;
attachments: boolean;
}; };
} }
const UploadStore = findByPropsLazy("getUploads");
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); 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) { export function PreviewButton(chatBoxProps: Props) {
const { isEmpty, attachments } = chatBoxProps.type;
const channelId = SelectedChannelStore.getChannelId(); const channelId = SelectedChannelStore.getChannelId();
const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
if (chatBoxProps.type.analyticsName !== "normal") return null; 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 ( return (
<Tooltip text="Preview Message"> <Tooltip text="Preview Message">
{tooltipProps => ( {tooltipProps => (
<Button <Button
{...tooltipProps} {...tooltipProps}
onClick={() => onClick={async () =>
sendBotMessage( sendBotMessage(
channelId, channelId,
{ {
content: getDraft(channelId), content: getDraft(channelId),
author: UserStore.getCurrentUser() author: UserStore.getCurrentUser(),
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
} }
)} )}
size="" size=""
@ -66,7 +125,7 @@ export function PreviewButton(chatBoxProps: Props) {
export default definePlugin({ export default definePlugin({
name: "PreviewMessage", name: "PreviewMessage",
description: "Lets you preview your message before sending it", description: "Lets you preview your message before sending it.",
authors: [Devs.Aria], authors: [Devs.Aria],
patches: [ patches: [
{ {

View File

@ -0,0 +1,5 @@
# SearchReply
Adds a reply button to search results.
![Screenshot](https://i.imgur.com/SjIEHpw.png)

View 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;
}

View File

@ -16,11 +16,35 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Forms, React } from "@webpack/common"; 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 { interface AppStartPerformance {
prefix: string; prefix: string;
logs: Log[]; logs: Log[];
@ -91,11 +115,11 @@ function TimingSection({ title, logs, traceEnd }: TimingSectionProps) {
<Forms.FormSection title={title} tag="h1"> <Forms.FormSection title={title} tag="h1">
<code> <code>
{traceEnd && ( {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()} Trace ended at: {(new Date(traceEnd)).toTimeString()}
</div> </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>Start</span>
<span>Interval</span> <span>Interval</span>
<span>Delta</span> <span>Delta</span>
@ -119,13 +143,66 @@ function ServerTrace({ trace }: ServerTraceProps) {
return ( return (
<Forms.FormSection title="Server Trace" tag="h2"> <Forms.FormSection title="Server Trace" tag="h2">
<code> <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 => ( {lines.map(line => (
<span>{line}</span> <span>{line}</span>
))} ))}
</Flex> </Flex>
</code> </code>
</Forms.FormSection >
);
}
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> </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>
); );
} }
@ -134,6 +211,12 @@ function StartupTimingPage() {
const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace; 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 ( return (
<React.Fragment> <React.Fragment>
<TimingSection <TimingSection
@ -141,9 +224,26 @@ function StartupTimingPage() {
logs={AppStartPerformance.logs} logs={AppStartPerformance.logs}
traceEnd={AppStartPerformance.endTime_} traceEnd={AppStartPerformance.endTime_}
/> />
{/* Lazy Divider */} <Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
<div style={{ marginTop: 5 }}>&nbsp;</div>
{serverTrace && <ServerTrace trace={serverTrace} />} {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> </React.Fragment>
); );
} }

View File

@ -366,6 +366,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
RuukuLada: { RuukuLada: {
name: "RuukuLada", name: "RuukuLada",
id: 119705748346241027n, id: 119705748346241027n,
},
blahajZip: {
name: "blahaj.zip",
id: 683954422241427471n,
},
archeruwu: {
name: "archer_uwu",
id: 160068695383736320n
} }
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);

View File

@ -65,8 +65,13 @@ export interface Menu {
id: string; id: string;
interactive?: boolean; interactive?: boolean;
}>; }>;
// TODO: Type me MenuSliderControl: RC<{
MenuSliderControl: RC<any>; minValue: number,
maxValue: number,
value: number,
onChange(value: number): void,
renderValue?(value: number): string,
}>;
} }
export interface ContextMenuApi { export interface ContextMenuApi {