Compare commits

...

49 commits

Author SHA1 Message Date
camila
d6aa6c29b0
Merge e7dd0638a8 into bcfef05a8a 2024-09-15 21:45:33 -07: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
camila
e7dd0638a8
Merge branch 'dev' into prbranch 2024-06-25 14:57:42 -05:00
camila
899bfc6a23
Merge pull request #2 from x3rt/author
Add x3rt to authors
2024-06-24 13:45:45 -05:00
x3rt
bfbb1f1aad Add x3rt to authors 2024-06-24 10:35:24 -06:00
camila314
a572da18f5 Ignore case toggle 2024-06-24 02:46:52 -05:00
camila314
bac0ac1b6b do good changes 2024-06-24 01:56:14 -05:00
camila
31b18b221d
Merge branch 'dev' into prbranch 2024-06-23 20:20:24 -05:00
camila314
b33e203775 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-06-23 14:33:16 -05:00
camila314
4e7e09f4ff review changes 2024-06-23 14:33:11 -05:00
camila
ae5c1014cb
Merge branch 'dev' into main 2024-06-23 14:13:52 -05:00
camila314
5c843bdbb7 make work again 2024-06-21 14:07:01 -05:00
camila
d3a6f71b1d
Merge branch 'Vendicated:main' into main 2024-06-19 19:05:54 -05:00
camila314
8b32786678 remove useless comment 2024-06-18 00:03:59 -05:00
camila314
f496724422 remove console log 2024-06-17 23:59:29 -05:00
camila314
91f6541d8a Update 2024-06-17 23:42:50 -05:00
camila314
b9b037bdb9 merge 2024-06-17 23:39:49 -05:00
camila
7934318967
Merge branch 'Vendicated:main' into main 2024-04-19 00:54:03 -05:00
camila
37c1172311
Merge branch 'Vendicated:main' into main 2024-04-03 11:24:26 -05:00
camila
aeb3096e77
Merge branch 'Vendicated:main' into main 2024-03-24 19:55:20 -05:00
camila
57d22f9b6e
Merge branch 'Vendicated:main' into main 2024-03-08 15:28:57 -06:00
camila
e157097c7e
Merge branch 'Vendicated:main' into main 2024-01-31 10:13:13 -06:00
camila
db2b08d2fb
Merge branch 'Vendicated:main' into main 2024-01-21 09:31:44 -06:00
camila314
0e9c1ebea8 embed support 2024-01-04 12:54:08 -06:00
camila
79118251c8
Merge branch 'Vendicated:main' into main 2023-12-31 12:32:34 -06:00
camila
1d3579d788
Merge branch 'Vendicated:main' into main 2023-12-15 12:26:21 -06:00
camila314
b5804d548c someone made destructuring with strings not work lol 2023-11-28 02:09:34 -06:00
camila314
c7ed529a6c add ignore bots 2023-11-26 08:32:37 -06:00
camila314
42b09a3d0b bring back destructuring 2023-11-25 17:18:42 -06:00
camila
2870bd7003
Merge branch 'Vendicated:main' into main 2023-11-25 17:16:00 -06:00
camila314
59902a3729 no more mapMangledModule 2023-11-25 17:13:19 -06:00
camila314
5b647c307d fix small crash bug 2023-11-25 11:48:10 -06:00
camila314
5240085285 remove default from find/replace 2023-11-21 06:47:18 -06:00
camila314
5648b16107 brand new inbox feature 2023-11-20 21:21:23 -06:00
camila314
808c664d93 no longer crashes on invalid regex 2023-11-19 18:25:11 -06:00
camila
4b4ad65c6e
Merge branch 'main' into main 2023-11-17 01:13:27 -06:00
camila
3cfe1dd9cc
Update src/plugins/keywordNotify/index.tsx
Co-authored-by: AutumnVN <autumnvnchino@gmail.com>
2023-11-17 00:35:11 -06:00
camila314
311c4bee3c my bad. this time i actually fixed it 2023-11-16 11:14:39 -06:00
camila314
8ab224562a fix newly generated rule causing mass pings 2023-11-15 23:06:49 -06:00
camila314
c0fb89496f KeywordNotify now highlights messages 2023-11-11 22:36:37 -06:00
camila
99036977bb
Update src/plugins/keywordNotify/index.tsx
Co-authored-by: AutumnVN <autumnvnchino@gmail.com>
2023-11-11 20:36:19 -06:00
camila314
473d147854 Merge remote-tracking branch 'refs/remotes/origin/main' 2023-11-11 17:54:28 -06:00
camila314
defa669d0b i have to have a readme 2023-11-11 17:54:21 -06:00
camila
6d4d38c2d5
Update src/plugins/keywordNotify/index.tsx
Co-authored-by: ant0n <antonickadoo@gmail.com>
2023-11-11 17:50:25 -06:00
camila
2792158eb5
Merge branch 'main' into main 2023-11-11 10:15:16 -06:00
camila314
c8df5e044b update keyword notify 2023-11-07 18:22:33 -06:00
camila314
32062fbc05 Keyword Notify 2023-11-05 20:17:58 -06:00
9 changed files with 531 additions and 127 deletions

View file

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

View file

@ -11,37 +11,11 @@ import type { TrackData } from ".";
const exec = promisify(execFile); 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[]) { async function applescript(cmds: string[]) {
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat()); const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
return stdout; 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 { interface RemoteData {
appleMusicLink?: string, appleMusicLink?: string,
songLink?: string, songLink?: string,
@ -51,6 +25,24 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null; 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; }) { async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
if (id === cachedRemoteData?.id) { if (id === cachedRemoteData?.id) {
if ("data" in cachedRemoteData) return cachedRemoteData.data; if ("data" in cachedRemoteData) return cachedRemoteData.data;
@ -58,21 +50,39 @@ async function fetchRemoteData({ id, name, artist, album }: { id: string, name:
} }
try { try {
const [songData, artistData] = await Promise.all([ const dataUrl = new URL("https://amp-api-edge.music.apple.com/v1/catalog/us/search");
fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()), dataUrl.searchParams.set("platform", "web");
fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json()) 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 token = await getToken();
const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512"); const songData = await fetch(dataUrl, {
const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512"); 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 = { cachedRemoteData = {
id, 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; return cachedRemoteData.data;
} catch (e) { } catch (e) {
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e); console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);

View file

@ -0,0 +1,3 @@
# KeywordNotify
Allows for custom regex-defined keywords to notify the user exactly how a ping would. Adds a custom inbox for viewing keywords next to the mentions.

View file

@ -0,0 +1,456 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated, camila314, and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Select, Switch, SelectedChannelStore, TabBar, TextInput, UserStore, UserUtils, useState } from "@webpack/common";
import { classNameFactory } from "@api/Styles";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { DeleteIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { Message, User } from "discord-types/general/index.js";
import { useForceUpdater } from "@utils/react";
type KeywordEntry = { regex: string, listIds: Array<string>, listType: ListType, ignoreCase: boolean };
let keywordEntries: Array<KeywordEntry> = [];
let currentUser: User;
let keywordLog: Array<any> = [];
const MenuHeader = findByCodeLazy(".sv)()?(0,");
const Popout = findByCodeLazy(".loadingMore&&null==");
const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout");
const createMessageRecord = findByCodeLazy("THREAD_CREATED?[]:(0,");
const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries";
const KEYWORD_LOG_KEY = "KeywordNotify_log";
const cl = classNameFactory("vc-keywordnotify-");
async function addKeywordEntry(forceUpdate: () => void) {
keywordEntries.push({ regex: "", listIds: [], listType: ListType.BlackList, ignoreCase: false });
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
forceUpdate();
}
async function removeKeywordEntry(idx: number, forceUpdate: () => void) {
keywordEntries.splice(idx, 1);
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
forceUpdate();
}
function safeMatchesRegex(str: string, regex: string, flags: string) {
try {
return str.match(new RegExp(regex, flags));
} catch {
return false;
}
}
enum ListType {
BlackList = "BlackList",
Whitelist = "Whitelist"
}
function highlightKeywords(str: string, entries: Array<KeywordEntry>) {
let regexes: Array<RegExp>;
try {
regexes = entries.map(e => new RegExp(e.regex, "g" + (e.ignoreCase ? "i" : "")));
} catch (err) {
return [str];
}
const matches = regexes.map(r => str.match(r)).flat().filter(e => e != null);
if (matches.length == 0) {
return [str];
}
const idx = str.indexOf(matches[0]);
return [
<span>{str.substring(0, idx)}</span>,
<span className="highlight">{matches[0]}</span>,
<span>{str.substring(idx + matches[0].length)}</span>
];
}
function Collapsible({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Button
onClick={() => setIsOpen(!isOpen)}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
className={cl("collapsible")}>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginLeft: "auto", color: "var(--text-muted)", paddingRight: "5px" }}>{isOpen ? "▼" : "▶"}</div>
<Forms.FormTitle tag="h4">{title}</Forms.FormTitle>
</div>
</Button>
{isOpen && children}
</div>
);
}
function ListedIds({ listIds, setListIds }) {
const update = useForceUpdater();
const [values] = useState(listIds);
async function onChange(e: string, index: number) {
values[index] = e;
setListIds(values);
update();
}
const elements = values.map((currentValue: string, index: number) => {
return (
<Flex flexDirection="row" style={{ marginBottom: "5px" }}>
<div style={{ flexGrow: 1 }}>
<TextInput
placeholder="ID"
spellCheck={false}
value={currentValue}
onChange={e => onChange(e, index)}
/>
</div>
<Button
onClick={() => {
values.splice(index, 1);
setListIds(values);
update();
}}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
className={cl("delete")}>
<DeleteIcon/>
</Button>
</Flex>
);
});
return (
<>
{elements}
</>
);
}
function ListTypeSelector({ listType, setListType }) {
return (
<Select
options={[
{ label: "Whitelist", value: ListType.Whitelist },
{ label: "Blacklist", value: ListType.BlackList }
]}
placeholder={"Select a list type"}
isSelected={v => v === listType}
closeOnSelect={true}
value={listType}
select={setListType}
serialize={v => v}
/>
);
}
function KeywordEntries() {
const update = useForceUpdater();
const [values] = useState(keywordEntries);
async function setRegex(index: number, value: string) {
keywordEntries[index].regex = value;
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
update();
}
async function setListType(index: number, value: ListType) {
keywordEntries[index].listType = value;
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
update();
}
async function setListIds(index: number, value: Array<string>) {
keywordEntries[index].listIds = value ?? [];
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
update();
}
const elements = keywordEntries.map((entry, i) => {
return (
<>
<Collapsible title={`Keyword Entry ${i + 1}`}>
<Flex flexDirection="row">
<div style={{ flexGrow: 1 }}>
<TextInput
placeholder="example|regex"
spellCheck={false}
value={values[i].regex}
onChange={e => setRegex(i, e)}
/>
</div>
<Button
onClick={() => removeKeywordEntry(i, update)}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
className={cl("delete")}>
<DeleteIcon/>
</Button>
</Flex>
<Switch
value={values[i].ignoreCase}
onChange={() => {
values[i].ignoreCase = !values[i].ignoreCase;
update();
}}
style={{ marginTop: "0.5em", marginRight: "40px" }}
>
Ignore Case
</Switch>
<Forms.FormDivider className={[Margins.top8, Margins.bottom8].join(" ") }/>
<Forms.FormTitle tag="h5">Whitelist/Blacklist</Forms.FormTitle>
<Flex flexDirection="row">
<div style={{ flexGrow: 1 }}>
<ListedIds listIds={values[i].listIds} setListIds={e => setListIds(i, e)}/>
</div>
</Flex>
<div className={[Margins.top8, Margins.bottom8].join(" ") }/>
<Flex flexDirection="row">
<Button onClick={() => {
values[i].listIds.push("");
update();
}}>Add ID</Button>
<div style={{ flexGrow: 1 }}>
<ListTypeSelector listType={values[i].listType} setListType={e => setListType(i, e)}/>
</div>
</Flex>
</Collapsible>
</>
);
});
return (
<>
{elements}
<div><Button onClick={() => addKeywordEntry(update)}>Add Keyword Entry</Button></div>
</>
);
}
const settings = definePluginSettings({
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Ignore messages from bots",
default: true
},
keywords: {
type: OptionType.COMPONENT,
component: () => <KeywordEntries/>
}
});
export default definePlugin({
name: "KeywordNotify",
authors: [Devs.camila314, Devs.x3rt],
description: "Sends a notification if a given message matches certain keywords or regexes",
settings,
patches: [
{
find: "Dispatch.dispatch(...) called without an action type",
replacement: {
match: /}_dispatch\((\i),\i\){/,
replace: "$&$1=$self.modify($1);"
}
},
{
find: "Messages.UNREADS_TAB_LABEL}",
replacement: {
match: /\i\?\(0,\i\.jsxs\)\(\i\.TabBar\.Item/,
replace: "$self.keywordTabBar(),$&"
}
},
{
find: "location:\"RecentsPopout\"",
replacement: {
match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?setTab:(\i),onJump:(\i),badgeState:\i,closePopout:(\i)/,
replace: ": $1 === 5 ? $self.tryKeywordMenu($2, $3, $4) $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /function (\i)\(\i\){let{message:\i,gotoMessage/,
replace: "$self.renderMsg = $1; $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /onClick:\(\)=>(\i\.\i\.deleteRecentMention\((\i)\.id\))/,
replace: "onClick: () => $2._keyword ? $self.deleteKeyword($2.id) : $1"
}
}
],
async start() {
keywordEntries = await DataStore.get(KEYWORD_ENTRIES_KEY) ?? [];
currentUser = UserStore.getCurrentUser();
this.onUpdate = () => null;
(await DataStore.get(KEYWORD_LOG_KEY) ?? []).map(e => JSON.parse(e)).forEach(e => {
this.addToLog(e);
});
},
applyKeywordEntries(m: Message) {
let matches = false;
for (let entry of keywordEntries) {
if (entry.regex === "") {
return;
}
let listed = entry.listIds.some(id => id === m.channel_id || id === m.author.id);
if (!listed) {
const channel = ChannelStore.getChannel(m.channel_id);
if (channel != null) {
listed = entry.listIds.some(id => id === channel.guild_id);
}
}
const whitelistMode = entry.listType === ListType.Whitelist;
if (!whitelistMode && listed) {
return;
}
if (whitelistMode && !listed) {
return;
}
if (settings.store.ignoreBots && m.author.bot && (!whitelistMode || !entry.listIds.includes(m.author.id))) {
return;
}
const flags = entry.ignoreCase ? "i" : "";
if (safeMatchesRegex(m.content, entry.regex, flags)) {
matches = true;
}
for (const embed of m.embeds as any) {
if (safeMatchesRegex(embed.description, entry.regex, flags) || safeMatchesRegex(embed.title, entry.regex, flags)) {
matches = true;
} else if (embed.fields != null) {
for (const field of embed.fields as Array<{ name: string, value: string }>) {
if (safeMatchesRegex(field.value, entry.regex, flags) || safeMatchesRegex(field.name, entry.regex, flags)) {
matches = true;
}
}
}
}
}
if (matches) {
m.mentions.push(currentUser);
if (m.author.id !== currentUser.id)
this.addToLog(m);
}
},
addToLog(m: Message) {
if (m == null || keywordLog.some(e => e.id === m.id))
return;
DataStore.get(KEYWORD_LOG_KEY).then(log => {
DataStore.set(KEYWORD_LOG_KEY, [...log, JSON.stringify(m)]);
});
const thing = createMessageRecord(m);
keywordLog.push(thing);
keywordLog.sort((a, b) => b.timestamp - a.timestamp);
if (keywordLog.length > 50)
keywordLog.pop();
this.onUpdate();
},
deleteKeyword(id) {
keywordLog = keywordLog.filter(e => e.id !== id);
this.onUpdate();
},
keywordTabBar() {
return (
<TabBar.Item className="vc-settings-tab-bar-item" id={5}>
Keywords
</TabBar.Item>
);
},
tryKeywordMenu(setTab, onJump, closePopout) {
const header = (
<MenuHeader tab={5} setTab={setTab} closePopout={closePopout} badgeState={{ badgeForYou: false }}/>
);
const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
const [tempLogs, setKeywordLog] = useState(keywordLog);
this.onUpdate = () => {
const newLog = Array.from(keywordLog);
setKeywordLog(newLog);
};
const messageRender = (e, t) => {
e._keyword = true;
e.customRenderedContent = {
content: highlightKeywords(e.content, keywordEntries)
};
const msg = this.renderMsg({
message: e,
gotoMessage: t,
dismissible: true
});
return [msg];
};
return (
<>
<Popout
className={recentMentionsPopoutClass.recentMentionsPopout}
renderHeader={() => header}
renderMessage={messageRender}
channel={channel}
onJump={onJump}
onFetch={() => null}
onCloseMessage={this.deleteKeyword}
loadMore={() => null}
messages={tempLogs}
renderEmptyState={() => null}
/>
</>
);
},
modify(e) {
if (e.type === "MESSAGE_CREATE") {
this.applyKeywordEntries(e.message);
} else if (e.type === "LOAD_MESSAGES_SUCCESS") {
for (let msg = 0; msg < e.messages.length; ++msg) {
this.applyKeywordEntries(e.messages[msg]);
}
}
return e;
}
});

View file

@ -0,0 +1,16 @@
.vc-keywordnotify-delete:hover {
color: var(--status-danger);
}
.vc-keywordnotify-delete {
padding: 0;
color: var(--primary-400);
transition: color 0.2s ease-in-out;
}
.vc-keywordnotify-collapsible {
display: flex;
align-items: center;
padding: 8px;
cursor: pointer;
}

View file

@ -33,7 +33,7 @@ interface URLReplacementRule {
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant // Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = { const UrlReplacementRules: Record<string, URLReplacementRule> = {
spotify: { 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}`, replace: (_, type, id) => `spotify://${type}/${id}`,
description: "Open Spotify links in the Spotify app", description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/, 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 id: 841509053422632990n
}, },
F53: { F53: {
name: "F53", name: "Cassie (Code)",
id: 280411966126948353n id: 280411966126948353n
}, },
AutumnVN: { AutumnVN: {
@ -375,6 +375,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ProffDea", name: "ProffDea",
id: 609329952180928513n id: 609329952180928513n
}, },
camila314: {
name: "camila314",
id: 738592270617542716n
},
x3rt: {
name: "x3rt",
id: 131602100332396544n
},
UlyssesZhan: { UlyssesZhan: {
name: "UlyssesZhan", name: "UlyssesZhan",
id: 586808226058862623n id: 586808226058862623n