Compare commits

...

36 commits

Author SHA1 Message Date
vishnyanetchereshnya
332a3cddb2
Merge 0187e5bd96 into bcfef05a8a 2024-09-16 18:08:16 +09:00
Vendicated
bcfef05a8a
delete TimeBarAllActivites ~ now a stock feature 2024-09-14 17:22:29 +02:00
Cookie
f17b92c2fd
OpenInApp: support Spotify prerelease links (#2870) 2024-09-14 15:03:58 +00:00
Ryan Cao
292f7d71d3
AppleMusicRichPresence: fix metadata fetching (#2864) 2024-09-14 16:59:05 +02:00
vishnyanetchereshnya
0187e5bd96
Merge branch 'dev' into NotesSearcher 2024-07-14 17:00:06 +03:00
vishnyanetchereshnya
4d1cf661f5
bugfix 2024-07-14 16:59:05 +03:00
vishnyanetchereshnya
e41fed6372
lint fix 2024-07-12 08:06:22 +03:00
vishnyanetchereshnya
b0daabdf71
apply css only only if plugin enabled 2024-07-12 08:00:52 +03:00
vishnyanetchereshnya
fc3f7761fd
Merge branch 'dev' into NotesSearcher 2024-07-12 07:52:09 +03:00
vishnyanetchereshnya
1502ae688f
Update src/plugins/notesSearcher/components/NotesDataButton.tsx
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:51:59 +03:00
vishnyanetchereshnya
16d349e6ed
Update src/plugins/notesSearcher/components/NotesDataRow.tsx
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:51:45 +03:00
vishnyanetchereshnya
f26111ad27
Update src/plugins/notesSearcher/components/NotesDataRow.tsx
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:51:37 +03:00
vishnyanetchereshnya
69efbaa06d
Update src/plugins/notesSearcher/components/NotesDataModal.tsx
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:49:40 +03:00
vishnyanetchereshnya
4fae539fa4
Update src/plugins/notesSearcher/components/NotesDataModal.tsx
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:47:48 +03:00
vishnyanetchereshnya
f866808e23
Update src/plugins/notesSearcher/data.ts
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:43:10 +03:00
vishnyanetchereshnya
7bb0bc4700
Update src/plugins/notesSearcher/components/NotesDataRow.tsx
Co-authored-by: dolfies <jeyalfie47@gmail.com>
2024-07-12 07:42:38 +03:00
vishnyanetchereshnya
d0db4aa634
header fix 2024-07-09 05:20:54 +03:00
vishnyanetchereshnya
84aaac67b7
Merge branch 'dev' into NotesSearcher 2024-07-09 05:19:21 +03:00
vishnyanetchereshnya
381dd6e438
Update index.tsx 2024-07-09 05:19:15 +03:00
vishnyanetchereshnya
fec2638fd5
Delete src/plugins/notesSearcher/noteStore.d.ts 2024-06-24 22:45:31 +03:00
vishnyanetchereshnya
71e3a5cc99
Delete src/plugins/notesSearcher/components/CachePopout.tsx 2024-06-24 22:05:11 +03:00
vishnyanetchereshnya
6b88d4e4bd
Merge branch 'dev' into NotesSearcher 2024-06-24 22:02:55 +03:00
vishnyanetchereshnya
c2c5771b21
cache menu remove
cache menu removed -> users will be cached once modal open (try to fetch users from mutual guilds if not then by RestAPI with 1 sec delay (won't cause any api timeouts))
moved much part of css from styles.css into elements `style` prop
2024-06-24 22:02:37 +03:00
vishnyanetchereshnya
f0077caa3c
console.log remove 2024-06-21 13:18:22 +03:00
vishnyanetchereshnya
e4fa9320d6
bugfix 2024-06-21 13:17:25 +03:00
vishnyanetchereshnya
8c26c493f0
Delete src/plugins/_api/userSettingsDefinitions.ts 2024-06-21 13:07:02 +03:00
vishnyanetchereshnya
5f539208bf
Delete src/api/UserSettingDefinitions.ts 2024-06-21 13:06:43 +03:00
vishnyanetchereshnya
ce7e0e6c72
Merge branch 'dev' into NotesSearcher 2024-06-21 13:02:34 +03:00
vishnyanetchereshnya
b4cc9844d5
new way of users caching & receive notes in READY 2024-06-21 05:59:40 +03:00
vishnyanetchereshnya
f31805c02f
refactoring 2024-06-21 03:21:31 +03:00
vishnyanetchereshnya
96894c21a7
simple-header 2024-06-20 23:55:26 +03:00
vishnyanetchereshnya
a4408263a7
Update index.tsx 2024-06-20 23:53:36 +03:00
vishnyanetchereshnya
a5be1873ba
[Plugin] NotesSearcher
Allows you to open modal with all of your notes and search throught them by UserID, Note text and Global/Username if user is cached
2024-06-20 23:47:15 +03:00
Vendicated
a43d5d595a
Plugin Page: add indicator for excluded plugins 2024-06-20 19:56:14 +02:00
Nuckyz
ceaaf9ab8a
Reporter: Test mapMangledModule 2024-06-20 01:00:07 -03:00
Nuckyz
a01ee40591
Clean-up related additions to mangled exports 2024-06-19 23:50:04 -03:00
17 changed files with 1091 additions and 127 deletions

View file

@ -120,7 +120,7 @@ const settings = definePluginSettings({
stateString: {
type: OptionType.STRING,
description: "Activity state format string",
default: "{artist}"
default: "{artist} · {album}"
},
largeImageType: {
type: OptionType.SELECT,

View file

@ -11,37 +11,11 @@ import type { TrackData } from ".";
const exec = promisify(execFile);
// function exec(file: string, args: string[] = []) {
// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
// let stdout: string | null = null;
// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
// let stderr: string | null = null;
// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
// process.on("exit", code => { resolve({ code, stdout, stderr }); });
// process.on("error", err => reject(err));
// });
// }
async function applescript(cmds: string[]) {
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
return stdout;
}
function makeSearchUrl(type: string, query: string) {
const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
url.searchParams.set("types", type);
url.searchParams.set("limit", "1");
url.searchParams.set("term", query);
return url;
}
const requestOptions: RequestInit = {
headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
};
interface RemoteData {
appleMusicLink?: string,
songLink?: string,
@ -51,6 +25,24 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
const APPLE_MUSIC_TOKEN_REGEX = /\w+="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)",\w+="x-apple-jingle-correlation-key"/;
let cachedToken: string | undefined = undefined;
const getToken = async () => {
if (cachedToken) return cachedToken;
const html = await fetch("https://music.apple.com/").then(r => r.text());
const bundleUrl = new URL(html.match(APPLE_MUSIC_BUNDLE_REGEX)![1], "https://music.apple.com/");
const bundle = await fetch(bundleUrl).then(r => r.text());
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![1];
cachedToken = token;
return token;
};
async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
if (id === cachedRemoteData?.id) {
if ("data" in cachedRemoteData) return cachedRemoteData.data;
@ -58,21 +50,39 @@ async function fetchRemoteData({ id, name, artist, album }: { id: string, name:
}
try {
const [songData, artistData] = await Promise.all([
fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
]);
const dataUrl = new URL("https://amp-api-edge.music.apple.com/v1/catalog/us/search");
dataUrl.searchParams.set("platform", "web");
dataUrl.searchParams.set("l", "en-US");
dataUrl.searchParams.set("limit", "1");
dataUrl.searchParams.set("with", "serverBubbles");
dataUrl.searchParams.set("types", "songs");
dataUrl.searchParams.set("term", `${name} ${artist} ${album}`);
dataUrl.searchParams.set("include[songs]", "artists");
const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
const token = await getToken();
const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
const songData = await fetch(dataUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"authorization": `Bearer ${token}`,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"origin": "https://music.apple.com",
},
})
.then(r => r.json())
.then(data => data.results.song.data[0]);
cachedRemoteData = {
id,
data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
data: {
appleMusicLink: songData.attributes.url,
songLink: `https://song.link/i/${songData.id}`,
albumArtwork: songData.attributes.artwork.url.replace("{w}x{h}", "512x512"),
artistArtwork: songData.relationships.artists.data[0].attributes.artwork.url.replace("{w}x{h}", "512x512"),
}
};
return cachedRemoteData.data;
} catch (e) {
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);

View file

@ -0,0 +1,7 @@
# NotesSearcher
Allows you to open a modal with all of your notes and search through them by user ID, note text, and username
## Preview
![preview](https://i.imgur.com/yBwhcx6.png)

View file

@ -0,0 +1,76 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { LazyComponent } from "@utils/lazyReact";
import { React } from "@webpack/common";
export const NotesDataIcon = LazyComponent(() => React.memo(() => {
return (
<svg stroke="currentColor" width="24" height="24" viewBox="1 1 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M10.0002 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2839 19.7822 18.9076C20 18.4802 20 17.921 20 16.8031V14M16 5L10 11V14H13L19 8M16 5L19 2L22 5L19 8M16 5L19 8" />
</svg>
);
}));
export const SaveIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32">
<path fill="#fff" fill-rule="evenodd" clip-rule="evenodd" d="M18.1716 1C18.702 1 19.2107 1.21071 19.5858 1.58579L22.4142 4.41421C22.7893 4.78929 23 5.29799 23 5.82843V20C23 21.6569 21.6569 23 20 23H4C2.34315 23 1 21.6569 1 20V4C1 2.34315 2.34315 1 4 1H18.1716ZM4 3C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21L5 21L5 15C5 13.3431 6.34315 12 8 12L16 12C17.6569 12 19 13.3431 19 15V21H20C20.5523 21 21 20.5523 21 20V6.82843C21 6.29799 20.7893 5.78929 20.4142 5.41421L18.5858 3.58579C18.2107 3.21071 17.702 3 17.1716 3H17V5C17 6.65685 15.6569 8 14 8H10C8.34315 8 7 6.65685 7 5V3H4ZM17 21V15C17 14.4477 16.5523 14 16 14L8 14C7.44772 14 7 14.4477 7 15L7 21L17 21ZM9 3H15V5C15 5.55228 14.5523 6 14 6H10C9.44772 6 9 5.55228 9 5V3Z" />
</svg>
);
}));
export const DeleteIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32">
<path fill="#fff" d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z" />
</svg>
);
}));
export const RefreshIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32" fill="none">
<path stroke-linejoin="round" stroke-linecap="round" stroke="#fff" stroke-width="2" d="M21 3V8M21 8H16M21 8L18 5.29168C16.4077 3.86656 14.3051 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.2832 21 19.8675 18.008 20.777 14" />
</svg>
);
}));
export const PopupIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32">
<path fill="#fff" d="M10 5V3H5.375C4.06519 3 3 4.06519 3 5.375V18.625C3 19.936 4.06519 21 5.375 21H18.625C19.936 21 21 19.936 21 18.625V14H19V19H5V5H10Z" />
<path fill="#fff" d="M21 2.99902H14V4.99902H17.586L9.29297 13.292L10.707 14.706L19 6.41302V9.99902H21V2.99902Z" />
</svg>
);
}));
export const CrossIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32px" height="32px" viewBox="-3.2 -3.2 38.40 38.40">
<rect fill="#dc3545" x="-3.2" y="-3.2" width="38.40" height="38.40" rx="19.2" />
<path fill="#fff" d="M18.8,16l5.5-5.5c0.8-0.8,0.8-2,0-2.8l0,0C24,7.3,23.5,7,23,7c-0.5,0-1,0.2-1.4,0.6L16,13.2l-5.5-5.5 c-0.8-0.8-2.1-0.8-2.8,0C7.3,8,7,8.5,7,9.1s0.2,1,0.6,1.4l5.5,5.5l-5.5,5.5C7.3,21.9,7,22.4,7,23c0,0.5,0.2,1,0.6,1.4 C8,24.8,8.5,25,9,25c0.5,0,1-0.2,1.4-0.6l5.5-5.5l5.5,5.5c0.8,0.8,2.1,0.8,2.8,0c0.8-0.8,0.8-2.1,0-2.8L18.8,16z" />
</svg>
);
}));
export const ProblemIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32px" height="32px" viewBox="0 0 1024 1024">
<rect fill="#fbbd04" x="0" y="0" width="1024" height="1024" rx="512" />
<path fill="#fff" d="M512 254.08a140.16 140.16 0 0 0-140.672 139.392 32.128 32.128 0 0 0 64.32 0c0-42.112 33.536-75.136 76.352-75.136 42.112 0 76.352 34.56 76.352 76.992 0 16-22.912 38.976-43.2 59.2-30.592 30.592-65.28 65.28-65.28 111.744v45.888a32.128 32.128 0 1 0 64.256 0v-45.888c0-19.84 23.68-43.52 46.464-66.304 29.056-29.056 62.08-62.016 62.08-104.64A141.12 141.12 0 0 0 512 254.08z m-48.192 500.928a48.192 48.192 0 1 0 96.384 0 48.192 48.192 0 0 0-96.384 0z" />
</svg>
);
}));
export const SuccessIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32px" height="32px" viewBox="0.55 2.3 15.834375 15.834375">
<circle fill="#fff" cx="8.5" cy="10.25" r="6.5" />
<path fill="#28a745" d="M16.417 10.283A7.917 7.917 0 1 1 8.5 2.366a7.916 7.916 0 0 1 7.917 7.917zm-4.105-4.498a.791.791 0 0 0-1.082.29l-3.828 6.63-1.733-2.08a.791.791 0 1 0-1.216 1.014l2.459 2.952a.792.792 0 0 0 .608.285.83.83 0 0 0 .068-.003.791.791 0 0 0 .618-.393L12.6 6.866a.791.791 0 0 0-.29-1.081z" />
</svg>
);
}));

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { LazyComponent } from "@utils/react";
import { React } from "@webpack/common";
export const LoadingSpinner = LazyComponent(() => React.memo(() => {
return (
<div className={"vc-notes-searcher-modal-spinner-container"} style={{
width: "56px",
height: "56px",
margin: "12px",
}}>
<span className={"vc-notes-searcher-modal-spinner"} style={{
width: "56px",
height: "56px",
border: "5px solid #fff",
borderRadius: "50%",
display: "inline-block",
boxSizing: "border-box",
position: "relative",
animation: "vc-notes-searcher-pulse 1s linear infinite",
}} />
</div>
);
}));

View file

@ -0,0 +1,25 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { LazyComponent } from "@utils/react";
import { findExportedComponentLazy } from "@webpack";
import { React } from "@webpack/common";
import { NotesDataIcon } from "./Icons";
import { openNotesDataModal } from "./NotesDataModal";
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
export const OpenNotesDataButton = LazyComponent(() => React.memo(() => {
return (
<HeaderBarIcon
className="vc-notes-searcher-toolbox-button"
onClick={() => openNotesDataModal()}
tooltip={"View Notes"}
icon={NotesDataIcon}
/>
);
}));

View file

@ -0,0 +1,217 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import {
closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, openModal
} from "@utils/modal";
import { LazyComponent } from "@utils/react";
import { Button, React, RelationshipStore, Select, Text, TextInput, useCallback, useMemo, useReducer, useState } from "@webpack/common";
import { cacheUsers, getNotes, usersCache as usersCache$1 } from "../data";
import NotesDataRow from "./NotesDataRow";
const cl = classNameFactory("vc-notes-searcher-modal-");
const enum SearchStatus {
ALL,
FRIENDS,
BLOCKED,
}
const filterUser = (query: string, userId: string, userNotes: string) => {
if (query === "" || userId.includes(query)) return true;
query = query.toLowerCase();
const user = usersCache$1.get(userId);
return user && (
user.globalName?.toLowerCase().includes(query) || user.username.toLowerCase().includes(query)
) || userNotes.toLowerCase().includes(query);
};
// looks like a shit but I don't know better way to do it
// P.S. using `getNotes()` as deps for useMemo won't work due to object init outside of component
let RefreshNotesDataEx: () => void | undefined;
export const refreshNotesData = () => {
if (!RefreshNotesDataEx) return;
RefreshNotesDataEx();
};
export function NotesDataModal({ modalProps, close }: {
modalProps: ModalProps;
close(): void;
}) {
const [searchValue, setSearchValue] = useState({ query: "", status: SearchStatus.ALL });
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, query }));
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const [usersNotesData, refreshNotesData] = useReducer(() => {
return Object.entries(getNotes())
.map<[string, string]>(([userId, { note }]) => [userId, note])
.filter((([_, note]) => note && note !== ""));
},
Object.entries(getNotes())
.map<[string, string]>(([userId, { note }]) => [userId, note])
.filter((([_, note]) => note && note !== ""))
);
RefreshNotesDataEx = refreshNotesData;
const filteredNotes = useMemo(() => {
const { query, status } = searchValue;
if (query === "" && status === SearchStatus.ALL) {
return usersNotesData;
}
return usersNotesData
.filter(([userId, userNotes]) => {
switch (status) {
case SearchStatus.FRIENDS:
return RelationshipStore.isFriend(userId) && filterUser(query, userId, userNotes);
case SearchStatus.BLOCKED:
return RelationshipStore.isBlocked(userId) && filterUser(query, userId, userNotes);
default:
return filterUser(query, userId, userNotes);
}
});
}, [usersNotesData, searchValue]);
const [visibleNotesNum, setVisibleNotesNum] = useState(10);
const loadMore = useCallback(() => {
setVisibleNotesNum(prevNum => prevNum + 10);
}, []);
const visibleNotes = filteredNotes.slice(0, visibleNotesNum);
const canLoadMore = visibleNotesNum < filteredNotes.length;
return (
<ModalRoot className={cl("root")} {...modalProps}>
<ModalHeader className={cl("header")}>
<Text className={cl("header-text")} variant="heading-lg/semibold" style={{ whiteSpace: "nowrap", width: "fit-content", marginRight: "16px" }}>User Notes</Text>
<TextInput className={cl("header-input")} value={searchValue.query} onChange={onSearch} placeholder="Filter Notes (ID/Display Name/Username/Note Text)" style={{ width: "100% !important", marginRight: "16px" }} />
<div className={cl("header-user-type")} style={{ minWidth: "160px", marginRight: "16px" }}>
<Select
options={[
{ label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Friends", value: SearchStatus.FRIENDS },
{ label: "Show Blocked", value: SearchStatus.BLOCKED },
]}
serialize={String}
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
/>
</div>
<ModalCloseButton onClick={close} />
</ModalHeader>
<div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0", overflow: "hidden", height: "100%" }} className={cl("content-container")}>
{
modalProps.transitionState === 1 &&
<ModalContent className={cl("content")}>
{
!visibleNotes.length ? <NoNotes /> : (
<NotesDataContent
visibleNotes={visibleNotes}
canLoadMore={canLoadMore}
loadMore={loadMore}
refreshNotesData={refreshNotesData}
/>
)
}
</ModalContent>
}
</div>
</ModalRoot>
);
}
// looks like a shit but I don't know better way to do it
// P.S. using `usersCache` as deps for useMemo won't work due to object init outside of component
let RefreshUsersCacheEx: () => void | undefined;
export const refreshUsersCache = () => {
if (!RefreshUsersCacheEx) return;
RefreshUsersCacheEx();
};
const NotesDataContent = ({ visibleNotes, canLoadMore, loadMore, refreshNotesData }: {
visibleNotes: [string, string][];
canLoadMore: boolean;
loadMore(): void;
refreshNotesData(): void;
}) => {
if (!visibleNotes.length)
return <NoNotes />;
const [usersCache, refreshUsersCache] = useReducer(() => {
return new Map(usersCache$1);
}, usersCache$1);
RefreshUsersCacheEx = refreshUsersCache;
return (
<div className={cl("content-inner")} style={{ paddingTop: "16px", height: "fit-content" }}>
{
visibleNotes
.map(([userId, userNotes]) => {
return (
<NotesDataRow
key={userId}
userId={userId}
userNotes={userNotes}
usersCache={usersCache}
refreshNotesData={refreshNotesData}
/>
);
})
}
{
canLoadMore &&
<Button
className={cl("load-more")}
size={Button.Sizes.NONE}
style={{ marginTop: "16px", width: "100%", height: "32px" }}
onClick={() => loadMore()}
>
Load More
</Button>
}
</div>
);
};
const NoNotes = LazyComponent(() => React.memo(() => (
<div className={cl("no-notes")} style={{ textAlign: "center", display: "grid", placeContent: "center", height: "100%" }}>
<Text variant="text-lg/normal">
No Notes.
</Text>
</div>
)));
let fistTimeOpen = true;
export const openNotesDataModal = async () => {
if (fistTimeOpen) {
cacheUsers();
fistTimeOpen = false;
}
const key = openModal(modalProps => (
<NotesDataModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
};

View file

@ -0,0 +1,248 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { openPrivateChannel, openUserProfile } from "@utils/discord";
import { copyWithToast } from "@utils/misc";
import { Alerts, Avatar, Button, ContextMenuApi, Menu, React, Text, TextArea, Tooltip, useState } from "@webpack/common";
import { updateNote } from "../data";
import { UsersCache } from "../types";
import { DeleteIcon, PopupIcon, RefreshIcon, SaveIcon } from "./Icons";
import { LoadingSpinner } from "./LoadingSpinner";
const cl = classNameFactory("vc-notes-searcher-modal-");
export default ({ userId, userNotes: userNotesArg, refreshNotesData, usersCache }: {
userId: string;
userNotes: string;
refreshNotesData(): void;
usersCache: UsersCache;
}) => {
let userCache = usersCache.get(userId);
const pending = !userCache;
userCache ??= {
id: userId,
globalName: "Loading...",
username: "Loading...",
avatar: "https://cdn.discordapp.com/embed/avatars/4.png",
};
const [userNotes, setUserNotes] = useState(userNotesArg);
return (
<div
className={cl("user")}
style={{
width: "100%",
height: "80px",
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
backgroundColor: "var(--background-secondary)",
borderRadius: "12px",
boxSizing: "border-box",
}}
onContextMenu={event => {
ContextMenuApi.openContextMenu(event, () =>
<Menu.Menu
navId={cl("user-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="User Notes Data"
>
<Menu.MenuItem
id={cl("open-user-profile")}
label="Open User Profile"
action={() => openUserProfile(userId)}
/>
<Menu.MenuItem
id={cl("open-user-chat")}
label="Open User Chat"
action={() => openPrivateChannel(userId)}
/>
<Menu.MenuItem
id={cl("copy-user-id")}
label="Copy ID"
action={() => copyWithToast(userCache!.id)}
/>
{
!pending &&
(
<>
<Menu.MenuItem
id={cl("copy-user-globalname")}
label="Copy Display Name"
action={() => copyWithToast(userCache!.globalName ?? userCache!.username)}
/>
<Menu.MenuItem
id={cl("copy-user-username")}
label="Copy Username"
action={() => copyWithToast(userCache!.username)}
/>
<Menu.MenuItem
id={cl("copy-user-avatar")}
label="Copy Avatar URL"
action={() => copyWithToast(userCache!.avatar)}
/>
</>
)
}
<Menu.MenuItem
id={cl("copy-user-notes")}
label="Copy Note"
action={() => copyWithToast(userNotes)}
/>
</Menu.Menu>
);
}}
>
{
pending ? <LoadingSpinner /> :
<Avatar
className={cl("user-avatar")}
size="SIZE_56"
src={userCache.avatar}
/>
}
<div className={cl("user-info")} style={{
minWidth: "50px",
maxWidth: "275px",
width: "100%",
}}>
<Text className={cl("user-info-globalname")} variant="text-lg/bold" style={{
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
color: "#fff"
}}>{userCache.globalName}</Text>
<Text className={cl("user-info-username")} variant="text-md/normal" style={{
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
color: "#d3d3d3"
}}>{userCache.username}</Text>
<Text className={cl("user-info-id")} variant="text-md/normal" style={{
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
color: "#989898"
}}>{userCache.id}</Text>
</div>
<div className={cl("user-notes-container")} style={{
display: "grid",
gridTemplateColumns: "calc(100% - 86px) min-content",
alignItems: "center",
justifyContent: "flex-end",
flexGrow: "1",
paddingRight: "8px",
gap: "8px",
}}>
<TextArea
className={cl("user-text-area")}
style={{
width: "100%",
height: "100%",
}}
placeholder="Click to add a note"
value={userNotes}
onChange={setUserNotes}
spellCheck={false}
/>
<div className={cl("user-actions")} style={{
display: "grid",
gridTemplateColumns: "auto auto",
gridTemplateRows: "auto auto",
gap: "3px",
aspectRatio: "1 / 1",
height: "auto",
boxSizing: "border-box",
overflow: "visible !important",
}}>
<Tooltip text={"Save"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-save")}
size={Button.Sizes.NONE}
color={Button.Colors.GREEN}
style={{ width: "32px", height: "32px" }}
onClick={() => {
updateNote(userId, userNotes);
refreshNotesData();
}}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<SaveIcon />
</Button>
)}
</Tooltip>
<Tooltip text={"Delete"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-delete")}
size={Button.Sizes.NONE}
color={Button.Colors.RED}
style={{ width: "32px", height: "32px" }}
onClick={() => {
Alerts.show({
title: "Delete Notes",
body: `Are you sure you want to delete notes for ${pending ? userId : `${userCache!.globalName} (${userId})`}?`,
confirmColor: Button.Colors.RED,
confirmText: "Delete",
cancelText: "Cancel",
onConfirm: () => {
updateNote(userId, "");
refreshNotesData();
},
});
}}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<DeleteIcon />
</Button>
)}
</Tooltip>
<Tooltip text={"Undo text area changes"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-refresh")}
size={Button.Sizes.NONE}
color={Button.Colors.LINK}
style={{ width: "32px", height: "32px" }}
onClick={() => setUserNotes(userNotesArg)}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<RefreshIcon />
</Button>
)}
</Tooltip>
<Tooltip text={"Open User Profile"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-popup")}
size={Button.Sizes.NONE}
color={Button.Colors.PRIMARY}
style={{ width: "32px", height: "32px" }}
onClick={async () => {
openUserProfile(userId);
}}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<PopupIcon />
</Button>
)}
</Tooltip>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,153 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Constants, FluxDispatcher, GuildStore, RestAPI, SnowflakeUtils, UserStore, UserUtils } from "@webpack/common";
import { waitForStore } from "webpack/common/internal";
import { refreshNotesData, refreshUsersCache } from "./components/NotesDataModal";
import * as t from "./types";
let NoteStore: t.NoteStore;
waitForStore("NoteStore", s => NoteStore = s);
export const getNotes = () => {
return NoteStore.getNotes();
};
export const onNoteUpdate = () => {
refreshNotesData();
};
export const updateNote = (userId: string, note: string | null) => {
RestAPI.put({
url: Constants.Endpoints.NOTE(userId),
body: { note: note },
oldFormErrors: true
});
};
export const usersCache: t.UsersCache = new Map();
export const onUserUpdate = ({ user }: { user: t.User; }) => {
if (!getNotes()[user.id]) return;
// doesn't have .getAvatarURL
const userFromStore = UserStore.getUser(user.id);
if (!userFromStore) return;
cacheUser(userFromStore);
};
const fetchUser = async (userId: string) => {
for (let _ = 0; _ < 5; _++) {
try {
return await UserUtils.getUser(userId);
} catch (error: any) {
const wait = error?.body?.retry_after;
if (!wait) return;
await new Promise(resolve => setTimeout(resolve, wait * 1000 + 100));
}
}
};
const cacheUser = (user: t.User) => {
usersCache.set(user.id, {
id: user.id,
globalName: user.globalName ?? user.username,
username: user.username,
avatar: user.getAvatarURL(void 0, void 0, false),
});
};
export const cacheUsers = async () => {
const toRequest: string[] = [];
for (const userId of Object.keys(getNotes())) {
const user = UserStore.getUser(userId);
if (user) {
cacheUser(user);
continue;
}
toRequest.push(userId);
}
if (usersCache.size >= Object.keys(getNotes()).length) {
return;
}
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
const allGuildIds = Object.keys(GuildStore.getGuilds());
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
const processed = new Set<string>();
const callback = async ({ chunks }) => {
for (const chunk of chunks) {
const { nonce, members }: {
nonce: string;
members: {
user: t.User;
}[];
} = chunk;
if (nonce !== sentNonce) {
return;
}
members.forEach(({ user }) => {
if (processed.has(user.id)) return;
processed.add(user.id);
cacheUser(UserStore.getUser(user.id));
});
refreshUsersCache();
if (--count === 0) {
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
const userIds = Object.keys(getNotes());
if (usersCache.size !== userIds.length) {
for (const userId of userIds) {
if (usersCache.has(userId)) continue;
await new Promise(resolve => setTimeout(resolve, 1000));
const user = await fetchUser(userId);
if (user) {
cacheUser(user);
refreshUsersCache();
}
}
} else
refreshUsersCache();
}
}
};
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
for (let i = 0; i < toRequest.length; i += 100) {
FluxDispatcher.dispatch({
type: "GUILD_MEMBERS_REQUEST",
guildIds: allGuildIds,
userIds: toRequest.slice(i, i + 100),
nonce: sentNonce,
});
}
};

View file

@ -0,0 +1,128 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { OpenNotesDataButton } from "./components/NotesDataButton";
import { getNotes, onNoteUpdate, onUserUpdate } from "./data";
import settings from "./settings";
import styles from "./styles.css?managed";
import { Notes } from "./types";
export default definePlugin({
name: "NotesSearcher",
description: "Allows you to open a modal with all of your notes and search through them by user ID, note text, and username",
authors: [Devs.Vishnya],
settings,
patches: [
{
find: "toolbar:function",
replacement: {
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/,
replace: "$1$self.addToolBarButton(arguments[0]);$2",
},
},
{
find: '="NoteStore",',
replacement: [
{
match: /getNote\(\i\){return (\i)/,
replace: "getNotes(){return $1}$&",
},
// not sure it won't break anything but should be fine
{
match: /CONNECTION_OPEN:\i,OVERLAY_INITIALIZE:\i,/,
replace: "",
},
],
},
{
find: ".REQUEST_GUILD_MEMBERS",
replacement: {
match: /\.send\(8,{(?!nonce)/,
replace: "$&nonce:arguments[1].nonce,",
},
},
{
find: "GUILD_MEMBERS_REQUEST:",
replacement: {
match: /presences:!!(\i)\.presences(?!,nonce)/,
replace: "$&,nonce:$1.nonce",
},
},
{
find: ".not_found",
replacement: {
match: /notFound:(\i)\.not_found(?!,nonce)/,
replace: "$&,nonce:$1.nonce",
},
},
{
find: "[IDENTIFY]",
replacement: {
match: /capabilities:(\i\.\i),/,
replace: "capabilities:$1&~1,",
},
},
{
find: "_handleDispatch",
replacement: {
match: /let \i=(\i).session_id;/,
replace: "$&$self.ready($1);",
},
},
],
start: async () => {
FluxDispatcher.subscribe("USER_NOTE_UPDATE", onNoteUpdate);
FluxDispatcher.subscribe("USER_UPDATE", onUserUpdate);
enableStyle(styles);
},
stop: () => {
FluxDispatcher.unsubscribe("USER_NOTE_UPDATE", onNoteUpdate);
FluxDispatcher.unsubscribe("USER_UPDATE", onUserUpdate);
disableStyle(styles);
},
ready: ({ notes }: { notes: { [userId: string]: string; }; }) => {
const notesFromStore = getNotes();
for (const userId of Object.keys(notesFromStore)) {
delete notesFromStore[userId];
}
Object.assign(notesFromStore, Object.entries(notes).reduce((newNotes, [userId, note]) => {
newNotes[userId] = {
note,
loading: false,
};
return newNotes;
}, {} as Notes));
},
addToolBarButton: (children: { toolbar: React.ReactNode[] | React.ReactNode; }) => {
if (Array.isArray(children.toolbar))
return children.toolbar.push(
<ErrorBoundary noop={true}>
<OpenNotesDataButton />
</ErrorBoundary>
);
children.toolbar = [
<ErrorBoundary noop={true}>
<OpenNotesDataButton />
</ErrorBoundary>,
children.toolbar,
];
},
});

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
export default definePluginSettings({
startupCache: {
default: false,
type: OptionType.BOOLEAN,
description: "Cache all users on startup",
},
});

View file

@ -0,0 +1,107 @@
.vc-notes-searcher-toolbox-button svg {
color: var(--interactive-normal);
}
.vc-notes-searcher-modal-user-actions * svg {
width: 32px !important;
height: 32px !important;
}
.vc-notes-searcher-toolbox-button:hover svg,
.vc-notes-searcher-toolbox-button[class*="selected"] svg {
color: var(--interactive-active);
}
.vc-notes-searcher-modal-root {
min-height: 75vh;
max-height: 75vh;
min-width: 70vw;
max-width: 70vw;
}
.vc-notes-searcher-modal-header-input {
width: 100%;
margin-right: 16px;
}
.vc-notes-searcher-modal-content {
padding-bottom: 16px;
height: 100%;
}
.vc-notes-searcher-modal-content div[aria-hidden="true"] {
display: none;
}
.vc-notes-searcher-modal-content-inner > *:not(:last-child) {
margin-bottom: 8px;
}
.vc-notes-searcher-modal-user:hover:not(
:has(
.vc-notes-searcher-modal-user-text-area:hover,
.vc-notes-searcher-modal-user-actions:hover
)
) {
background-color: var(--background-secondary-alt);
}
.vc-notes-searcher-modal-user-avatar {
aspect-ratio: 1 / 1;
margin: 12px;
}
.vc-notes-searcher-modal-user-actions * div:has(svg) {
width: 32px !important;
height: 32px !important;
overflow: visible !important;
}
.vc-notes-searcher-modal-user-notes-container
*
div:has(.vc-notes-searcher-modal-user-text-area) {
height: 67px;
}
.vc-notes-searcher-modal-user-text-area {
width: 100%;
height: 100%;
}
.vc-notes-searcher-modal-spinner::after {
content: "";
position: absolute;
width: 56px;
height: 56px;
border: 5px solid #fff;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
animation: vc-notes-searcher-scale-up 1s linear infinite;
}
@keyframes vc-notes-searcher-scale-up {
0% {
transform: translate(-50%, -50%) scale(0);
}
60%,
100% {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes vc-notes-searcher-pulse {
0%,
60%,
100% {
transform: scale(0.9);
}
80% {
transform: scale(1.1);
}
}

37
src/plugins/notesSearcher/types.d.ts vendored Normal file
View file

@ -0,0 +1,37 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { FluxStore } from "@webpack/types";
import { User as User$1 } from "discord-types/general";
export type Note = {
loading: boolean;
note: string;
};
export type Notes = {
[userId: string]: Note;
};
export class NoteStore extends FluxStore {
getNotes(): Notes;
getNote(userId: string): Note;
}
export type User = User$1 & {
globalName?: string;
};
export type Dispatch = ReturnType<typeof useState<any>>[1];
export type UserCache = {
id: string;
globalName?: string;
username: string;
avatar: string;
};
export type UsersCache = Map<string, UserCache>;

View file

@ -33,7 +33,7 @@ interface URLReplacementRule {
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = {
spotify: {
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode|prerelease)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `spotify://${type}/${id}`,
description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,

View file

@ -1,5 +0,0 @@
# TimeBarAllActivities
Adds the Spotify time bar to all activities if they have start and end timestamps.
![](https://github.com/user-attachments/assets/9fbbe33c-8218-43c9-8b8d-f907a4e809fe)

View file

@ -1,84 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { RequiredDeep } from "type-fest";
interface Activity {
timestamps?: ActivityTimestamps;
}
interface ActivityTimestamps {
start?: string;
end?: string;
}
interface TimebarComponentProps {
activity: Activity;
}
const ActivityTimeBar = findComponentByCodeLazy<ActivityTimestamps>(".bar", ".progress", "(100*");
function isActivityTimestamped(activity: Activity): activity is RequiredDeep<Activity> {
return activity.timestamps != null && activity.timestamps.start != null && activity.timestamps.end != null;
}
export const settings = definePluginSettings({
hideActivityDetailText: {
type: OptionType.BOOLEAN,
description: "Hide the large title text next to the activity",
default: true,
},
hideActivityTimerBadges: {
type: OptionType.BOOLEAN,
description: "Hide the timer badges next to the activity",
default: true,
}
});
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.fawn, Devs.niko],
settings,
patches: [
{
find: ".gameState,children:",
replacement: [
// Insert Spotify time bar component
{
match: /\(0,.{0,30}activity:(\i),className:\i\.badges\}\)/g,
replace: "$&,$self.TimebarComponent({activity:$1})"
},
// Hide the large title on listening activities, to make them look more like Spotify (also visible from hovering over the large icon)
{
match: /(\i).type===(\i\.\i)\.WATCHING/,
replace: "($self.settings.store.hideActivityDetailText&&$self.isActivityTimestamped($1)&&$1.type===$2.LISTENING)||$&"
}
]
},
// Hide the "badge" timers that count the time since the activity starts
{
find: ".TvIcon).otherwise",
replacement: {
match: /null!==\(\i=null===\(\i=(\i)\.timestamps\).{0,50}created_at/,
replace: "($self.settings.store.hideActivityTimerBadges&&$self.isActivityTimestamped($1))?null:$&"
}
}
],
isActivityTimestamped,
TimebarComponent: ErrorBoundary.wrap(({ activity }: TimebarComponentProps) => {
if (!isActivityTimestamped(activity)) return null;
return <ActivityTimeBar start={activity.timestamps.start} end={activity.timestamps.end} />;
}, { noop: true })
});

View file

@ -268,7 +268,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
id: 841509053422632990n
},
F53: {
name: "F53",
name: "Cassie (Code)",
id: 280411966126948353n
},
AutumnVN: {