Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35509f7412 | ||
|
|
3abb5fcda8 | ||
|
|
ec6fbb190f | ||
|
|
2a52efbd97 | ||
|
|
aa52e1a42b | ||
|
|
8411026c51 | ||
|
|
8ec5b0a8d8 | ||
|
|
4c315b6886 | ||
|
|
c2d7b68950 | ||
|
|
c2fbcec3bd | ||
|
|
619b0ef858 | ||
|
|
3bbf885146 | ||
|
|
34fa7cba20 | ||
|
|
2f6dfd9bee | ||
|
|
0e75d8a42f | ||
|
|
bef4b01382 | ||
| 09ed2c78ba | |||
| 82f668ab92 | |||
| a202904f7d | |||
| f4888f29f1 | |||
| 826e774af1 | |||
| 9051269657 | |||
| 2a87063176 | |||
| 741af9eb43 | |||
| acccc963ae | |||
| 77084203c6 |
244 changed files with 3553 additions and 4593 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -22,4 +22,4 @@ lerna-debug.log*
|
||||||
src/userplugins
|
src/userplugins
|
||||||
|
|
||||||
ExtensionCache/
|
ExtensionCache/
|
||||||
/settings
|
settings/
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
|
@ -13,14 +13,11 @@
|
||||||
"typescript.format.semicolons": "insert",
|
"typescript.format.semicolons": "insert",
|
||||||
"typescript.preferences.quoteStyle": "double",
|
"typescript.preferences.quoteStyle": "double",
|
||||||
"javascript.preferences.quoteStyle": "double",
|
"javascript.preferences.quoteStyle": "double",
|
||||||
|
|
||||||
"gitlens.remotes": [
|
"gitlens.remotes": [
|
||||||
{
|
{
|
||||||
"domain": "codeberg.org",
|
"domain": "codeberg.org",
|
||||||
"type": "Gitea"
|
"type": "Gitea"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"css.format.spaceAroundSelectorSeparator": true,
|
|
||||||
"[css]": {
|
|
||||||
"editor.defaultFormatter": "vscode.css-language-features"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,12 @@
|
||||||
|
# A fork. A fork. We're a fork.
|
||||||
|
|
||||||
|
Installing is the same as the Vencord devs laid out: [(Install)](https://docs.vencord.dev/installing/)
|
||||||
|
* Beyond that, be sure to clone *this* repository instead of their upstream one.
|
||||||
|
* `git clone https://git.dorkbutt.lol/dorkbutt/vencord`
|
||||||
|
* After completing the steps, go to Settings > Vencord > Plugins and search for
|
||||||
|
"FORKED - usrbg" and enable it. Be sure to tweak its settings!
|
||||||
|
|
||||||
|
|
||||||
# Vencord
|
# Vencord
|
||||||
|
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.13.0",
|
"version": "1.12.6",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Hint: https://docs.discord.food is an incredible resource and allows you to copy paste complete enums and interfaces
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
export const enum ActivityType {
|
|
||||||
PLAYING = 0,
|
|
||||||
STREAMING = 1,
|
|
||||||
LISTENING = 2,
|
|
||||||
WATCHING = 3,
|
|
||||||
CUSTOM_STATUS = 4,
|
|
||||||
COMPETING = 5,
|
|
||||||
HANG_STATUS = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum ActivityFlags {
|
|
||||||
INSTANCE = 1 << 0,
|
|
||||||
JOIN = 1 << 1,
|
|
||||||
/** @deprecated */
|
|
||||||
SPECTATE = 1 << 2,
|
|
||||||
/** @deprecated */
|
|
||||||
JOIN_REQUEST = 1 << 3,
|
|
||||||
SYNC = 1 << 4,
|
|
||||||
PLAY = 1 << 5,
|
|
||||||
PARTY_PRIVACY_FRIENDS = 1 << 6,
|
|
||||||
PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7,
|
|
||||||
EMBEDDED = 1 << 8,
|
|
||||||
CONTEXTLESS = 1 << 9
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum ActivityStatusDisplayType {
|
|
||||||
NAME = 0,
|
|
||||||
STATE = 1,
|
|
||||||
DETAILS = 2
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
export const enum ChannelType {
|
|
||||||
GUILD_TEXT = 0,
|
|
||||||
DM = 1,
|
|
||||||
GUILD_VOICE = 2,
|
|
||||||
GROUP_DM = 3,
|
|
||||||
GUILD_CATEGORY = 4,
|
|
||||||
GUILD_ANNOUNCEMENT = 5,
|
|
||||||
ANNOUNCEMENT_THREAD = 10,
|
|
||||||
PUBLIC_THREAD = 11,
|
|
||||||
PRIVATE_THREAD = 12,
|
|
||||||
GUILD_STAGE_VOICE = 13,
|
|
||||||
GUILD_DIRECTORY = 14,
|
|
||||||
GUILD_FORUM = 15,
|
|
||||||
GUILD_MEDIA = 16
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
export * from "./activity";
|
|
||||||
export * from "./channel";
|
|
||||||
export * from "./commands";
|
export * from "./commands";
|
||||||
export * from "./messages";
|
|
||||||
export * from "./misc";
|
|
||||||
|
|
|
||||||
|
|
@ -1,596 +0,0 @@
|
||||||
export const enum StickerType {
|
|
||||||
/** an official sticker in a pack */
|
|
||||||
STANDARD = 1,
|
|
||||||
/** a sticker uploaded to a guild for the guild's members */
|
|
||||||
GUILD = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum StickerFormatType {
|
|
||||||
PNG = 1,
|
|
||||||
APNG = 2,
|
|
||||||
LOTTIE = 3,
|
|
||||||
GIF = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum MessageType {
|
|
||||||
/**
|
|
||||||
* A default message (see below)
|
|
||||||
*
|
|
||||||
* Value: 0
|
|
||||||
* Name: DEFAULT
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
DEFAULT = 0,
|
|
||||||
/**
|
|
||||||
* A message sent when a user is added to a group DM or thread
|
|
||||||
*
|
|
||||||
* Value: 1
|
|
||||||
* Name: RECIPIENT_ADD
|
|
||||||
* Rendered Content: "{author} added {mentions [0] } to the {group/thread}."
|
|
||||||
* Deletable: false
|
|
||||||
*/
|
|
||||||
RECIPIENT_ADD = 1,
|
|
||||||
/**
|
|
||||||
* A message sent when a user is removed from a group DM or thread
|
|
||||||
*
|
|
||||||
* Value: 2
|
|
||||||
* Name: RECIPIENT_REMOVE
|
|
||||||
* Rendered Content: "{author} removed {mentions [0] } from the {group/thread}."
|
|
||||||
* Deletable: false
|
|
||||||
*/
|
|
||||||
RECIPIENT_REMOVE = 2,
|
|
||||||
/**
|
|
||||||
* A message sent when a user creates a call in a private channel
|
|
||||||
*
|
|
||||||
* Value: 3
|
|
||||||
* Name: CALL
|
|
||||||
* Rendered Content: participated ? "{author} started a call{ended ? " that lasted {duration}" : " — Join the call"}." : "You missed a call from {author} that lasted {duration}."
|
|
||||||
* Deletable: false
|
|
||||||
*/
|
|
||||||
CALL = 3,
|
|
||||||
/**
|
|
||||||
* A message sent when a group DM or thread's name is changed
|
|
||||||
*
|
|
||||||
* Value: 4
|
|
||||||
* Name: CHANNEL_NAME_CHANGE
|
|
||||||
* Rendered Content: "{author} changed the {is_forum ? "post title" : "channel name"}: {content} "
|
|
||||||
* Deletable: false
|
|
||||||
*/
|
|
||||||
CHANNEL_NAME_CHANGE = 4,
|
|
||||||
/**
|
|
||||||
* A message sent when a group DM's icon is changed
|
|
||||||
*
|
|
||||||
* Value: 5
|
|
||||||
* Name: CHANNEL_ICON_CHANGE
|
|
||||||
* Rendered Content: "{author} changed the channel icon."
|
|
||||||
* Deletable: false
|
|
||||||
*/
|
|
||||||
CHANNEL_ICON_CHANGE = 5,
|
|
||||||
/**
|
|
||||||
* A message sent when a message is pinned in a channel
|
|
||||||
*
|
|
||||||
* Value: 6
|
|
||||||
* Name: CHANNEL_PINNED_MESSAGE
|
|
||||||
* Rendered Content: "{author} pinned a message to this channel."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CHANNEL_PINNED_MESSAGE = 6,
|
|
||||||
/**
|
|
||||||
* A message sent when a user joins a guild
|
|
||||||
*
|
|
||||||
* Value: 7
|
|
||||||
* Name: USER_JOIN
|
|
||||||
* Rendered Content: See user join message type , obtained via the formula timestamp_ms % 13
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
USER_JOIN = 7,
|
|
||||||
/**
|
|
||||||
* A message sent when a user subscribes to (boosts) a guild
|
|
||||||
*
|
|
||||||
* Value: 8
|
|
||||||
* Name: PREMIUM_GUILD_SUBSCRIPTION
|
|
||||||
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
PREMIUM_GUILD_SUBSCRIPTION = 8,
|
|
||||||
/**
|
|
||||||
* A message sent when a user subscribes to (boosts) a guild to tier 1
|
|
||||||
*
|
|
||||||
* Value: 9
|
|
||||||
* Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_1
|
|
||||||
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 1! "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,
|
|
||||||
/**
|
|
||||||
* A message sent when a user subscribes to (boosts) a guild to tier 2
|
|
||||||
*
|
|
||||||
* Value: 10
|
|
||||||
* Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_2
|
|
||||||
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 2! "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,
|
|
||||||
/**
|
|
||||||
* A message sent when a user subscribes to (boosts) a guild to tier 3
|
|
||||||
*
|
|
||||||
* Value: 11
|
|
||||||
* Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_3
|
|
||||||
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 3! "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,
|
|
||||||
/**
|
|
||||||
* A message sent when a news channel is followed
|
|
||||||
*
|
|
||||||
* Value: 12
|
|
||||||
* Name: CHANNEL_FOLLOW_ADD
|
|
||||||
* Rendered Content: "{author} has added {content} to this channel. Its most important updates will show up here."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CHANNEL_FOLLOW_ADD = 12,
|
|
||||||
/**
|
|
||||||
* A message sent when a guild is disqualified from discovery
|
|
||||||
*
|
|
||||||
* Value: 14
|
|
||||||
* Name: GUILD_DISCOVERY_DISQUALIFIED
|
|
||||||
* Rendered Content: "This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_DISCOVERY_DISQUALIFIED = 14,
|
|
||||||
/**
|
|
||||||
* A message sent when a guild requalifies for discovery
|
|
||||||
*
|
|
||||||
* Value: 15
|
|
||||||
* Name: GUILD_DISCOVERY_REQUALIFIED
|
|
||||||
* Rendered Content: "This server is eligible for Server Discovery again and has been automatically relisted!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_DISCOVERY_REQUALIFIED = 15,
|
|
||||||
/**
|
|
||||||
* A message sent when a guild has failed discovery requirements for a week
|
|
||||||
*
|
|
||||||
* Value: 16
|
|
||||||
* Name: GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING
|
|
||||||
* Rendered Content: "This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16,
|
|
||||||
/**
|
|
||||||
* A message sent when a guild has failed discovery requirements for 3 weeks
|
|
||||||
*
|
|
||||||
* Value: 17
|
|
||||||
* Name: GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING
|
|
||||||
* Rendered Content: "This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17,
|
|
||||||
/**
|
|
||||||
* A message sent when a thread is created
|
|
||||||
*
|
|
||||||
* Value: 18
|
|
||||||
* Name: THREAD_CREATED
|
|
||||||
* Rendered Content: "{author} started a thread: {content} . See all threads."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
THREAD_CREATED = 18,
|
|
||||||
/**
|
|
||||||
* A message sent when a user replies to a message
|
|
||||||
*
|
|
||||||
* Value: 19
|
|
||||||
* Name: REPLY
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
REPLY = 19,
|
|
||||||
/**
|
|
||||||
* A message sent when a user uses a slash command
|
|
||||||
*
|
|
||||||
* Value: 20
|
|
||||||
* Name: CHAT_INPUT_COMMAND
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CHAT_INPUT_COMMAND = 20,
|
|
||||||
/**
|
|
||||||
* A message sent when a thread starter message is added to a thread
|
|
||||||
*
|
|
||||||
* Value: 21
|
|
||||||
* Name: THREAD_STARTER_MESSAGE
|
|
||||||
* Rendered Content: "{referenced_message?.content}" ?? "Sorry, we couldn't load the first message in this thread"
|
|
||||||
* Deletable: false
|
|
||||||
*/
|
|
||||||
THREAD_STARTER_MESSAGE = 21,
|
|
||||||
/**
|
|
||||||
* A message sent to remind users to invite friends to a guild
|
|
||||||
*
|
|
||||||
* Value: 22
|
|
||||||
* Name: GUILD_INVITE_REMINDER
|
|
||||||
* Rendered Content: "Wondering who to invite?\nStart by inviting anyone who can help you build the server!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_INVITE_REMINDER = 22,
|
|
||||||
/**
|
|
||||||
* A message sent when a user uses a context menu command
|
|
||||||
*
|
|
||||||
* Value: 23
|
|
||||||
* Name: CONTEXT_MENU_COMMAND
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CONTEXT_MENU_COMMAND = 23,
|
|
||||||
/**
|
|
||||||
* A message sent when auto moderation takes an action
|
|
||||||
*
|
|
||||||
* Value: 24
|
|
||||||
* Name: AUTO_MODERATION_ACTION
|
|
||||||
* Rendered Content: Special embed rendered from embeds[0]
|
|
||||||
* Deletable: true 1
|
|
||||||
*/
|
|
||||||
AUTO_MODERATION_ACTION = 24,
|
|
||||||
/**
|
|
||||||
* A message sent when a user purchases or renews a role subscription
|
|
||||||
*
|
|
||||||
* Value: 25
|
|
||||||
* Name: ROLE_SUBSCRIPTION_PURCHASE
|
|
||||||
* Rendered Content: "{author} {is_renewal ? "renewed" : "joined"} {role_subscription.tier_name} and has been a subscriber of {guild} for {role_subscription.total_months_subscribed} month(?s)!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
ROLE_SUBSCRIPTION_PURCHASE = 25,
|
|
||||||
/**
|
|
||||||
* A message sent when a user is upsold to a premium interaction
|
|
||||||
*
|
|
||||||
* Value: 26
|
|
||||||
* Name: INTERACTION_PREMIUM_UPSELL
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
INTERACTION_PREMIUM_UPSELL = 26,
|
|
||||||
/**
|
|
||||||
* A message sent when a stage channel starts
|
|
||||||
*
|
|
||||||
* Value: 27
|
|
||||||
* Name: STAGE_START
|
|
||||||
* Rendered Content: "{author} started {content} "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
STAGE_START = 27,
|
|
||||||
/**
|
|
||||||
* A message sent when a stage channel ends
|
|
||||||
*
|
|
||||||
* Value: 28
|
|
||||||
* Name: STAGE_END
|
|
||||||
* Rendered Content: "{author} ended {content} "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
STAGE_END = 28,
|
|
||||||
/**
|
|
||||||
* A message sent when a user starts speaking in a stage channel
|
|
||||||
*
|
|
||||||
* Value: 29
|
|
||||||
* Name: STAGE_SPEAKER
|
|
||||||
* Rendered Content: "{author} is now a speaker."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
STAGE_SPEAKER = 29,
|
|
||||||
/**
|
|
||||||
* A message sent when a user raises their hand in a stage channel
|
|
||||||
*
|
|
||||||
* Value: 30
|
|
||||||
* Name: STAGE_RAISE_HAND
|
|
||||||
* Rendered Content: "{author} requested to speak."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
STAGE_RAISE_HAND = 30,
|
|
||||||
/**
|
|
||||||
* A message sent when a stage channel's topic is changed
|
|
||||||
*
|
|
||||||
* Value: 31
|
|
||||||
* Name: STAGE_TOPIC
|
|
||||||
* Rendered Content: "{author} changed the Stage topic: {content} "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
STAGE_TOPIC = 31,
|
|
||||||
/**
|
|
||||||
* A message sent when a user purchases an application premium subscription
|
|
||||||
*
|
|
||||||
* Value: 32
|
|
||||||
* Name: GUILD_APPLICATION_PREMIUM_SUBSCRIPTION
|
|
||||||
* Rendered Content: "{author} upgraded {application ?? "a deleted application"} to premium for this server!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32,
|
|
||||||
/**
|
|
||||||
* A message sent when a user gifts a premium (Nitro) referral
|
|
||||||
*
|
|
||||||
* Value: 35
|
|
||||||
* Name: PREMIUM_REFERRAL
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
PREMIUM_REFERRAL = 35,
|
|
||||||
/**
|
|
||||||
* A message sent when a user enabled lockdown for the guild
|
|
||||||
*
|
|
||||||
* Value: 36
|
|
||||||
* Name: GUILD_INCIDENT_ALERT_MODE_ENABLED
|
|
||||||
* Rendered Content: "{author} enabled security actions until {content}."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_INCIDENT_ALERT_MODE_ENABLED = 36,
|
|
||||||
/**
|
|
||||||
* A message sent when a user disables lockdown for the guild
|
|
||||||
*
|
|
||||||
* Value: 37
|
|
||||||
* Name: GUILD_INCIDENT_ALERT_MODE_DISABLED
|
|
||||||
* Rendered Content: "{author} disabled security actions."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_INCIDENT_ALERT_MODE_DISABLED = 37,
|
|
||||||
/**
|
|
||||||
* A message sent when a user reports a raid for the guild
|
|
||||||
*
|
|
||||||
* Value: 38
|
|
||||||
* Name: GUILD_INCIDENT_REPORT_RAID
|
|
||||||
* Rendered Content: "{author} reported a raid in {guild}."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_INCIDENT_REPORT_RAID = 38,
|
|
||||||
/**
|
|
||||||
* A message sent when a user reports a false alarm for the guild
|
|
||||||
*
|
|
||||||
* Value: 39
|
|
||||||
* Name: GUILD_INCIDENT_REPORT_FALSE_ALARM
|
|
||||||
* Rendered Content: "{author} reported a false alarm in {guild}."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_INCIDENT_REPORT_FALSE_ALARM = 39,
|
|
||||||
/**
|
|
||||||
* A message sent when no one sends a message in the current channel for 1 hour
|
|
||||||
*
|
|
||||||
* Value: 40
|
|
||||||
* Name: GUILD_DEADCHAT_REVIVE_PROMPT
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_DEADCHAT_REVIVE_PROMPT = 40,
|
|
||||||
/**
|
|
||||||
* A message sent when a user buys another user a gift
|
|
||||||
*
|
|
||||||
* Value: 41
|
|
||||||
* Name: CUSTOM_GIFT
|
|
||||||
* Rendered Content: Special embed rendered from embeds[0].url and gift_info
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CUSTOM_GIFT = 41,
|
|
||||||
/**
|
|
||||||
* Value: 42
|
|
||||||
* Name: GUILD_GAMING_STATS_PROMPT
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_GAMING_STATS_PROMPT = 42,
|
|
||||||
/**
|
|
||||||
* A message sent when a user purchases a guild product
|
|
||||||
*
|
|
||||||
* Value: 44
|
|
||||||
* Name: PURCHASE_NOTIFICATION
|
|
||||||
* Rendered Content: "{author} has purchased {purchase_notification.guild_product_purchase.product_name}!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
PURCHASE_NOTIFICATION = 44,
|
|
||||||
/**
|
|
||||||
* A message sent when a poll is finalized
|
|
||||||
*
|
|
||||||
* Value: 46
|
|
||||||
* Name: POLL_RESULT
|
|
||||||
* Rendered Content: Special embed rendered from embeds[0]
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
POLL_RESULT = 46,
|
|
||||||
/**
|
|
||||||
* A message sent by the Discord Updates account when a new changelog is posted
|
|
||||||
*
|
|
||||||
* Value: 47
|
|
||||||
* Name: CHANGELOG
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CHANGELOG = 47,
|
|
||||||
/**
|
|
||||||
* A message sent when a Nitro promotion is triggered
|
|
||||||
*
|
|
||||||
* Value: 48
|
|
||||||
* Name: NITRO_NOTIFICATION
|
|
||||||
* Rendered Content: Special embed rendered from content
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
NITRO_NOTIFICATION = 48,
|
|
||||||
/**
|
|
||||||
* A message sent when a voice channel is linked to a lobby
|
|
||||||
*
|
|
||||||
* Value: 49
|
|
||||||
* Name: CHANNEL_LINKED_TO_LOBBY
|
|
||||||
* Rendered Content: "{content}"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
CHANNEL_LINKED_TO_LOBBY = 49,
|
|
||||||
/**
|
|
||||||
* A local-only ephemeral message sent when a user is prompted to gift Nitro to a friend on their friendship anniversary
|
|
||||||
*
|
|
||||||
* Value: 50
|
|
||||||
* Name: GIFTING_PROMPT
|
|
||||||
* Rendered Content: Special embed
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GIFTING_PROMPT = 50,
|
|
||||||
/**
|
|
||||||
* A local-only message sent when a user receives an in-game message NUX
|
|
||||||
*
|
|
||||||
* Value: 51
|
|
||||||
* Name: IN_GAME_MESSAGE_NUX
|
|
||||||
* Rendered Content: "{author} messaged you from {application.name}. In-game chat may not include rich messaging features such as images, polls, or apps. Learn More "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
IN_GAME_MESSAGE_NUX = 51,
|
|
||||||
/**
|
|
||||||
* A message sent when a user accepts a guild join request
|
|
||||||
*
|
|
||||||
* Value: 52
|
|
||||||
* Name: GUILD_JOIN_REQUEST_ACCEPT_NOTIFICATION 2
|
|
||||||
* Rendered Content: "{join_request.user}'s application to {content} was approved! Welcome!"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_JOIN_REQUEST_ACCEPT_NOTIFICATION = 52,
|
|
||||||
/**
|
|
||||||
* A message sent when a user rejects a guild join request
|
|
||||||
*
|
|
||||||
* Value: 53
|
|
||||||
* Name: GUILD_JOIN_REQUEST_REJECT_NOTIFICATION 2
|
|
||||||
* Rendered Content: "{join_request.user}'s application to {content} was rejected."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_JOIN_REQUEST_REJECT_NOTIFICATION = 53,
|
|
||||||
/**
|
|
||||||
* A message sent when a user withdraws a guild join request
|
|
||||||
*
|
|
||||||
* Value: 54
|
|
||||||
* Name: GUILD_JOIN_REQUEST_WITHDRAWN_NOTIFICATION 2
|
|
||||||
* Rendered Content: "{join_request.user}'s application to {content} has been withdrawn."
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
GUILD_JOIN_REQUEST_WITHDRAWN_NOTIFICATION = 54,
|
|
||||||
/**
|
|
||||||
* A message sent when a user upgrades to HD streaming
|
|
||||||
*
|
|
||||||
* Value: 55
|
|
||||||
* Name: HD_STREAMING_UPGRADED
|
|
||||||
* Rendered Content: "{author} activated HD Splash Potion "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
HD_STREAMING_UPGRADED = 55,
|
|
||||||
/**
|
|
||||||
* A message sent when a user resolves a moderation report by deleting the offending message
|
|
||||||
*
|
|
||||||
* Value: 58
|
|
||||||
* Name: REPORT_TO_MOD_DELETED_MESSAGE
|
|
||||||
* Rendered Content: "{author} deleted the message"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
REPORT_TO_MOD_DELETED_MESSAGE = 58,
|
|
||||||
/**
|
|
||||||
* A message sent when a user resolves a moderation report by timing out the offending user
|
|
||||||
*
|
|
||||||
* Value: 59
|
|
||||||
* Name: REPORT_TO_MOD_TIMEOUT_USER
|
|
||||||
* Rendered Content: "{author} timed out {mentions [0] }"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
REPORT_TO_MOD_TIMEOUT_USER = 59,
|
|
||||||
/**
|
|
||||||
* A message sent when a user resolves a moderation report by kicking the offending user
|
|
||||||
*
|
|
||||||
* Value: 60
|
|
||||||
* Name: REPORT_TO_MOD_KICK_USER
|
|
||||||
* Rendered Content: "{author} kicked {mentions [0] }"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
REPORT_TO_MOD_KICK_USER = 60,
|
|
||||||
/**
|
|
||||||
* A message sent when a user resolves a moderation report by banning the offending user
|
|
||||||
*
|
|
||||||
* Value: 61
|
|
||||||
* Name: REPORT_TO_MOD_BAN_USER
|
|
||||||
* Rendered Content: "{author} banned {mentions [0] }"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
REPORT_TO_MOD_BAN_USER = 61,
|
|
||||||
/**
|
|
||||||
* A message sent when a user resolves a moderation report
|
|
||||||
*
|
|
||||||
* Value: 62
|
|
||||||
* Name: REPORT_TO_MOD_CLOSED_REPORT
|
|
||||||
* Rendered Content: "{author} resolved this flag"
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
REPORT_TO_MOD_CLOSED_REPORT = 62,
|
|
||||||
/**
|
|
||||||
* A message sent when a user adds a new emoji to a guild
|
|
||||||
*
|
|
||||||
* Value: 63
|
|
||||||
* Name: EMOJI_ADDED
|
|
||||||
* Rendered Content: "{author} added a new emoji, {content} :{emoji.name}: "
|
|
||||||
* Deletable: true
|
|
||||||
*/
|
|
||||||
EMOJI_ADDED = 63,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum MessageFlags {
|
|
||||||
/**
|
|
||||||
* Message has been published to subscribed channels (via Channel Following)
|
|
||||||
*
|
|
||||||
* Value: 1 << 0
|
|
||||||
*/
|
|
||||||
CROSSPOSTED = 1 << 0,
|
|
||||||
/**
|
|
||||||
* Message originated from a message in another channel (via Channel Following)
|
|
||||||
*/
|
|
||||||
IS_CROSSPOST = 1 << 1,
|
|
||||||
/**
|
|
||||||
* Embeds will not be included when serializing this message
|
|
||||||
*/
|
|
||||||
SUPPRESS_EMBEDS = 1 << 2,
|
|
||||||
/**
|
|
||||||
* Source message for this crosspost has been deleted (via Channel Following)
|
|
||||||
*/
|
|
||||||
SOURCE_MESSAGE_DELETED = 1 << 3,
|
|
||||||
/**
|
|
||||||
* Message came from the urgent message system
|
|
||||||
*/
|
|
||||||
URGENT = 1 << 4,
|
|
||||||
/**
|
|
||||||
* Message has an associated thread, with the same ID as the message
|
|
||||||
*/
|
|
||||||
HAS_THREAD = 1 << 5,
|
|
||||||
/**
|
|
||||||
* Message is only visible to the user who invoked the interaction
|
|
||||||
*/
|
|
||||||
EPHEMERAL = 1 << 6,
|
|
||||||
/**
|
|
||||||
* Message is an interaction response and the bot is "thinking"
|
|
||||||
*/
|
|
||||||
LOADING = 1 << 7,
|
|
||||||
/**
|
|
||||||
* Some roles were not mentioned and added to the thread
|
|
||||||
*/
|
|
||||||
FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8,
|
|
||||||
/**
|
|
||||||
* Message is hidden from the guild's feed
|
|
||||||
*/
|
|
||||||
GUILD_FEED_HIDDEN = 1 << 9,
|
|
||||||
/**
|
|
||||||
* Message contains a link that impersonates Discord
|
|
||||||
*/
|
|
||||||
SHOULD_SHOW_LINK_NOT_DISCORD_WARNING = 1 << 10,
|
|
||||||
/**
|
|
||||||
* Message will not trigger push and desktop notifications
|
|
||||||
*/
|
|
||||||
SUPPRESS_NOTIFICATIONS = 1 << 12,
|
|
||||||
/**
|
|
||||||
* Message's audio attachment is rendered as a voice message
|
|
||||||
*/
|
|
||||||
IS_VOICE_MESSAGE = 1 << 13,
|
|
||||||
/**
|
|
||||||
* Message has a forwarded message snapshot attached
|
|
||||||
*/
|
|
||||||
HAS_SNAPSHOT = 1 << 14,
|
|
||||||
/**
|
|
||||||
* Message contains components from version 2 of the UI kit
|
|
||||||
*/
|
|
||||||
IS_COMPONENTS_V2 = 1 << 15,
|
|
||||||
/**
|
|
||||||
* Message was triggered by the social layer integration
|
|
||||||
*/
|
|
||||||
SENT_BY_SOCIAL_LAYER_INTEGRATION = 1 << 16,
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export const enum CloudUploadPlatform {
|
|
||||||
REACT_NATIVE = 0,
|
|
||||||
WEB = 1,
|
|
||||||
}
|
|
||||||
5
packages/discord-types/src/classes.d.ts
vendored
5
packages/discord-types/src/classes.d.ts
vendored
|
|
@ -1,3 +1,8 @@
|
||||||
|
export interface ImageModalClasses {
|
||||||
|
image: string,
|
||||||
|
modal: string,
|
||||||
|
}
|
||||||
|
|
||||||
export interface ButtonWrapperClasses {
|
export interface ButtonWrapperClasses {
|
||||||
hoverScale: string;
|
hoverScale: string;
|
||||||
buttonWrapper: string;
|
buttonWrapper: string;
|
||||||
|
|
|
||||||
36
packages/discord-types/src/common/Activity.d.ts
vendored
36
packages/discord-types/src/common/Activity.d.ts
vendored
|
|
@ -1,36 +0,0 @@
|
||||||
import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "../../enums";
|
|
||||||
|
|
||||||
export interface ActivityAssets {
|
|
||||||
large_image?: string;
|
|
||||||
large_text?: string;
|
|
||||||
small_image?: string;
|
|
||||||
small_text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActivityButton {
|
|
||||||
label: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Activity {
|
|
||||||
name: string;
|
|
||||||
application_id: string;
|
|
||||||
type: ActivityType;
|
|
||||||
state?: string;
|
|
||||||
state_url?: string;
|
|
||||||
details?: string;
|
|
||||||
details_url?: string;
|
|
||||||
url?: string;
|
|
||||||
flags: ActivityFlags;
|
|
||||||
status_display_type?: ActivityStatusDisplayType;
|
|
||||||
timestamps?: {
|
|
||||||
start?: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
assets?: ActivityAssets;
|
|
||||||
buttons?: string[];
|
|
||||||
metadata?: {
|
|
||||||
button_urls?: Array<string>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
1
packages/discord-types/src/common/index.d.ts
vendored
1
packages/discord-types/src/common/index.d.ts
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./Activity";
|
|
||||||
export * from "./Application";
|
export * from "./Application";
|
||||||
export * from "./Channel";
|
export * from "./Channel";
|
||||||
export * from "./Guild";
|
export * from "./Guild";
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ import { CommandOption } from './Commands';
|
||||||
import { User, UserJSON } from '../User';
|
import { User, UserJSON } from '../User';
|
||||||
import { Embed, EmbedJSON } from './Embed';
|
import { Embed, EmbedJSON } from './Embed';
|
||||||
import { DiscordRecord } from "../Record";
|
import { DiscordRecord } from "../Record";
|
||||||
import { MessageFlags, MessageType, StickerFormatType } from "../../../enums";
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* TODO: looks like discord has moved over to Date instead of Moment;
|
* TODO: looks like discord has moved over to Date instead of Moment;
|
||||||
*/
|
*/
|
||||||
export class Message extends DiscordRecord {
|
export class Message extends DiscordRecord {
|
||||||
|
|
@ -35,7 +34,7 @@ export class Message extends DiscordRecord {
|
||||||
customRenderedContent: unknown;
|
customRenderedContent: unknown;
|
||||||
editedTimestamp: Date;
|
editedTimestamp: Date;
|
||||||
embeds: Embed[];
|
embeds: Embed[];
|
||||||
flags: MessageFlags;
|
flags: number;
|
||||||
giftCodes: string[];
|
giftCodes: string[];
|
||||||
id: string;
|
id: string;
|
||||||
interaction: {
|
interaction: {
|
||||||
|
|
@ -84,23 +83,20 @@ export class Message extends DiscordRecord {
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
message_id: string;
|
message_id: string;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
messageSnapshots: {
|
|
||||||
message: Message;
|
|
||||||
}[];
|
|
||||||
nick: unknown; // probably a string
|
nick: unknown; // probably a string
|
||||||
nonce: string | undefined;
|
nonce: string | undefined;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
reactions: MessageReaction[];
|
reactions: MessageReaction[];
|
||||||
state: string;
|
state: string;
|
||||||
stickerItems: {
|
stickerItems: {
|
||||||
format_type: StickerFormatType;
|
format_type: number;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
stickers: unknown[];
|
stickers: unknown[];
|
||||||
timestamp: moment.Moment;
|
timestamp: moment.Moment;
|
||||||
tts: boolean;
|
tts: boolean;
|
||||||
type: MessageType;
|
type: number;
|
||||||
webhookId: string | undefined;
|
webhookId: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,13 +117,10 @@ export class Message extends DiscordRecord {
|
||||||
removeReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;
|
removeReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;
|
||||||
|
|
||||||
getChannelId(): string;
|
getChannelId(): string;
|
||||||
hasFlag(flag: MessageFlags): boolean;
|
hasFlag(flag: number): boolean;
|
||||||
isCommandType(): boolean;
|
isCommandType(): boolean;
|
||||||
isEdited(): boolean;
|
isEdited(): boolean;
|
||||||
isSystemDM(): boolean;
|
isSystemDM(): boolean;
|
||||||
|
|
||||||
/** Vencord added */
|
|
||||||
deleted?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A smaller Message object found in FluxDispatcher and elsewhere. */
|
/** A smaller Message object found in FluxDispatcher and elsewhere. */
|
||||||
|
|
@ -196,9 +189,3 @@ export interface MessageReaction {
|
||||||
emoji: ReactionEmoji;
|
emoji: ReactionEmoji;
|
||||||
me: boolean;
|
me: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object.keys(findByProps("REPLYABLE")).map(JSON.stringify).join("|")
|
|
||||||
export type MessageTypeSets = Record<
|
|
||||||
"UNDELETABLE" | "GUILD_DISCOVERY_STATUS" | "USER_MESSAGE" | "NOTIFIABLE_SYSTEM_MESSAGE" | "REPLYABLE" | "FORWARDABLE" | "REFERENCED_MESSAGE_AVAILABLE" | "AVAILABLE_IN_GUILD_FEED" | "DEADCHAT_PROMPTS" | "NON_COLLAPSIBLE" | "NON_PARSED" | "AUTOMOD_INCIDENT_ACTIONS" | "SELF_MENTIONABLE_SYSTEM" | "SCHEDULABLE",
|
|
||||||
Set<MessageType>
|
|
||||||
>;
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { StickerFormatType, StickerType } from "../../../enums";
|
|
||||||
|
|
||||||
interface BaseSticker {
|
|
||||||
asset: string;
|
|
||||||
available: boolean;
|
|
||||||
description: string;
|
|
||||||
format_type: StickerFormatType;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
sort_value?: number;
|
|
||||||
/** a comma separated string */
|
|
||||||
tags: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PackSticker extends BaseSticker {
|
|
||||||
pack_id: string;
|
|
||||||
type: StickerType.STANDARD;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GuildSticker extends BaseSticker {
|
|
||||||
guild_id: string;
|
|
||||||
type: StickerType.GUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Sticker = PackSticker | GuildSticker;
|
|
||||||
|
|
||||||
export interface PremiumStickerPack {
|
|
||||||
banner_asset_id?: string;
|
|
||||||
cover_sticker_id?: string;
|
|
||||||
description: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
sku_id: string;
|
|
||||||
stickers: PackSticker[];
|
|
||||||
}
|
|
||||||
|
|
@ -2,4 +2,3 @@ export * from "./Commands";
|
||||||
export * from "./Message";
|
export * from "./Message";
|
||||||
export * from "./Embed";
|
export * from "./Embed";
|
||||||
export * from "./Emoji";
|
export * from "./Emoji";
|
||||||
export * from "./Sticker";
|
|
||||||
|
|
|
||||||
66
packages/discord-types/src/components.d.ts
vendored
66
packages/discord-types/src/components.d.ts
vendored
|
|
@ -43,7 +43,12 @@ export type FormDivider = ComponentType<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type FormText = ComponentType<TextProps>;
|
export type FormText = ComponentType<PropsWithChildren<{
|
||||||
|
disabled?: boolean;
|
||||||
|
selectable?: boolean;
|
||||||
|
/** defaults to FormText.Types.DEFAULT */
|
||||||
|
type?: string;
|
||||||
|
}> & TextProps> & { Types: FormTextTypes; };
|
||||||
|
|
||||||
export type Tooltip = ComponentType<{
|
export type Tooltip = ComponentType<{
|
||||||
text: ReactNode | ComponentType;
|
text: ReactNode | ComponentType;
|
||||||
|
|
@ -239,10 +244,8 @@ export type TextInput = ComponentType<PropsWithChildren<{
|
||||||
Sizes: Record<"DEFAULT" | "MINI", string>;
|
Sizes: Record<"DEFAULT" | "MINI", string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: this is wrong, it's not actually just HTMLTextAreaElement
|
|
||||||
export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
|
export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
|
||||||
onChange(v: string): void;
|
onChange(v: string): void;
|
||||||
inputRef?: Ref<HTMLTextAreaElement>;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
|
|
@ -469,66 +472,19 @@ export type MaskedLink = ComponentType<PropsWithChildren<{
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
export interface ScrollerBaseProps {
|
export type ScrollerThin = ComponentType<PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
|
||||||
dir?: "ltr";
|
dir?: "ltr";
|
||||||
|
orientation?: "horizontal" | "vertical" | "auto";
|
||||||
paddingFix?: boolean;
|
paddingFix?: boolean;
|
||||||
|
fade?: boolean;
|
||||||
|
|
||||||
onClose?(): void;
|
onClose?(): void;
|
||||||
onScroll?(): void;
|
onScroll?(): void;
|
||||||
}
|
|
||||||
|
|
||||||
export type ScrollerThin = ComponentType<PropsWithChildren<ScrollerBaseProps & {
|
|
||||||
orientation?: "horizontal" | "vertical" | "auto";
|
|
||||||
fade?: boolean;
|
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
interface BaseListItem {
|
|
||||||
anchorId: any;
|
|
||||||
listIndex: number;
|
|
||||||
offsetTop: number;
|
|
||||||
section: number;
|
|
||||||
}
|
|
||||||
interface ListSection extends BaseListItem {
|
|
||||||
type: "section";
|
|
||||||
}
|
|
||||||
interface ListRow extends BaseListItem {
|
|
||||||
type: "row";
|
|
||||||
row: number;
|
|
||||||
rowIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListScrollerThin = ComponentType<ScrollerBaseProps & {
|
|
||||||
sections: number[];
|
|
||||||
renderSection?: (item: ListSection) => React.ReactNode;
|
|
||||||
renderRow: (item: ListRow) => React.ReactNode;
|
|
||||||
renderFooter?: (item: any) => React.ReactNode;
|
|
||||||
renderSidebar?: (listVisible: boolean, sidebarVisible: boolean) => React.ReactNode;
|
|
||||||
wrapSection?: (section: number, children: React.ReactNode) => React.ReactNode;
|
|
||||||
|
|
||||||
sectionHeight: number;
|
|
||||||
rowHeight: number;
|
|
||||||
footerHeight?: number;
|
|
||||||
sidebarHeight?: number;
|
|
||||||
|
|
||||||
chunkSize?: number;
|
|
||||||
|
|
||||||
paddingTop?: number;
|
|
||||||
paddingBottom?: number;
|
|
||||||
fade?: boolean;
|
|
||||||
onResize?: Function;
|
|
||||||
getAnchorId?: any;
|
|
||||||
|
|
||||||
innerTag?: string;
|
|
||||||
innerId?: string;
|
|
||||||
innerClassName?: string;
|
|
||||||
innerRole?: string;
|
|
||||||
innerAriaLabel?: string;
|
|
||||||
// Yes, Discord uses this casing
|
|
||||||
innerAriaMultiselectable?: boolean;
|
|
||||||
innerAriaOrientation?: "vertical" | "horizontal";
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
|
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
|
||||||
tag?: T;
|
tag?: T;
|
||||||
}) => ReactNode;
|
}) => ReactNode;
|
||||||
|
|
|
||||||
1
packages/discord-types/src/index.d.ts
vendored
1
packages/discord-types/src/index.d.ts
vendored
|
|
@ -4,7 +4,6 @@ export * from "./components";
|
||||||
export * from "./flux";
|
export * from "./flux";
|
||||||
export * from "./fluxEvents";
|
export * from "./fluxEvents";
|
||||||
export * from "./menu";
|
export * from "./menu";
|
||||||
export * from "./modules";
|
|
||||||
export * from "./stores";
|
export * from "./stores";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * as Webpack from "../webpack";
|
export * as Webpack from "../webpack";
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import EventEmitter from "events";
|
|
||||||
import { CloudUploadPlatform } from "../../enums";
|
|
||||||
|
|
||||||
interface BaseUploadItem {
|
|
||||||
platform: CloudUploadPlatform;
|
|
||||||
id?: string;
|
|
||||||
origin?: string;
|
|
||||||
isThumbnail?: boolean;
|
|
||||||
clip?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReactNativeUploadItem extends BaseUploadItem {
|
|
||||||
platform: CloudUploadPlatform.REACT_NATIVE;
|
|
||||||
uri: string;
|
|
||||||
filename?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
durationSecs?: number;
|
|
||||||
waveform?: string;
|
|
||||||
isRemix?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebUploadItem extends BaseUploadItem {
|
|
||||||
platform: CloudUploadPlatform.WEB;
|
|
||||||
file: File;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CloudUploadItem = ReactNativeUploadItem | WebUploadItem;
|
|
||||||
|
|
||||||
export class CloudUpload extends EventEmitter {
|
|
||||||
constructor(item: CloudUploadItem, channelId: string, showLargeMessageDialog?: boolean, reactNativeFileIndex?: number);
|
|
||||||
|
|
||||||
channelId: string;
|
|
||||||
classification: string;
|
|
||||||
clip: unknown;
|
|
||||||
contentHash: unknown;
|
|
||||||
currentSize: number;
|
|
||||||
description: string | null;
|
|
||||||
durationSecs: number | undefined;
|
|
||||||
etag: string | undefined;
|
|
||||||
error: unknown;
|
|
||||||
filename: string;
|
|
||||||
id: string;
|
|
||||||
isImage: boolean;
|
|
||||||
isRemix: boolean | undefined;
|
|
||||||
isThumbnail: boolean;
|
|
||||||
isVideo: boolean;
|
|
||||||
item: {
|
|
||||||
file: File;
|
|
||||||
platform: CloudUploadPlatform;
|
|
||||||
origin: string;
|
|
||||||
};
|
|
||||||
loaded: number;
|
|
||||||
mimeType: string;
|
|
||||||
origin: string;
|
|
||||||
postCompressionSize: number | undefined;
|
|
||||||
preCompressionSize: number;
|
|
||||||
responseUrl: string;
|
|
||||||
sensitive: boolean;
|
|
||||||
showLargeMessageDialog: boolean;
|
|
||||||
spoiler: boolean;
|
|
||||||
startTime: number;
|
|
||||||
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED" | "REMOVED_FROM_MSG_DRAFT";
|
|
||||||
uniqueId: string;
|
|
||||||
uploadedFilename: string;
|
|
||||||
waveform: string | undefined;
|
|
||||||
|
|
||||||
// there are many more methods than just these but I didn't find them particularly useful
|
|
||||||
upload(): Promise<void>;
|
|
||||||
cancel(): void;
|
|
||||||
delete(): Promise<void>;
|
|
||||||
getSize(): number;
|
|
||||||
maybeConvertToWebP(): Promise<void>;
|
|
||||||
removeFromMsgDraft(): void;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./CloudUpload";
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { FluxStore } from "..";
|
|
||||||
|
|
||||||
export class AuthenticationStore extends FluxStore {
|
|
||||||
/**
|
|
||||||
* Gets the id of the current user
|
|
||||||
*/
|
|
||||||
getId(): string;
|
|
||||||
|
|
||||||
// This Store has a lot more methods related to everything Auth, but they really should
|
|
||||||
// not be needed, so they are not typed
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { FluxStore, Role } from "..";
|
import { FluxStore, Role } from "..";
|
||||||
|
|
||||||
// TODO: add the rest of the methods for GuildRoleStore
|
|
||||||
export class GuildRoleStore extends FluxStore {
|
export class GuildRoleStore extends FluxStore {
|
||||||
getRole(guildId: string, roleId: string): Role;
|
getRole(guildId: string, roleId: string): Role;
|
||||||
getSortedRoles(guildId: string): Role[];
|
getRoles(guildId: string): Record<string, Role>;
|
||||||
getRolesSnapshot(guildId: string): Record<string, Role>;
|
getAllGuildRoles(): Record<string, Record<string, Role>>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@ export class RelationshipStore extends FluxStore {
|
||||||
isFriend(userId: string): boolean;
|
isFriend(userId: string): boolean;
|
||||||
isBlocked(userId: string): boolean;
|
isBlocked(userId: string): boolean;
|
||||||
isIgnored(userId: string): boolean;
|
isIgnored(userId: string): boolean;
|
||||||
/**
|
|
||||||
* @see {@link isBlocked}
|
|
||||||
* @see {@link isIgnored}
|
|
||||||
*/
|
|
||||||
isBlockedOrIgnored(userId: string): boolean;
|
|
||||||
getSince(userId: string): string;
|
getSince(userId: string): string;
|
||||||
|
|
||||||
getMutableRelationships(): Map<string, number>;
|
getMutableRelationships(): Map<string, number>;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { FluxStore, GuildSticker, PremiumStickerPack, Sticker } from "..";
|
|
||||||
|
|
||||||
export type StickerGuildMap = Map<string, GuildSticker[]>;
|
|
||||||
|
|
||||||
export class StickersStore extends FluxStore {
|
|
||||||
getAllGuildStickers(): StickerGuildMap;
|
|
||||||
getRawStickersByGuild(): StickerGuildMap;
|
|
||||||
getPremiumPacks(): PremiumStickerPack[];
|
|
||||||
|
|
||||||
getStickerById(id: string): Sticker | undefined;
|
|
||||||
getStickerPack(id: string): PremiumStickerPack | undefined;
|
|
||||||
getStickersByGuildId(guildId: string): Sticker[] | undefined;
|
|
||||||
|
|
||||||
isPremiumPack(id: string): boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { FluxStore } from "@vencord/discord-types";
|
|
||||||
|
|
||||||
export class StreamerModeStore extends FluxStore {
|
|
||||||
get autoToggle(): boolean;
|
|
||||||
get disableNotifications(): boolean;
|
|
||||||
get disableSounds(): boolean;
|
|
||||||
get enableContentProtection(): boolean;
|
|
||||||
get enabled(): boolean;
|
|
||||||
get hideInstantInvites(): boolean;
|
|
||||||
get hidePersonalInformation(): boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { FluxStore } from "..";
|
|
||||||
|
|
||||||
export class TypingStore extends FluxStore {
|
|
||||||
/**
|
|
||||||
* returns a map of user ids to timeout ids
|
|
||||||
*/
|
|
||||||
getTypingUsers(channelId: string): Record<string, number>;
|
|
||||||
isTyping(channelId: string, userId: string): boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import { DiscordRecord } from "../common";
|
|
||||||
import { FluxStore } from "./FluxStore";
|
|
||||||
|
|
||||||
export type UserVoiceStateRecords = Record<string, VoiceState>;
|
|
||||||
export type VoiceStates = Record<string, UserVoiceStateRecords>;
|
|
||||||
|
|
||||||
export interface VoiceState extends DiscordRecord {
|
|
||||||
userId: string;
|
|
||||||
channelId: string | null | undefined;
|
|
||||||
sessionId: string | null | undefined;
|
|
||||||
mute: boolean;
|
|
||||||
deaf: boolean;
|
|
||||||
selfMute: boolean;
|
|
||||||
selfDeaf: boolean;
|
|
||||||
selfVideo: boolean;
|
|
||||||
selfStream: boolean | undefined;
|
|
||||||
suppress: boolean;
|
|
||||||
requestToSpeakTimestamp: string | null | undefined;
|
|
||||||
discoverable: boolean;
|
|
||||||
|
|
||||||
isVoiceMuted(): boolean;
|
|
||||||
isVoiceDeafened(): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceStateStore extends FluxStore {
|
|
||||||
getAllVoiceStates(): VoiceStates;
|
|
||||||
|
|
||||||
getVoiceStates(guildId?: string | null): UserVoiceStateRecords;
|
|
||||||
getVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;
|
|
||||||
getVideoVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;
|
|
||||||
|
|
||||||
getVoiceState(guildId: string | null, userId: string): VoiceState | undefined;
|
|
||||||
getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined;
|
|
||||||
getVoiceStateForChannel(channelId: string, userId?: string): VoiceState | undefined;
|
|
||||||
getVoiceStateForUser(userId: string): VoiceState | undefined;
|
|
||||||
|
|
||||||
getCurrentClientVoiceChannelId(guildId: string | null): string | undefined;
|
|
||||||
isCurrentClientInVoiceChannel(): boolean;
|
|
||||||
|
|
||||||
isInChannel(channelId: string, userId?: string): boolean;
|
|
||||||
hasVideo(channelId: string): boolean;
|
|
||||||
}
|
|
||||||
5
packages/discord-types/src/stores/index.d.ts
vendored
5
packages/discord-types/src/stores/index.d.ts
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
// please keep in alphabetical order
|
// please keep in alphabetical order
|
||||||
export * from "./AuthenticationStore";
|
|
||||||
export * from "./ChannelStore";
|
export * from "./ChannelStore";
|
||||||
export * from "./DraftStore";
|
export * from "./DraftStore";
|
||||||
export * from "./EmojiStore";
|
export * from "./EmojiStore";
|
||||||
|
|
@ -11,13 +10,9 @@ export * from "./MessageStore";
|
||||||
export * from "./RelationshipStore";
|
export * from "./RelationshipStore";
|
||||||
export * from "./SelectedChannelStore";
|
export * from "./SelectedChannelStore";
|
||||||
export * from "./SelectedGuildStore";
|
export * from "./SelectedGuildStore";
|
||||||
export * from "./StickersStore";
|
|
||||||
export * from "./StreamerModeStore";
|
|
||||||
export * from "./ThemeStore";
|
export * from "./ThemeStore";
|
||||||
export * from "./TypingStore";
|
|
||||||
export * from "./UserProfileStore";
|
export * from "./UserProfileStore";
|
||||||
export * from "./UserStore";
|
export * from "./UserStore";
|
||||||
export * from "./VoiceStateStore";
|
|
||||||
export * from "./WindowStore";
|
export * from "./WindowStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
8
packages/discord-types/src/utils.d.ts
vendored
8
packages/discord-types/src/utils.d.ts
vendored
|
|
@ -6,15 +6,13 @@ import type { FluxEvents } from "./fluxEvents";
|
||||||
|
|
||||||
export { FluxEvents };
|
export { FluxEvents };
|
||||||
|
|
||||||
type FluxEventsAutoComplete = LiteralUnion<FluxEvents, string>;
|
|
||||||
|
|
||||||
export interface FluxDispatcher {
|
export interface FluxDispatcher {
|
||||||
_actionHandlers: any;
|
_actionHandlers: any;
|
||||||
_subscriptions: any;
|
_subscriptions: any;
|
||||||
dispatch(event: { [key: string]: unknown; type: FluxEventsAutoComplete; }): Promise<void>;
|
dispatch(event: { [key: string]: unknown; type: FluxEvents; }): Promise<void>;
|
||||||
isDispatching(): boolean;
|
isDispatching(): boolean;
|
||||||
subscribe(event: FluxEventsAutoComplete, callback: (data: any) => void): void;
|
subscribe(event: FluxEvents, callback: (data: any) => void): void;
|
||||||
unsubscribe(event: FluxEventsAutoComplete, callback: (data: any) => void): void;
|
unsubscribe(event: FluxEvents, callback: (data: any) => void): void;
|
||||||
wait(callback: () => void): void;
|
wait(callback: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!!
|
|
||||||
import "~plugins";
|
|
||||||
|
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
export * as Components from "./components";
|
export * as Components from "./components";
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
|
|
@ -32,8 +29,7 @@ export { PlainSettings, Settings };
|
||||||
import "./utils/quickCss";
|
import "./utils/quickCss";
|
||||||
import "./webpack/patchWebpack";
|
import "./webpack/patchWebpack";
|
||||||
|
|
||||||
import { openUpdaterModal } from "@components/settings/tabs/updater";
|
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||||
import { IS_WINDOWS } from "@utils/constants";
|
|
||||||
import { StartAt } from "@utils/types";
|
import { StartAt } from "@utils/types";
|
||||||
|
|
||||||
import { get as dsGet } from "./api/DataStore";
|
import { get as dsGet } from "./api/DataStore";
|
||||||
|
|
@ -165,7 +161,7 @@ init();
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
startAllPlugins(StartAt.DOMContentLoaded);
|
startAllPlugins(StartAt.DOMContentLoaded);
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) {
|
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||||
document.head.append(Object.assign(document.createElement("style"), {
|
document.head.append(Object.assign(document.createElement("style"), {
|
||||||
id: "vencord-native-titlebar-style",
|
id: "vencord-native-titlebar-style",
|
||||||
textContent: "[class*=titleBar]{display: none!important}"
|
textContent: "[class*=titleBar]{display: none!important}"
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import BadgeAPIPlugin from "plugins/_api/badges";
|
|
||||||
import { ComponentType, HTMLProps } from "react";
|
import { ComponentType, HTMLProps } from "react";
|
||||||
|
|
||||||
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
export const enum BadgePosition {
|
export const enum BadgePosition {
|
||||||
START,
|
START,
|
||||||
END
|
END
|
||||||
|
|
@ -34,9 +35,7 @@ export interface ProfileBadge {
|
||||||
image?: string;
|
image?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
|
||||||
/** Action to perform when you right click the badge */
|
|
||||||
onContextMenu?(event: React.MouseEvent, props: BadgeUserArgs & BadgeUserArgs): void;
|
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
|
|
@ -78,34 +77,21 @@ export function removeProfileBadge(badge: ProfileBadge) {
|
||||||
export function _getBadges(args: BadgeUserArgs) {
|
export function _getBadges(args: BadgeUserArgs) {
|
||||||
const badges = [] as ProfileBadge[];
|
const badges = [] as ProfileBadge[];
|
||||||
for (const badge of Badges) {
|
for (const badge of Badges) {
|
||||||
if (badge.shouldShow && !badge.shouldShow(args)) {
|
if (!badge.shouldShow || badge.shouldShow(args)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const b = badge.getBadges
|
const b = badge.getBadges
|
||||||
? badge.getBadges(args).map(badge => ({
|
? badge.getBadges(args).map(b => {
|
||||||
...args,
|
b.component &&= ErrorBoundary.wrap(b.component, { noop: true });
|
||||||
...badge,
|
return b;
|
||||||
component: badge.component && ErrorBoundary.wrap(badge.component, { noop: true })
|
})
|
||||||
}))
|
: [{ ...badge, ...args }];
|
||||||
: [{ ...args, ...badge }];
|
|
||||||
|
|
||||||
if (badge.position === BadgePosition.START) {
|
badge.position === BadgePosition.START
|
||||||
badges.unshift(...b);
|
? badges.unshift(...b)
|
||||||
} else {
|
: badges.push(...b);
|
||||||
badges.push(...b);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId);
|
||||||
const donorBadges = BadgeAPIPlugin.getDonorBadges(args.userId);
|
if (donorBadges) badges.unshift(...donorBadges);
|
||||||
if (donorBadges) {
|
|
||||||
badges.unshift(
|
|
||||||
...donorBadges.map(badge => ({
|
|
||||||
...args,
|
|
||||||
...badge,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return badges;
|
return badges;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import type { Channel, CloudUpload, CustomEmoji, Message } from "@vencord/discord-types";
|
import type { Channel, CustomEmoji, Message } from "@vencord/discord-types";
|
||||||
import { MessageStore } from "@webpack/common";
|
import { MessageStore } from "@webpack/common";
|
||||||
import type { Promisable } from "type-fest";
|
import type { Promisable } from "type-fest";
|
||||||
|
|
||||||
|
|
@ -30,6 +30,30 @@ export interface MessageObject {
|
||||||
tts: boolean;
|
tts: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Upload {
|
||||||
|
classification: string;
|
||||||
|
currentSize: number;
|
||||||
|
description: string | null;
|
||||||
|
filename: string;
|
||||||
|
id: string;
|
||||||
|
isImage: boolean;
|
||||||
|
isVideo: boolean;
|
||||||
|
item: {
|
||||||
|
file: File;
|
||||||
|
platform: number;
|
||||||
|
};
|
||||||
|
loaded: number;
|
||||||
|
mimeType: string;
|
||||||
|
preCompressionSize: number;
|
||||||
|
responseUrl: string;
|
||||||
|
sensitive: boolean;
|
||||||
|
showLargeMessageDialog: boolean;
|
||||||
|
spoiler: boolean;
|
||||||
|
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED";
|
||||||
|
uniqueId: string;
|
||||||
|
uploadedFilename: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageReplyOptions {
|
export interface MessageReplyOptions {
|
||||||
messageReference: Message["messageReference"];
|
messageReference: Message["messageReference"];
|
||||||
allowedMentions?: {
|
allowedMentions?: {
|
||||||
|
|
@ -38,9 +62,9 @@ export interface MessageReplyOptions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageOptions {
|
export interface MessageExtra {
|
||||||
stickers?: string[];
|
stickers?: string[];
|
||||||
uploads?: CloudUpload[];
|
uploads?: Upload[];
|
||||||
replyOptions: MessageReplyOptions;
|
replyOptions: MessageReplyOptions;
|
||||||
content: string;
|
content: string;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
|
|
@ -48,17 +72,17 @@ export interface MessageOptions {
|
||||||
openWarningPopout: (props: any) => any;
|
openWarningPopout: (props: any) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageSendListener = (channelId: string, messageObj: MessageObject, options: MessageOptions) => Promisable<void | { cancel: boolean; }>;
|
export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||||
export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
|
export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
|
||||||
|
|
||||||
const sendListeners = new Set<MessageSendListener>();
|
const sendListeners = new Set<MessageSendListener>();
|
||||||
const editListeners = new Set<MessageEditListener>();
|
const editListeners = new Set<MessageEditListener>();
|
||||||
|
|
||||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, options: MessageOptions, replyOptions: MessageReplyOptions) {
|
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
|
||||||
options.replyOptions = replyOptions;
|
extra.replyOptions = replyOptions;
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = await listener(channelId, messageObj, options);
|
const result = await listener(channelId, messageObj, extra);
|
||||||
if (result?.cancel) {
|
if (result?.cancel) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Message } from "@vencord/discord-types";
|
import { FluxStore, Message } from "@vencord/discord-types";
|
||||||
import { MessageCache, MessageStore } from "@webpack/common";
|
import { MessageCache, MessageStore } from "@webpack/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,5 +24,5 @@ export function updateMessage(channelId: string, messageId: string, fields?: Par
|
||||||
});
|
});
|
||||||
|
|
||||||
MessageCache.commit(newChannelMessageCache);
|
MessageCache.commit(newChannelMessageCache);
|
||||||
MessageStore.emitChange();
|
(MessageStore as unknown as FluxStore).emitChange();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@
|
||||||
* 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 ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { isPrimitiveReactNode } from "@utils/react";
|
|
||||||
import { waitFor } from "@webpack";
|
import { waitFor } from "@webpack";
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
let NoticesModule: any;
|
let NoticesModule: any;
|
||||||
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||||
|
|
@ -39,11 +36,7 @@ export function nextNotice() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showNotice(message: ReactNode, buttonText: string, onOkClick: () => void) {
|
export function showNotice(message: string, buttonText: string, onOkClick: () => void) {
|
||||||
const notice = isPrimitiveReactNode(message)
|
noticesQueue.push(["GENERIC", message, buttonText, onOkClick]);
|
||||||
? message
|
|
||||||
: <ErrorBoundary fallback={() => "Error Showing Notice"}>{message}</ErrorBoundary>;
|
|
||||||
|
|
||||||
noticesQueue.push(["GENERIC", notice, buttonText, onOkClick]);
|
|
||||||
if (!currentNotice) nextNotice();
|
if (!currentNotice) nextNotice();
|
||||||
}
|
}
|
||||||
|
|
@ -104,9 +104,11 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
{timeout !== 0 && !permanent && (
|
{timeout !== 0 && !permanent && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ import * as DataStore from "@api/DataStore";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
|
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
|
||||||
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { Alerts, Button, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import type { DispatchWithoutAction } from "react";
|
import type { DispatchWithoutAction } from "react";
|
||||||
|
|
||||||
|
|
@ -103,9 +103,21 @@ export function useLogs() {
|
||||||
|
|
||||||
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
||||||
const [removing, setRemoving] = useState(false);
|
const [removing, setRemoving] = useState(false);
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const div = ref.current!;
|
||||||
|
|
||||||
|
const setHeight = () => {
|
||||||
|
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
|
||||||
|
div.style.height = `${div.clientHeight}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
setHeight();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cl("wrapper", { removing })}>
|
<div className={cl("wrapper", { removing })} ref={ref}>
|
||||||
<NotificationComponent
|
<NotificationComponent
|
||||||
{...data}
|
{...data}
|
||||||
permanent={true}
|
permanent={true}
|
||||||
|
|
@ -117,8 +129,8 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
||||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
setTimeout(() => deleteNotification(data.timestamp), 200);
|
||||||
}}
|
}}
|
||||||
richBody={
|
richBody={
|
||||||
<div className={cl("body-wrapper")}>
|
<div className={cl("body")}>
|
||||||
<div className={cl("body")}>{data.body}</div>
|
{data.body}
|
||||||
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
|
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -139,14 +151,9 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListScrollerThin
|
<div className={cl("container")}>
|
||||||
className={cl("container")}
|
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
|
||||||
sections={[log.length]}
|
</div>
|
||||||
sectionHeight={0}
|
|
||||||
rowHeight={120}
|
|
||||||
renderSection={() => null}
|
|
||||||
renderRow={item => <NotificationEntry data={log[item.row]} key={log[item.row].id} />}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,15 +161,15 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
|
||||||
const [log, pending] = useLogs();
|
const [log, pending] = useLogs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}>
|
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
|
||||||
<ModalCloseButton onClick={close} />
|
<ModalCloseButton onClick={close} />
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<ModalContent>
|
||||||
<NotificationLog log={log} pending={pending} />
|
<NotificationLog log={log} pending={pending} />
|
||||||
</div>
|
</ModalContent>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Flex>
|
<Flex>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text-default);
|
color: var(--text-default);
|
||||||
background-color: var(--background-base-low);
|
background-color: var(--background-base-lower-alt);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visual-refresh .vc-notification-root {
|
||||||
|
background-color: var(--background-base-low);
|
||||||
|
}
|
||||||
|
|
||||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
|
|
@ -28,7 +32,6 @@
|
||||||
|
|
||||||
.vc-notification-content {
|
.vc-notification-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-header {
|
.vc-notification-header {
|
||||||
|
|
@ -78,11 +81,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-modal {
|
|
||||||
max-width: 962px;
|
|
||||||
width: clamp(var(--modal-width-large, 800px), 962px, 85vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-empty {
|
.vc-notification-log-empty {
|
||||||
height: 218px;
|
height: 218px;
|
||||||
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
||||||
|
|
@ -90,23 +88,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-container {
|
.vc-notification-log-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
max-height: min(750px, 75vh);
|
overflow: hidden;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-wrapper {
|
.vc-notification-log-wrapper {
|
||||||
height: 120px;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: 200ms ease;
|
transition: 200ms ease;
|
||||||
transition-property: height, opacity;
|
transition-property: height, opacity;
|
||||||
|
|
||||||
/* stylelint-disable-next-line no-descending-specificity */
|
|
||||||
.vc-notification-root {
|
|
||||||
height: 104px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-notification-log-wrapper:not(:last-child) {
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-removing {
|
.vc-notification-log-removing {
|
||||||
|
|
@ -115,18 +109,9 @@
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-body-wrapper {
|
.vc-notification-log-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-body {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 1.2em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-timestamp {
|
.vc-notification-log-timestamp {
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function AddonBadge({ text, color }) {
|
export function Badge({ text, color }) {
|
||||||
return (
|
return (
|
||||||
<div className="vc-addon-badge" style={{
|
<div className="vc-plugins-badge" style={{
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
justifySelf: "flex-end",
|
justifySelf: "flex-end",
|
||||||
marginLeft: "auto"
|
marginLeft: "auto"
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import { React, TextInput } from "@webpack/common";
|
import { React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
|
// TODO: Refactor settings to use this as well
|
||||||
interface TextInputProps {
|
interface TextInputProps {
|
||||||
/**
|
/**
|
||||||
* WARNING: Changing this between renders will have no effect!
|
* WARNING: Changing this between renders will have no effect!
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@
|
||||||
* 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 { Heart } from "@components/Heart";
|
|
||||||
import { ButtonProps } from "@vencord/discord-types";
|
import { ButtonProps } from "@vencord/discord-types";
|
||||||
import { Button } from "@webpack/common";
|
import { Button } from "@webpack/common";
|
||||||
|
|
||||||
|
import { Heart } from "./Heart";
|
||||||
|
|
||||||
export default function DonateButton({
|
export default function DonateButton({
|
||||||
look = Button.Looks.LINK,
|
look = Button.Looks.LINK,
|
||||||
color = Button.Colors.TRANSPARENT,
|
color = Button.Colors.TRANSPARENT,
|
||||||
|
|
@ -27,6 +27,7 @@ interface BaseIconProps extends IconProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
type IconProps = JSX.IntrinsicElements["svg"];
|
type IconProps = JSX.IntrinsicElements["svg"];
|
||||||
|
type ImageProps = JSX.IntrinsicElements["img"];
|
||||||
|
|
||||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./ContributorModal.css";
|
import "./contributorModal.css";
|
||||||
|
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
|
@ -19,8 +19,8 @@ import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromSto
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
import { PluginCard } from ".";
|
||||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||||
import { PluginCard } from "./PluginCard";
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-author-modal-");
|
const cl = classNameFactory("vc-author-modal-");
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 4px solid var(--background-base-lowest);
|
border: 4px solid var(--background-tertiary);
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6,10 +6,11 @@
|
||||||
|
|
||||||
import "./LinkIconButton.css";
|
import "./LinkIconButton.css";
|
||||||
|
|
||||||
import { GithubIcon, WebsiteIcon } from "@components/Icons";
|
|
||||||
import { getTheme, Theme } from "@utils/discord";
|
import { getTheme, Theme } from "@utils/discord";
|
||||||
import { MaskedLink, Tooltip } from "@webpack/common";
|
import { MaskedLink, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
import { GithubIcon, WebsiteIcon } from "..";
|
||||||
|
|
||||||
export function GithubLinkIcon() {
|
export function GithubLinkIcon() {
|
||||||
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
|
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
|
||||||
return <GithubIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
|
return <GithubIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
|
||||||
|
|
@ -23,32 +23,41 @@ import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
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 { debounce } from "@shared/debounce";
|
|
||||||
import { gitRemote } from "@shared/vencordUserAgent";
|
import { gitRemote } from "@shared/vencordUserAgent";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { isObjectEmpty } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
import { User } from "@vencord/discord-types";
|
import { User } from "@vencord/discord-types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { Clickable, FluxDispatcher, Forms, React, Text, Tooltip, useEffect, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
|
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
import { PluginMeta } from "~plugins";
|
import { PluginMeta } from "~plugins";
|
||||||
|
|
||||||
import { OptionComponentMap } from "./components";
|
import {
|
||||||
|
ISettingCustomElementProps,
|
||||||
|
ISettingElementProps,
|
||||||
|
SettingBooleanComponent,
|
||||||
|
SettingCustomComponent,
|
||||||
|
SettingNumericComponent,
|
||||||
|
SettingSelectComponent,
|
||||||
|
SettingSliderComponent,
|
||||||
|
SettingTextComponent
|
||||||
|
} from "./components";
|
||||||
import { openContributorModal } from "./ContributorModal";
|
import { openContributorModal } from "./ContributorModal";
|
||||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugin-modal-");
|
const cl = classNameFactory("vc-plugin-modal-");
|
||||||
|
|
||||||
|
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||||
|
|
||||||
interface PluginModalProps extends ModalProps {
|
interface PluginModalProps extends ModalProps {
|
||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
onRestartNeeded(key: string): void;
|
onRestartNeeded(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
|
function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
|
||||||
|
|
@ -59,22 +68,39 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
|
||||||
/** To stop discord making unwanted requests... */
|
/** To stop discord making unwanted requests... */
|
||||||
bot: true,
|
bot: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "USER_UPDATE",
|
type: "USER_UPDATE",
|
||||||
user: newUser,
|
user: newUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any> | ISettingCustomElementProps<any>>> = {
|
||||||
|
[OptionType.STRING]: SettingTextComponent,
|
||||||
|
[OptionType.NUMBER]: SettingNumericComponent,
|
||||||
|
[OptionType.BIGINT]: SettingNumericComponent,
|
||||||
|
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
||||||
|
[OptionType.SELECT]: SettingSelectComponent,
|
||||||
|
[OptionType.SLIDER]: SettingSliderComponent,
|
||||||
|
[OptionType.COMPONENT]: SettingCustomComponent,
|
||||||
|
[OptionType.CUSTOM]: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||||
|
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
||||||
|
|
||||||
const pluginSettings = useSettings().plugins[plugin.name];
|
const pluginSettings = useSettings().plugins[plugin.name];
|
||||||
|
|
||||||
|
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
||||||
|
const [saveError, setSaveError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
|
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
|
||||||
|
|
||||||
const [authors, setAuthors] = useState<Partial<User>[]>([]);
|
React.useEffect(() => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const user of plugin.authors.slice(0, 6)) {
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
const author = user.id
|
const author = user.id
|
||||||
|
|
@ -87,40 +113,63 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
})();
|
})();
|
||||||
}, [plugin.authors]);
|
}, [plugin.authors]);
|
||||||
|
|
||||||
function renderSettings() {
|
async function saveAndClose() {
|
||||||
if (!hasSettings || !plugin.options)
|
if (!plugin.options) {
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.beforeSave) {
|
||||||
|
const result = await Promise.resolve(plugin.beforeSave(tempSettings));
|
||||||
|
if (result !== true) {
|
||||||
|
setSaveError(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let restartNeeded = false;
|
||||||
|
for (const [key, value] of Object.entries(tempSettings)) {
|
||||||
|
const option = plugin.options[key];
|
||||||
|
pluginSettings[key] = value;
|
||||||
|
|
||||||
|
if (option.type === OptionType.CUSTOM) continue;
|
||||||
|
if (option?.restartNeeded) restartNeeded = true;
|
||||||
|
}
|
||||||
|
if (restartNeeded) onRestartNeeded();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettings() {
|
||||||
|
if (!hasSettings || !plugin.options) {
|
||||||
|
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||||
|
} else {
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||||
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
|
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
|
||||||
|
|
||||||
function onChange(newValue: any) {
|
function onChange(newValue: any) {
|
||||||
const option = plugin.options?.[key];
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
if (!option || option.type === OptionType.CUSTOM) return;
|
|
||||||
|
|
||||||
pluginSettings[key] = newValue;
|
|
||||||
|
|
||||||
if (option.restartNeeded) onRestartNeeded(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component = OptionComponentMap[setting.type];
|
function onError(hasError: boolean) {
|
||||||
|
setErrors(e => ({ ...e, [key]: hasError }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = Components[setting.type];
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
id={key}
|
id={key}
|
||||||
key={key}
|
key={key}
|
||||||
option={setting}
|
option={setting}
|
||||||
onChange={debounce(onChange)}
|
onChange={onChange}
|
||||||
|
onError={onError}
|
||||||
pluginSettings={pluginSettings}
|
pluginSettings={pluginSettings}
|
||||||
definedSettings={plugin.settings}
|
definedSettings={plugin.settings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return <Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>{options}</Flex>;
|
||||||
<div className="vc-plugins-settings">
|
}
|
||||||
{options}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMoreUsers(_label: string, count: number) {
|
function renderMoreUsers(_label: string, count: number) {
|
||||||
|
|
@ -143,16 +192,38 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function switchToPopout() {
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
const PopoutKey = `DISCORD_VENCORD_PLUGIN_SETTINGS_MODAL_${plugin.name}`;
|
||||||
|
PopoutActions.open(
|
||||||
|
PopoutKey,
|
||||||
|
() => <PluginModal
|
||||||
|
transitionState={transitionState}
|
||||||
|
plugin={plugin}
|
||||||
|
onRestartNeeded={onRestartNeeded}
|
||||||
|
onClose={() => PopoutActions.close(PopoutKey)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const pluginMeta = PluginMeta[plugin.name];
|
const pluginMeta = PluginMeta[plugin.name];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||||
<ModalHeader separator={false} className={Margins.bottom8}>
|
<ModalHeader separator={false}>
|
||||||
<Text variant="heading-xl/bold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<Button look={Button.Looks.BLANK} onClick={switchToPopout}>
|
||||||
|
<OpenExternalIcon aria-label="Open in Popout" />
|
||||||
|
</Button>
|
||||||
|
*/}
|
||||||
<ModalCloseButton onClick={onClose} />
|
<ModalCloseButton onClick={onClose} />
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
<ModalContent>
|
||||||
<ModalContent className={Margins.bottom16}>
|
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Flex className={cl("info")}>
|
<Flex className={cl("info")}>
|
||||||
<Forms.FormText className={cl("description")}>{plugin.description}</Forms.FormText>
|
<Forms.FormText className={cl("description")}>{plugin.description}</Forms.FormText>
|
||||||
|
|
@ -169,14 +240,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text variant="heading-lg/semibold" className={classes(Margins.top8, Margins.bottom8)}>Authors</Text>
|
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
||||||
<div style={{ width: "fit-content" }}>
|
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
||||||
<UserSummaryItem
|
<UserSummaryItem
|
||||||
users={authors}
|
users={authors}
|
||||||
|
count={plugin.authors.length}
|
||||||
guildId={undefined}
|
guildId={undefined}
|
||||||
renderIcon={false}
|
renderIcon={false}
|
||||||
max={6}
|
max={6}
|
||||||
showDefaultAvatarsForNullUsers
|
showDefaultAvatarsForNullUsers
|
||||||
|
showUserPopout
|
||||||
renderMoreUsers={renderMoreUsers}
|
renderMoreUsers={renderMoreUsers}
|
||||||
renderUser={(user: User) => (
|
renderUser={(user: User) => (
|
||||||
<Clickable
|
<Clickable
|
||||||
|
|
@ -194,32 +267,59 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
|
||||||
{!!plugin.settingsAboutComponent && (
|
{!!plugin.settingsAboutComponent && (
|
||||||
<div className={Margins.top16}>
|
<div className={Margins.bottom8}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
||||||
<plugin.settingsAboutComponent />
|
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Forms.FormSection className={Margins.bottom16}>
|
||||||
<Forms.FormSection>
|
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
||||||
<Text variant="heading-lg/semibold" className={classes(Margins.top16, Margins.bottom8)}>Settings</Text>
|
|
||||||
{renderSettings()}
|
{renderSettings()}
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
{hasSettings && <ModalFooter>
|
||||||
|
<Flex flexDirection="column" style={{ width: "100%" }}>
|
||||||
|
<Flex style={{ marginLeft: "auto" }}>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.PRIMARY}
|
||||||
|
look={Button.Looks.LINK}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.BRAND}
|
||||||
|
onClick={saveAndClose}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
disabled={!canSubmit()}
|
||||||
|
>
|
||||||
|
Save & Close
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
||||||
|
</Flex>
|
||||||
|
</ModalFooter>}
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string, key: string) => void) {
|
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string) => void) {
|
||||||
openModal(modalProps => (
|
openModal(modalProps => (
|
||||||
<PluginModal
|
<PluginModal
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
onRestartNeeded={(key: string) => onRestartNeeded?.(plugin.name, key)}
|
onRestartNeeded={() => onRestartNeeded?.(plugin.name)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||||
|
import { PluginOptionBoolean } from "@utils/types";
|
||||||
|
import { Forms, React, Switch } from "@webpack/common";
|
||||||
|
|
||||||
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
|
export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||||
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
|
const [state, setState] = React.useState(def ?? false);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue: boolean): void {
|
||||||
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else {
|
||||||
|
setError(null);
|
||||||
|
setState(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Switch
|
||||||
|
value={state}
|
||||||
|
onChange={handleChange}
|
||||||
|
note={option.description}
|
||||||
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
|
{...option.componentProps}
|
||||||
|
hideBorder
|
||||||
|
style={{ marginBottom: "0.5em" }}
|
||||||
|
>
|
||||||
|
{wordsToTitle(wordsFromCamel(id))}
|
||||||
|
</Switch>
|
||||||
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
|
|
||||||
import { PluginOptionComponent } from "@utils/types";
|
import { PluginOptionComponent } from "@utils/types";
|
||||||
|
|
||||||
import { ComponentSettingProps } from "./Common";
|
import { ISettingCustomElementProps } from ".";
|
||||||
|
|
||||||
export function ComponentSetting({ option, onChange }: ComponentSettingProps<PluginOptionComponent>) {
|
export function SettingCustomComponent({ option, onChange, onError }: ISettingCustomElementProps<PluginOptionComponent>) {
|
||||||
return option.component({ setValue: onChange, option });
|
return option.component({ setValue: onChange, setError: onError, option });
|
||||||
}
|
}
|
||||||
|
|
@ -16,49 +16,58 @@
|
||||||
* 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 { Margins } from "@utils/margins";
|
||||||
|
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||||
import { OptionType, PluginOptionNumber } from "@utils/types";
|
import { OptionType, PluginOptionNumber } from "@utils/types";
|
||||||
import { React, TextInput, useState } from "@webpack/common";
|
import { Forms, React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
export function NumberSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionNumber>) {
|
export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||||
function serialize(value: any) {
|
function serialize(value: any) {
|
||||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||||
return Number(value);
|
return Number(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [state, setState] = useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
|
const [state, setState] = React.useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
function handleChange(newValue: any) {
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
|
||||||
setError(resolveError(isValid));
|
setError(null);
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
if (isValid === true) {
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
onChange(serialize(newValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||||
|
onChange(serialize(newValue));
|
||||||
} else {
|
} else {
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
|
onChange(serialize(newValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection name={id} description={option.description} error={error}>
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
pattern="-?[0-9]+"
|
pattern="-?[0-9]+"
|
||||||
placeholder={option.placeholder ?? "Enter a number"}
|
|
||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
placeholder={option.placeholder ?? "Enter a number"}
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -16,41 +16,50 @@
|
||||||
* 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 { Margins } from "@utils/margins";
|
||||||
|
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||||
import { PluginOptionSelect } from "@utils/types";
|
import { PluginOptionSelect } from "@utils/types";
|
||||||
import { React, Select, useState } from "@webpack/common";
|
import { Forms, React, Select } from "@webpack/common";
|
||||||
|
|
||||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SelectSetting({ option, pluginSettings, definedSettings, onChange, id }: SettingProps<PluginOptionSelect>) {
|
export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||||
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||||
|
|
||||||
const [state, setState] = useState<any>(def ?? null);
|
const [state, setState] = React.useState<any>(def ?? null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
function handleChange(newValue: any) {
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else {
|
||||||
|
setError(null);
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
setError(resolveError(isValid));
|
|
||||||
|
|
||||||
if (isValid === true) {
|
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection name={id} description={option.description} error={error}>
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom16} type="description">{option.description}</Forms.FormText>
|
||||||
<Select
|
<Select
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
options={option.options}
|
options={option.options}
|
||||||
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
select={handleChange}
|
select={handleChange}
|
||||||
isSelected={v => v === state}
|
isSelected={v => v === state}
|
||||||
serialize={v => String(v)}
|
serialize={v => String(v)}
|
||||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -16,29 +16,46 @@
|
||||||
* 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 { Margins } from "@utils/margins";
|
||||||
|
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||||
import { PluginOptionSlider } from "@utils/types";
|
import { PluginOptionSlider } from "@utils/types";
|
||||||
import { React, Slider, useState } from "@webpack/common";
|
import { Forms, React, Slider } from "@webpack/common";
|
||||||
|
|
||||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SliderSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionSlider>) {
|
export function makeRange(start: number, end: number, step = 1) {
|
||||||
|
const ranges: number[] = [];
|
||||||
|
for (let value = start; value <= end; value += step) {
|
||||||
|
ranges.push(Math.round(value * 100) / 100);
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
||||||
const def = pluginSettings[id] ?? option.default;
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue: number): void {
|
function handleChange(newValue: number): void {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
setError(resolveError(isValid));
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else {
|
||||||
if (isValid === true) {
|
setError(null);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection name={id} description={option.description} error={error}>
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
|
||||||
<Slider
|
<Slider
|
||||||
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
markers={option.markers}
|
markers={option.markers}
|
||||||
minValue={option.markers[0]}
|
minValue={option.markers[0]}
|
||||||
maxValue={option.markers[option.markers.length - 1]}
|
maxValue={option.markers[option.markers.length - 1]}
|
||||||
|
|
@ -46,10 +63,9 @@ export function SliderSetting({ option, pluginSettings, definedSettings, id, onC
|
||||||
onValueChange={handleChange}
|
onValueChange={handleChange}
|
||||||
onValueRender={(v: number) => String(v.toFixed(2))}
|
onValueRender={(v: number) => String(v.toFixed(2))}
|
||||||
stickToMarkers={option.stickToMarkers ?? true}
|
stickToMarkers={option.stickToMarkers ?? true}
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,37 +16,45 @@
|
||||||
* 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 { Margins } from "@utils/margins";
|
||||||
|
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||||
import { PluginOptionString } from "@utils/types";
|
import { PluginOptionString } from "@utils/types";
|
||||||
import { React, TextInput, useState } from "@webpack/common";
|
import { Forms, React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function TextSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionString>) {
|
export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||||
const [state, setState] = useState(pluginSettings[id] ?? option.default ?? null);
|
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
function handleChange(newValue: string) {
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else setError(null);
|
||||||
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
setError(resolveError(isValid));
|
|
||||||
|
|
||||||
if (isValid === true) {
|
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection name={id} description={option.description} error={error}>
|
<Forms.FormSection>
|
||||||
|
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={option.placeholder ?? "Enter a value"}
|
|
||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
maxLength={null}
|
placeholder={option.placeholder ?? "Enter a value"}
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
|
maxLength={null}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
43
src/components/PluginSettings/components/index.ts
Normal file
43
src/components/PluginSettings/components/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||||
|
|
||||||
|
interface ISettingElementPropsBase<T> {
|
||||||
|
option: T;
|
||||||
|
onChange(newValue: any): void;
|
||||||
|
pluginSettings: {
|
||||||
|
[setting: string]: any;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
onError(hasError: boolean): void;
|
||||||
|
definedSettings?: DefinedSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
|
||||||
|
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
|
||||||
|
|
||||||
|
export * from "../../Badge";
|
||||||
|
export * from "./SettingBooleanComponent";
|
||||||
|
export * from "./SettingCustomComponent";
|
||||||
|
export * from "./SettingNumericComponent";
|
||||||
|
export * from "./SettingSelectComponent";
|
||||||
|
export * from "./SettingSliderComponent";
|
||||||
|
export * from "./SettingTextComponent";
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
.vc-author-modal-name {
|
.vc-author-modal-name {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
background: var(--background-base-lowest);
|
background: var(--background-tertiary);
|
||||||
border-radius: 0 9999px 9999px 0;
|
border-radius: 0 9999px 9999px 0;
|
||||||
padding: 6px 0.8em 6px 0.5em;
|
padding: 6px 0.8em 6px 0.5em;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
background: var(--background-base-lowest);
|
background: var(--background-tertiary);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
left: -32px;
|
left: -32px;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -19,32 +19,51 @@
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { useSettings } from "@api/Settings";
|
import { showNotice } from "@api/Notices";
|
||||||
|
import { Settings, useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
|
import { CogWheel, InfoIcon } from "@components/Icons";
|
||||||
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
|
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||||
|
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { useAwaiter, useCleanupEffect } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
|
import { Plugin } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Tooltip, useMemo, useState } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
|
|
||||||
import Plugins, { ExcludedPlugins } from "~plugins";
|
import Plugins, { ExcludedPlugins } from "~plugins";
|
||||||
|
|
||||||
import { PluginCard } from "./PluginCard";
|
// Avoid circular dependency
|
||||||
|
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
||||||
|
|
||||||
export const cl = classNameFactory("vc-plugins-");
|
const cl = classNameFactory("vc-plugins-");
|
||||||
export const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
|
||||||
const InputStyles = findByPropsLazy("inputWrapper", "inputError", "error");
|
const InputStyles = findByPropsLazy("inputWrapper", "inputError", "error");
|
||||||
|
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
||||||
|
|
||||||
|
|
||||||
|
function showErrorToast(message: string) {
|
||||||
|
Toasts.show({
|
||||||
|
message,
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
id: Toasts.genId(),
|
||||||
|
options: {
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
return (
|
return (
|
||||||
<Card className={classes(cl("info-card"), required && "vc-warning-card")}>
|
<Card className={classes(cl("info-card"), required && "vc-warning-card")}>
|
||||||
{required
|
{required ? (
|
||||||
? (
|
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
||||||
<Forms.FormText className={cl("dep-text")}>
|
<Forms.FormText className={cl("dep-text")}>
|
||||||
|
|
@ -54,8 +73,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
Restart
|
Restart
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)
|
) : (
|
||||||
: (
|
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
||||||
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
||||||
|
|
@ -66,6 +84,88 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
|
plugin: Plugin;
|
||||||
|
disabled: boolean;
|
||||||
|
onRestartNeeded(name: string): void;
|
||||||
|
isNew?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
|
const settings = Settings.plugins[plugin.name];
|
||||||
|
|
||||||
|
const isEnabled = () => Vencord.Plugins.isPluginEnabled(plugin.name);
|
||||||
|
|
||||||
|
function toggleEnabled() {
|
||||||
|
const wasEnabled = isEnabled();
|
||||||
|
|
||||||
|
// If we're enabling a plugin, make sure all deps are enabled recursively.
|
||||||
|
if (!wasEnabled) {
|
||||||
|
const { restartNeeded, failures } = startDependenciesRecursive(plugin);
|
||||||
|
if (failures.length) {
|
||||||
|
logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`);
|
||||||
|
showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null);
|
||||||
|
return;
|
||||||
|
} else if (restartNeeded) {
|
||||||
|
// If any dependencies have patches, don't start the plugin yet.
|
||||||
|
settings.enabled = true;
|
||||||
|
onRestartNeeded(plugin.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
||||||
|
if (plugin.patches?.length) {
|
||||||
|
settings.enabled = !wasEnabled;
|
||||||
|
onRestartNeeded(plugin.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
||||||
|
if (wasEnabled && !plugin.started) {
|
||||||
|
settings.enabled = !wasEnabled;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
settings.enabled = false;
|
||||||
|
|
||||||
|
const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`;
|
||||||
|
logger.error(msg);
|
||||||
|
showErrorToast(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.enabled = !wasEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddonCard
|
||||||
|
name={plugin.name}
|
||||||
|
description={plugin.description}
|
||||||
|
isNew={isNew}
|
||||||
|
enabled={isEnabled()}
|
||||||
|
setEnabled={toggleEnabled}
|
||||||
|
disabled={disabled}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
infoButton={
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
onClick={() => openPluginModal(plugin, onRestartNeeded)}
|
||||||
|
className={classes(ButtonClasses.button, cl("info-button"))}
|
||||||
|
>
|
||||||
|
{plugin.options && !isObjectEmpty(plugin.options)
|
||||||
|
? <CogWheel className={cl("info-icon")} />
|
||||||
|
: <InfoIcon className={cl("info-icon")} />}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const enum SearchStatus {
|
const enum SearchStatus {
|
||||||
ALL,
|
ALL,
|
||||||
ENABLED,
|
ENABLED,
|
||||||
|
|
@ -104,13 +204,12 @@ function ExcludedPluginsList({ search }: { search: string; }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PluginSettings() {
|
export default function PluginSettings() {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const changes = useMemo(() => new ChangeList<string>(), []);
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
|
||||||
useCleanupEffect(() => {
|
React.useEffect(() => {
|
||||||
if (changes.hasChanges)
|
return () => void (changes.hasChanges && Alerts.show({
|
||||||
Alerts.show({
|
|
||||||
title: "Restart required",
|
title: "Restart required",
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
|
|
@ -118,7 +217,7 @@ function PluginSettings() {
|
||||||
<div>{changes.map((s, i) => (
|
<div>{changes.map((s, i) => (
|
||||||
<>
|
<>
|
||||||
{i > 0 && ", "}
|
{i > 0 && ", "}
|
||||||
{Parser.parse("`" + s.split(".")[0] + "`")}
|
{Parser.parse("`" + s + "`")}
|
||||||
</>
|
</>
|
||||||
))}</div>
|
))}</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -126,10 +225,10 @@ function PluginSettings() {
|
||||||
confirmText: "Restart now",
|
confirmText: "Restart now",
|
||||||
cancelText: "Later!",
|
cancelText: "Later!",
|
||||||
onConfirm: () => location.reload()
|
onConfirm: () => location.reload()
|
||||||
});
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const depMap = useMemo(() => {
|
const depMap = React.useMemo(() => {
|
||||||
const o = {} as Record<string, string[]>;
|
const o = {} as Record<string, string[]>;
|
||||||
for (const plugin in Plugins) {
|
for (const plugin in Plugins) {
|
||||||
const deps = Plugins[plugin].dependencies;
|
const deps = Plugins[plugin].dependencies;
|
||||||
|
|
@ -143,12 +242,10 @@ function PluginSettings() {
|
||||||
return o;
|
return o;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sortedPlugins = useMemo(() =>
|
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
||||||
Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
|
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
||||||
|
|
||||||
const search = searchValue.value.toLowerCase();
|
const search = searchValue.value.toLowerCase();
|
||||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||||
|
|
@ -157,19 +254,9 @@ function PluginSettings() {
|
||||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||||
const { status } = searchValue;
|
const { status } = searchValue;
|
||||||
const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
|
const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
|
||||||
|
if (enabled && status === SearchStatus.DISABLED) return false;
|
||||||
switch (status) {
|
if (!enabled && status === SearchStatus.ENABLED) return false;
|
||||||
case SearchStatus.DISABLED:
|
if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
||||||
if (enabled) return false;
|
|
||||||
break;
|
|
||||||
case SearchStatus.ENABLED:
|
|
||||||
if (!enabled) return false;
|
|
||||||
break;
|
|
||||||
case SearchStatus.NEW:
|
|
||||||
if (!newPlugins?.includes(plugin.name)) return false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!search.length) return true;
|
if (!search.length) return true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -219,7 +306,7 @@ function PluginSettings() {
|
||||||
<PluginCard
|
<PluginCard
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onRestartNeeded={(name, key) => changes.handleChange(`${name}.${key}`)}
|
onRestartNeeded={name => changes.handleChange(name)}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
plugin={p}
|
plugin={p}
|
||||||
key={p.name}
|
key={p.name}
|
||||||
|
|
@ -230,7 +317,7 @@ function PluginSettings() {
|
||||||
} else {
|
} else {
|
||||||
plugins.push(
|
plugins.push(
|
||||||
<PluginCard
|
<PluginCard
|
||||||
onRestartNeeded={(name, key) => changes.handleChange(`${name}.${key}`)}
|
onRestartNeeded={name => changes.handleChange(name)}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
plugin={p}
|
plugin={p}
|
||||||
isNew={newPlugins?.includes(p.name)}
|
isNew={newPlugins?.includes(p.name)}
|
||||||
|
|
@ -262,6 +349,7 @@ function PluginSettings() {
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
isSelected={v => v === searchValue.status}
|
isSelected={v => v === searchValue.status}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
|
className={InputStyles.input}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -298,11 +386,9 @@ function PluginSettings() {
|
||||||
|
|
||||||
function makeDependencyList(deps: string[]) {
|
function makeDependencyList(deps: string[]) {
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||||
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||||
</>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default wrapTab(PluginSettings, "Plugins");
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
grid-template-columns: 1fr 150px;
|
grid-template-columns: 1fr 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-addon-badge {
|
.vc-plugins-badge {
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -74,9 +74,3 @@
|
||||||
.vc-plugins-info-icon:not(:hover, :focus) {
|
.vc-plugins-info-icon:not(:hover, :focus) {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-plugins-settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
@ -16,11 +16,11 @@
|
||||||
* 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 "./AddonCard.css";
|
import "./addonCard.css";
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { AddonBadge } from "@components/settings/PluginBadge";
|
import { Badge } from "@components/Badge";
|
||||||
import { Switch } from "@components/settings/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { Text, useRef } from "@webpack/common";
|
import { Text, useRef } from "@webpack/common";
|
||||||
import type { MouseEventHandler, ReactNode } from "react";
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
|
||||||
|
|
@ -44,7 +44,6 @@ interface Props {
|
||||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||||
const titleRef = useRef<HTMLDivElement>(null);
|
const titleRef = useRef<HTMLDivElement>(null);
|
||||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cl("card", { "card-disabled": disabled })}
|
className={cl("card", { "card-disabled": disabled })}
|
||||||
|
|
@ -68,10 +67,8 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
{isNew && <AddonBadge text="NEW" color="#ED4245" />}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{!!author && (
|
{!!author && (
|
||||||
<Text variant="text-md/normal" className={cl("author")}>
|
<Text variant="text-md/normal" className={cl("author")}>
|
||||||
{author}
|
{author}
|
||||||
|
|
@ -17,13 +17,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
|
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
import { Button, Card, Text } from "@webpack/common";
|
import { Button, Card, Text } from "@webpack/common";
|
||||||
|
|
||||||
function BackupAndRestoreTab() {
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
|
function BackupRestoreTab() {
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Backup & Restore">
|
<SettingsTab title="Backup & Restore">
|
||||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
|
|
@ -63,4 +64,4 @@ function BackupAndRestoreTab() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default wrapTab(BackupAndRestoreTab, "Backup & Restore");
|
export default wrapTab(BackupRestoreTab, "Backup & Restore");
|
||||||
|
|
@ -21,12 +21,13 @@ import { Settings, useSettings } from "@api/Settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Grid } from "@components/Grid";
|
import { Grid } from "@components/Grid";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
|
|
||||||
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
function validateUrl(url: string) {
|
function validateUrl(url: string) {
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
|
|
@ -4,46 +4,15 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity } from "@utils/misc";
|
import { identity } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { Button, Forms, Select, Slider, Text } from "@webpack/common";
|
import { Forms, Select, Slider, Text } from "@webpack/common";
|
||||||
|
|
||||||
export function NotificationSection() {
|
import { ErrorCard } from "..";
|
||||||
return (
|
|
||||||
<Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
|
|
||||||
<Flex>
|
|
||||||
<Button onClick={openNotificationSettingsModal}>
|
|
||||||
Notification Settings
|
|
||||||
</Button>
|
|
||||||
<Button onClick={openNotificationLogModal}>
|
|
||||||
View Notification Log
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openNotificationSettingsModal() {
|
export function NotificationSettings() {
|
||||||
openModal(props => (
|
|
||||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
|
|
||||||
<ModalCloseButton onClick={props.onClose} />
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalContent>
|
|
||||||
<NotificationSettings />
|
|
||||||
</ModalContent>
|
|
||||||
</ModalRoot>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationSettings() {
|
|
||||||
const settings = useSettings().notifications;
|
const settings = useSettings().notifications;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -120,3 +89,18 @@ function NotificationSettings() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openNotificationSettingsModal() {
|
||||||
|
openModal(props => (
|
||||||
|
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||||
|
<ModalHeader>
|
||||||
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
|
||||||
|
<ModalCloseButton onClick={props.onClose} />
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalContent>
|
||||||
|
<NotificationSettings />
|
||||||
|
</ModalContent>
|
||||||
|
</ModalRoot>
|
||||||
|
));
|
||||||
|
}
|
||||||
393
src/components/VencordSettings/PatchHelperTab.tsx
Normal file
393
src/components/VencordSettings/PatchHelperTab.tsx
Normal file
|
|
@ -0,0 +1,393 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
|
import { debounce } from "@shared/debounce";
|
||||||
|
import { copyToClipboard } from "@utils/clipboard";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
|
import { makeCodeblock } from "@utils/text";
|
||||||
|
import { Patch, ReplaceFn } from "@utils/types";
|
||||||
|
import { search } from "@webpack";
|
||||||
|
import { Button, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
|
// Do not include diff in non dev builds (side effects import)
|
||||||
|
if (IS_DEV) {
|
||||||
|
var differ = require("diff") as typeof import("diff");
|
||||||
|
}
|
||||||
|
|
||||||
|
const findCandidates = debounce(function ({ find, setModule, setError }) {
|
||||||
|
const candidates = search(find);
|
||||||
|
const keys = Object.keys(candidates);
|
||||||
|
const len = keys.length;
|
||||||
|
if (len === 0)
|
||||||
|
setError("No match. Perhaps that module is lazy loaded?");
|
||||||
|
else if (len !== 1)
|
||||||
|
setError("Multiple matches. Please refine your filter");
|
||||||
|
else
|
||||||
|
setModule([keys[0], candidates[keys[0]]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ReplacementComponentProps {
|
||||||
|
module: [id: number, factory: Function];
|
||||||
|
match: string;
|
||||||
|
replacement: string | ReplaceFn;
|
||||||
|
setReplacementError(error: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
|
||||||
|
const [id, fact] = module;
|
||||||
|
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
||||||
|
|
||||||
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||||
|
const src: string = fact.toString().replaceAll("\n", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
new RegExp(match);
|
||||||
|
} catch (e) {
|
||||||
|
return ["", [], []];
|
||||||
|
}
|
||||||
|
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||||
|
try {
|
||||||
|
const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
|
||||||
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||||
|
setReplacementError(void 0);
|
||||||
|
} catch (e) {
|
||||||
|
setReplacementError((e as Error).message);
|
||||||
|
return ["", [], []];
|
||||||
|
}
|
||||||
|
const m = src.match(canonicalMatch);
|
||||||
|
return [patched, m, makeDiff(src, patched, m)];
|
||||||
|
}, [id, match, replacement]);
|
||||||
|
|
||||||
|
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
|
||||||
|
if (!match || original === patched) return null;
|
||||||
|
|
||||||
|
const changeSize = patched.length - original.length;
|
||||||
|
|
||||||
|
// Use 200 surrounding characters of context
|
||||||
|
const start = Math.max(0, match.index! - 200);
|
||||||
|
const end = Math.min(original.length, match.index! + match[0].length + 200);
|
||||||
|
// (changeSize may be negative)
|
||||||
|
const endPatched = end + changeSize;
|
||||||
|
|
||||||
|
const context = original.slice(start, end);
|
||||||
|
const patchedContext = patched.slice(start, endPatched);
|
||||||
|
|
||||||
|
return differ.diffWordsWithSpace(context, patchedContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMatch() {
|
||||||
|
if (!matchResult)
|
||||||
|
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
|
||||||
|
|
||||||
|
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
|
||||||
|
const groups = matchResult.length > 1
|
||||||
|
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
|
||||||
|
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiff() {
|
||||||
|
return diff?.map((p, idx) => {
|
||||||
|
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
||||||
|
return <div key={idx} style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Module {id}</Forms.FormTitle>
|
||||||
|
|
||||||
|
{!!matchResult?.[0]?.length && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Match</Forms.FormTitle>
|
||||||
|
{renderMatch()}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{!!diff?.length && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Diff</Forms.FormTitle>
|
||||||
|
{renderDiff()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!diff?.length && (
|
||||||
|
<Button className={Margins.top20} onClick={() => {
|
||||||
|
try {
|
||||||
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||||
|
setCompileResult([true, "Compiled successfully"]);
|
||||||
|
} catch (err) {
|
||||||
|
setCompileResult([false, (err as Error).message]);
|
||||||
|
}
|
||||||
|
}}>Compile</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{compileResult &&
|
||||||
|
<Forms.FormText style={{ color: compileResult[0] ? "var(--status-positive)" : "var(--text-danger)" }}>
|
||||||
|
{compileResult[1]}
|
||||||
|
</Forms.FormText>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
const [isFunc, setIsFunc] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState<string>();
|
||||||
|
|
||||||
|
function onChange(v: string) {
|
||||||
|
setError(void 0);
|
||||||
|
|
||||||
|
if (isFunc) {
|
||||||
|
try {
|
||||||
|
const func = (0, eval)(v);
|
||||||
|
if (typeof func === "function")
|
||||||
|
setReplacement(() => func);
|
||||||
|
else
|
||||||
|
setError("Replacement must be a function");
|
||||||
|
} catch (e) {
|
||||||
|
setReplacement(v);
|
||||||
|
setError((e as Error).message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setReplacement(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => void (isFunc ? onChange(replacement) : setError(void 0)),
|
||||||
|
[isFunc]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
||||||
|
<Forms.FormTitle className="">replacement</Forms.FormTitle>
|
||||||
|
<TextInput
|
||||||
|
value={replacement?.toString()}
|
||||||
|
onChange={onChange}
|
||||||
|
error={error ?? replacementError}
|
||||||
|
/>
|
||||||
|
{!isFunc && (
|
||||||
|
<div>
|
||||||
|
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
||||||
|
{Object.entries({
|
||||||
|
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||||
|
"$$": "Insert a $",
|
||||||
|
"$&": "Insert the entire match",
|
||||||
|
"$`\u200b": "Insert the substring before the match",
|
||||||
|
"$'": "Insert the substring after the match",
|
||||||
|
"$n": "Insert the nth capturing group ($1, $2...)",
|
||||||
|
"$self": "Insert the plugin instance",
|
||||||
|
}).map(([placeholder, desc]) => (
|
||||||
|
<Forms.FormText key={placeholder}>
|
||||||
|
{Parser.parse("`" + placeholder + "`")}: {desc}
|
||||||
|
</Forms.FormText>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
className={Margins.top8}
|
||||||
|
value={isFunc}
|
||||||
|
onChange={setIsFunc}
|
||||||
|
note="'replacement' will be evaled if this is toggled"
|
||||||
|
hideBorder={true}
|
||||||
|
>
|
||||||
|
Treat as Function
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FullPatchInputProps {
|
||||||
|
setFind(v: string): void;
|
||||||
|
setParsedFind(v: string | RegExp): void;
|
||||||
|
setMatch(v: string): void;
|
||||||
|
setReplacement(v: string | ReplaceFn): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
||||||
|
const [fullPatch, setFullPatch] = React.useState<string>("");
|
||||||
|
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (fullPatch === "") {
|
||||||
|
setFullPatchError("");
|
||||||
|
|
||||||
|
setFind("");
|
||||||
|
setParsedFind("");
|
||||||
|
setMatch("");
|
||||||
|
setReplacement("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = (0, eval)(`([${fullPatch}][0])`) as Patch;
|
||||||
|
|
||||||
|
if (!parsed.find) throw new Error("No 'find' field");
|
||||||
|
if (!parsed.replacement) throw new Error("No 'replacement' field");
|
||||||
|
|
||||||
|
if (parsed.replacement instanceof Array) {
|
||||||
|
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
|
||||||
|
|
||||||
|
parsed.replacement = {
|
||||||
|
match: parsed.replacement[0].match,
|
||||||
|
replace: parsed.replacement[0].replace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
||||||
|
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
||||||
|
|
||||||
|
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
|
||||||
|
setParsedFind(parsed.find);
|
||||||
|
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
||||||
|
setReplacement(parsed.replacement.replace);
|
||||||
|
setFullPatchError("");
|
||||||
|
} catch (e) {
|
||||||
|
setFullPatchError((e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
||||||
|
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
||||||
|
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PatchHelper() {
|
||||||
|
const [find, setFind] = React.useState<string>("");
|
||||||
|
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
||||||
|
const [match, setMatch] = React.useState<string>("");
|
||||||
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||||
|
|
||||||
|
const [replacementError, setReplacementError] = React.useState<string>();
|
||||||
|
|
||||||
|
const [module, setModule] = React.useState<[number, Function]>();
|
||||||
|
const [findError, setFindError] = React.useState<string>();
|
||||||
|
const [matchError, setMatchError] = React.useState<string>();
|
||||||
|
|
||||||
|
const code = React.useMemo(() => {
|
||||||
|
return `
|
||||||
|
{
|
||||||
|
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
||||||
|
replacement: {
|
||||||
|
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||||
|
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`.trim();
|
||||||
|
}, [parsedFind, match, replacement]);
|
||||||
|
|
||||||
|
function onFindChange(v: string) {
|
||||||
|
setFind(v);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let parsedFind = v as string | RegExp;
|
||||||
|
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
||||||
|
|
||||||
|
setFindError(void 0);
|
||||||
|
setParsedFind(parsedFind);
|
||||||
|
|
||||||
|
if (v.length) {
|
||||||
|
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setFindError((e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMatchChange(v: string) {
|
||||||
|
setMatch(v);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new RegExp(v);
|
||||||
|
setMatchError(void 0);
|
||||||
|
} catch (e: any) {
|
||||||
|
setMatchError((e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsTab title="Patch Helper">
|
||||||
|
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||||
|
<FullPatchInput
|
||||||
|
setFind={onFindChange}
|
||||||
|
setParsedFind={setParsedFind}
|
||||||
|
setMatch={onMatchChange}
|
||||||
|
setReplacement={setReplacement}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
value={find}
|
||||||
|
onChange={onFindChange}
|
||||||
|
error={findError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
value={match}
|
||||||
|
onChange={onMatchChange}
|
||||||
|
error={matchError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={Margins.top8} />
|
||||||
|
<ReplacementInput
|
||||||
|
replacement={replacement}
|
||||||
|
setReplacement={setReplacement}
|
||||||
|
replacementError={replacementError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormDivider />
|
||||||
|
{module && (
|
||||||
|
<ReplacementComponent
|
||||||
|
module={module}
|
||||||
|
match={match}
|
||||||
|
replacement={replacement}
|
||||||
|
setReplacementError={setReplacementError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!(find && match && replacement) && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||||
|
<CodeBlock lang="js" content={code} />
|
||||||
|
<Button onClick={() => copyToClipboard(code)}>Copy to Clipboard</Button>
|
||||||
|
<Button className={Margins.top8} onClick={() => copyToClipboard("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
|
||||||
23
src/components/VencordSettings/PluginsTab.tsx
Normal file
23
src/components/VencordSettings/PluginsTab.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import PluginSettings from "@components/PluginSettings";
|
||||||
|
|
||||||
|
import { wrapTab } from "./shared";
|
||||||
|
|
||||||
|
export default wrapTab(PluginSettings, "Plugins");
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* 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 "./SpecialCard.css";
|
import "./specialCard.css";
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Card, Clickable, Forms, React } from "@webpack/common";
|
import { Card, Clickable, Forms, React } from "@webpack/common";
|
||||||
402
src/components/VencordSettings/ThemesTab.tsx
Normal file
402
src/components/VencordSettings/ThemesTab.tsx
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Settings, useSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
|
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
|
||||||
|
import { openInviteModal } from "@utils/discord";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
|
import { relaunch } from "@utils/native";
|
||||||
|
import { useForceUpdater } from "@utils/react";
|
||||||
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
|
import { findLazy } from "@webpack";
|
||||||
|
import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||||
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
|
||||||
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
import { AddonCard } from "./AddonCard";
|
||||||
|
import { QuickAction, QuickActionCard } from "./quickActions";
|
||||||
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
|
type FileInput = ComponentType<{
|
||||||
|
ref: Ref<HTMLInputElement>;
|
||||||
|
onChange: (e: SyntheticEvent<HTMLInputElement>) => void;
|
||||||
|
multiple?: boolean;
|
||||||
|
filters?: { name?: string; extensions: string[]; }[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-settings-theme-");
|
||||||
|
|
||||||
|
interface ThemeCardProps {
|
||||||
|
theme: UserThemeHeader;
|
||||||
|
enabled: boolean;
|
||||||
|
onChange: (enabled: boolean) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
|
||||||
|
return (
|
||||||
|
<AddonCard
|
||||||
|
name={theme.name}
|
||||||
|
description={theme.description}
|
||||||
|
author={theme.author}
|
||||||
|
enabled={enabled}
|
||||||
|
setEnabled={onChange}
|
||||||
|
infoButton={
|
||||||
|
IS_WEB && (
|
||||||
|
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
|
||||||
|
{!!theme.website && <Link href={theme.website}>Website</Link>}
|
||||||
|
{!!(theme.website && theme.invite) && " • "}
|
||||||
|
{!!theme.invite && (
|
||||||
|
<Link
|
||||||
|
href={`https://discord.gg/${theme.invite}`}
|
||||||
|
onClick={async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Discord Server
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ThemeTab {
|
||||||
|
LOCAL,
|
||||||
|
ONLINE
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemesTab() {
|
||||||
|
const settings = useSettings(["themeLinks", "enabledThemes"]);
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
||||||
|
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
||||||
|
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshLocalThemes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function refreshLocalThemes() {
|
||||||
|
const themes = await VencordNative.themes.getThemesList();
|
||||||
|
setUserThemes(themes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a local theme is enabled/disabled, update the settings
|
||||||
|
function onLocalThemeChange(fileName: string, value: boolean) {
|
||||||
|
if (value) {
|
||||||
|
if (settings.enabledThemes.includes(fileName)) return;
|
||||||
|
settings.enabledThemes = [...settings.enabledThemes, fileName];
|
||||||
|
} else {
|
||||||
|
settings.enabledThemes = settings.enabledThemes.filter(f => f !== fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.currentTarget?.files?.length) return;
|
||||||
|
const { files } = e.currentTarget;
|
||||||
|
|
||||||
|
const uploads = Array.from(files, file => {
|
||||||
|
const { name } = file;
|
||||||
|
if (!name.endsWith(".css")) return;
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
VencordNative.themes.uploadTheme(name, reader.result as string)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(uploads);
|
||||||
|
refreshLocalThemes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLocalThemes() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||||
|
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
|
||||||
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
|
BetterDiscord Themes
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||||
|
</div>
|
||||||
|
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
|
||||||
|
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
|
||||||
|
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Forms.FormSection title="Local Themes">
|
||||||
|
<QuickActionCard>
|
||||||
|
<>
|
||||||
|
{IS_WEB ?
|
||||||
|
(
|
||||||
|
<QuickAction
|
||||||
|
text={
|
||||||
|
<span style={{ position: "relative" }}>
|
||||||
|
Upload Theme
|
||||||
|
<FileInput
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={onFileUpload}
|
||||||
|
multiple={true}
|
||||||
|
filters={[{ extensions: ["css"] }]}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
Icon={PlusIcon}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<QuickAction
|
||||||
|
text="Open Themes Folder"
|
||||||
|
action={() => VencordNative.themes.openFolder()}
|
||||||
|
Icon={FolderIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<QuickAction
|
||||||
|
text="Load missing Themes"
|
||||||
|
action={refreshLocalThemes}
|
||||||
|
Icon={RestartIcon}
|
||||||
|
/>
|
||||||
|
<QuickAction
|
||||||
|
text="Edit QuickCSS"
|
||||||
|
action={() => VencordNative.quickCss.openEditor()}
|
||||||
|
Icon={PaintbrushIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Settings.plugins.ClientTheme.enabled && (
|
||||||
|
<QuickAction
|
||||||
|
text="Edit ClientTheme"
|
||||||
|
action={() => openPluginModal(Plugins.ClientTheme)}
|
||||||
|
Icon={PencilIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</QuickActionCard>
|
||||||
|
|
||||||
|
<div className={cl("grid")}>
|
||||||
|
{userThemes?.map(theme => (
|
||||||
|
<ThemeCard
|
||||||
|
key={theme.fileName}
|
||||||
|
enabled={settings.enabledThemes.includes(theme.fileName)}
|
||||||
|
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
||||||
|
onDelete={async () => {
|
||||||
|
onLocalThemeChange(theme.fileName, false);
|
||||||
|
await VencordNative.themes.deleteTheme(theme.fileName);
|
||||||
|
refreshLocalThemes();
|
||||||
|
}}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the user leaves the online theme textbox, update the settings
|
||||||
|
function onBlur() {
|
||||||
|
settings.themeLinks = [...new Set(
|
||||||
|
themeText
|
||||||
|
.trim()
|
||||||
|
.split(/\n+/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOnlineThemes() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className={classes("vc-warning-card", Margins.bottom16)}>
|
||||||
|
<Forms.FormText>
|
||||||
|
This section is for advanced users. If you are having difficulties using it, use the
|
||||||
|
Local Themes tab instead.
|
||||||
|
</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
<Card className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
||||||
|
<Forms.FormText>One link per line</Forms.FormText>
|
||||||
|
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
|
||||||
|
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Forms.FormSection title="Online Themes" tag="h5">
|
||||||
|
<TextArea
|
||||||
|
value={themeText}
|
||||||
|
onChange={setThemeText}
|
||||||
|
className={"vc-settings-theme-links"}
|
||||||
|
placeholder="Enter Theme Links..."
|
||||||
|
spellCheck={false}
|
||||||
|
onBlur={onBlur}
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsTab title="Themes">
|
||||||
|
<TabBar
|
||||||
|
type="top"
|
||||||
|
look="brand"
|
||||||
|
className="vc-settings-tab-bar"
|
||||||
|
selectedItem={currentTab}
|
||||||
|
onItemSelect={setCurrentTab}
|
||||||
|
>
|
||||||
|
<TabBar.Item
|
||||||
|
className="vc-settings-tab-bar-item"
|
||||||
|
id={ThemeTab.LOCAL}
|
||||||
|
>
|
||||||
|
Local Themes
|
||||||
|
</TabBar.Item>
|
||||||
|
<TabBar.Item
|
||||||
|
className="vc-settings-tab-bar-item"
|
||||||
|
id={ThemeTab.ONLINE}
|
||||||
|
>
|
||||||
|
Online Themes
|
||||||
|
</TabBar.Item>
|
||||||
|
</TabBar>
|
||||||
|
|
||||||
|
<CspErrorCard />
|
||||||
|
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
||||||
|
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CspErrorCard() {
|
||||||
|
if (IS_WEB) return null;
|
||||||
|
|
||||||
|
const errors = useCspErrors();
|
||||||
|
const forceUpdate = useForceUpdater();
|
||||||
|
|
||||||
|
if (!errors.length) return null;
|
||||||
|
|
||||||
|
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
|
||||||
|
|
||||||
|
const allowUrl = async (url: string) => {
|
||||||
|
const { origin: baseUrl, host } = new URL(url);
|
||||||
|
|
||||||
|
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
|
||||||
|
if (result !== "ok") return;
|
||||||
|
|
||||||
|
CspBlockedUrls.forEach(url => {
|
||||||
|
if (new URL(url).host === host) {
|
||||||
|
CspBlockedUrls.delete(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
|
||||||
|
Alerts.show({
|
||||||
|
title: "Restart Required",
|
||||||
|
body: "A restart is required to apply this change",
|
||||||
|
confirmText: "Restart now",
|
||||||
|
cancelText: "Later!",
|
||||||
|
onConfirm: relaunch
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorCard className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
|
||||||
|
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
|
||||||
|
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
|
||||||
|
<Forms.FormText>
|
||||||
|
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
|
||||||
|
<div className="vc-settings-csp-list">
|
||||||
|
{errors.map((url, i) => (
|
||||||
|
<div key={url}>
|
||||||
|
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
|
||||||
|
<div className="vc-settings-csp-row">
|
||||||
|
<Link href={url}>{url}</Link>
|
||||||
|
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
|
||||||
|
Allow
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasImgurHtmlDomain && (
|
||||||
|
<>
|
||||||
|
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
|
||||||
|
<Forms.FormText>
|
||||||
|
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ErrorCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserscriptThemesTab() {
|
||||||
|
return (
|
||||||
|
<SettingsTab title="Themes">
|
||||||
|
<Card className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
|
||||||
|
|
||||||
|
<Forms.FormText>
|
||||||
|
You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!
|
||||||
|
</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IS_USERSCRIPT
|
||||||
|
? wrapTab(UserscriptThemesTab, "Themes")
|
||||||
|
: wrapTab(ThemesTab, "Themes");
|
||||||
268
src/components/VencordSettings/UpdaterTab.tsx
Normal file
268
src/components/VencordSettings/UpdaterTab.tsx
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useSettings } from "@api/Settings";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
|
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
|
import { relaunch } from "@utils/native";
|
||||||
|
import { useAwaiter } from "@utils/react";
|
||||||
|
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
|
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
|
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
|
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||||
|
return async () => {
|
||||||
|
dispatcher(true);
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} catch (e: any) {
|
||||||
|
UpdateLogger.error("Failed to update", e);
|
||||||
|
|
||||||
|
let err: string;
|
||||||
|
if (!e) {
|
||||||
|
err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||||
|
} else if (e.code && e.cmd) {
|
||||||
|
const { code, path, cmd, stderr } = e;
|
||||||
|
|
||||||
|
if (code === "ENOENT")
|
||||||
|
err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||||
|
else {
|
||||||
|
err = `An error occurred while running \`${cmd}\`:\n`;
|
||||||
|
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
err = "An unknown error occurred. See the console for more info.";
|
||||||
|
}
|
||||||
|
|
||||||
|
Alerts.show({
|
||||||
|
title: "Oops!",
|
||||||
|
body: (
|
||||||
|
<ErrorCard>
|
||||||
|
{err.split("\n").map((line, idx) => <div key={idx}>{Parser.parse(line)}</div>)}
|
||||||
|
</ErrorCard>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
dispatcher(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommonProps {
|
||||||
|
repo: string;
|
||||||
|
repoPending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
|
||||||
|
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
|
||||||
|
{hash}
|
||||||
|
</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||||
|
return (
|
||||||
|
<Card style={{ padding: "0 0.5em" }}>
|
||||||
|
{updates.map(({ hash, author, message }) => (
|
||||||
|
<div key={hash} style={{
|
||||||
|
marginTop: "0.5em",
|
||||||
|
marginBottom: "0.5em"
|
||||||
|
}}>
|
||||||
|
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||||
|
<span style={{
|
||||||
|
marginLeft: "0.5em",
|
||||||
|
color: "var(--text-default)"
|
||||||
|
}}>{message} - {author}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Updatable(props: CommonProps) {
|
||||||
|
const [updates, setUpdates] = React.useState(changes);
|
||||||
|
const [isChecking, setIsChecking] = React.useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
|
||||||
|
const isOutdated = (updates?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!updates && updateError ? (
|
||||||
|
<>
|
||||||
|
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
|
||||||
|
<ErrorCard style={{ padding: "1em" }}>
|
||||||
|
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
|
||||||
|
</ErrorCard>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
|
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
|
||||||
|
</Forms.FormText>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOutdated && <Changes updates={updates} {...props} />}
|
||||||
|
|
||||||
|
<Flex className={classes(Margins.bottom8, Margins.top8)}>
|
||||||
|
{isOutdated && <Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={isUpdating || isChecking}
|
||||||
|
onClick={withDispatcher(setIsUpdating, async () => {
|
||||||
|
if (await update()) {
|
||||||
|
setUpdates([]);
|
||||||
|
await new Promise<void>(r => {
|
||||||
|
Alerts.show({
|
||||||
|
title: "Update Success!",
|
||||||
|
body: "Successfully updated. Restart now to apply the changes?",
|
||||||
|
confirmText: "Restart",
|
||||||
|
cancelText: "Not now!",
|
||||||
|
onConfirm() {
|
||||||
|
relaunch();
|
||||||
|
r();
|
||||||
|
},
|
||||||
|
onCancel: r
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Update Now
|
||||||
|
</Button>}
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={isUpdating || isChecking}
|
||||||
|
onClick={withDispatcher(setIsChecking, async () => {
|
||||||
|
const outdated = await checkForUpdates();
|
||||||
|
if (outdated) {
|
||||||
|
setUpdates(changes);
|
||||||
|
} else {
|
||||||
|
setUpdates([]);
|
||||||
|
Toasts.show({
|
||||||
|
message: "No updates found!",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.MESSAGE,
|
||||||
|
options: {
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Check for Updates
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Newer(props: CommonProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
|
Your local copy has more recent commits. Please stash or reset them.
|
||||||
|
</Forms.FormText>
|
||||||
|
<Changes {...props} updates={changes} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Updater() {
|
||||||
|
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
|
||||||
|
|
||||||
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (err)
|
||||||
|
UpdateLogger.error("Failed to retrieve repo", err);
|
||||||
|
}, [err]);
|
||||||
|
|
||||||
|
const commonProps: CommonProps = {
|
||||||
|
repo,
|
||||||
|
repoPending
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsTab title="Vencord Updater">
|
||||||
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
|
<Switch
|
||||||
|
value={settings.autoUpdate}
|
||||||
|
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||||
|
note="Automatically update Vencord without confirmation prompt"
|
||||||
|
>
|
||||||
|
Automatically update
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
value={settings.autoUpdateNotification}
|
||||||
|
onChange={(v: boolean) => settings.autoUpdateNotification = v}
|
||||||
|
note="Shows a notification when Vencord automatically updates"
|
||||||
|
disabled={!settings.autoUpdate}
|
||||||
|
>
|
||||||
|
Get notified when an automatic update completes
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||||
|
|
||||||
|
<Forms.FormText>
|
||||||
|
{repoPending
|
||||||
|
? repo
|
||||||
|
: err
|
||||||
|
? "Failed to retrieve - check console"
|
||||||
|
: (
|
||||||
|
<Link href={repo}>
|
||||||
|
{repo.split("/").slice(-2).join("/")}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{" "}(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
|
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||||
|
|
||||||
|
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
||||||
|
const UpdaterTab = wrapTab(Updater, "Updater");
|
||||||
|
|
||||||
|
try {
|
||||||
|
openModal(wrapTab((modalProps: ModalProps) => (
|
||||||
|
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||||
|
<ModalContent className="vc-updater-modal">
|
||||||
|
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
||||||
|
<UpdaterTab />
|
||||||
|
</ModalContent>
|
||||||
|
</ModalRoot>
|
||||||
|
), "UpdaterModal"));
|
||||||
|
} catch {
|
||||||
|
handleSettingsTabError();
|
||||||
|
}
|
||||||
|
};
|
||||||
301
src/components/VencordSettings/VencordTab.tsx
Normal file
301
src/components/VencordSettings/VencordTab.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||||
|
import { useSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import DonateButton from "@components/DonateButton";
|
||||||
|
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||||
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
|
import { gitRemote } from "@shared/vencordUserAgent";
|
||||||
|
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { identity, isPluginDev } from "@utils/misc";
|
||||||
|
import { relaunch } from "@utils/native";
|
||||||
|
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import BadgeAPI from "../../plugins/_api/badges";
|
||||||
|
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
|
||||||
|
import { openNotificationSettingsModal } from "./NotificationSettings";
|
||||||
|
import { QuickAction, QuickActionCard } from "./quickActions";
|
||||||
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
import { SpecialCard } from "./SpecialCard";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-settings-");
|
||||||
|
|
||||||
|
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||||
|
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
|
||||||
|
|
||||||
|
const VENNIE_DONATOR_IMAGE = "https://cdn.discordapp.com/emojis/1238120638020063377.png";
|
||||||
|
const COZY_CONTRIB_IMAGE = "https://cdn.discordapp.com/emojis/1026533070955872337.png";
|
||||||
|
|
||||||
|
const DONOR_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070116305436712.png?size=2048";
|
||||||
|
const CONTRIB_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070166481895484.png?size=2048";
|
||||||
|
|
||||||
|
type KeysOfType<Object, Type> = {
|
||||||
|
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||||
|
}[keyof Object];
|
||||||
|
|
||||||
|
function VencordSettings() {
|
||||||
|
const settings = useSettings();
|
||||||
|
|
||||||
|
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||||
|
|
||||||
|
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||||
|
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||||
|
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||||
|
|
||||||
|
const user = UserStore.getCurrentUser();
|
||||||
|
|
||||||
|
const Switches: Array<false | {
|
||||||
|
key: KeysOfType<typeof settings, boolean>;
|
||||||
|
title: string;
|
||||||
|
note: string;
|
||||||
|
}> =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: "useQuickCss",
|
||||||
|
title: "Enable Custom CSS",
|
||||||
|
note: "Loads your Custom CSS"
|
||||||
|
},
|
||||||
|
!IS_WEB && {
|
||||||
|
key: "enableReactDevtools",
|
||||||
|
title: "Enable React Developer Tools",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
},
|
||||||
|
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
|
||||||
|
key: "frameless",
|
||||||
|
title: "Disable the window frame",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
} : {
|
||||||
|
key: "winNativeTitleBar",
|
||||||
|
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
}),
|
||||||
|
!IS_WEB && {
|
||||||
|
key: "transparent",
|
||||||
|
title: "Enable window transparency.",
|
||||||
|
note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
|
||||||
|
},
|
||||||
|
!IS_WEB && isWindows && {
|
||||||
|
key: "winCtrlQ",
|
||||||
|
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
},
|
||||||
|
IS_DISCORD_DESKTOP && {
|
||||||
|
key: "disableMinSize",
|
||||||
|
title: "Disable minimum window size",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsTab title="Vencord Settings">
|
||||||
|
{isDonor(user?.id)
|
||||||
|
? (
|
||||||
|
<SpecialCard
|
||||||
|
title="Donations"
|
||||||
|
subtitle="Thank you for donating!"
|
||||||
|
description="You can manage your perks at any time by messaging @vending.machine."
|
||||||
|
cardImage={VENNIE_DONATOR_IMAGE}
|
||||||
|
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||||
|
backgroundColor="#ED87A9"
|
||||||
|
>
|
||||||
|
<DonateButtonComponent />
|
||||||
|
</SpecialCard>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<SpecialCard
|
||||||
|
title="Support the Project"
|
||||||
|
description="Please consider supporting the development of Vencord by donating!"
|
||||||
|
cardImage={donateImage}
|
||||||
|
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||||
|
backgroundColor="#c3a3ce"
|
||||||
|
>
|
||||||
|
<DonateButtonComponent />
|
||||||
|
</SpecialCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{isPluginDev(user?.id) && (
|
||||||
|
<SpecialCard
|
||||||
|
title="Contributions"
|
||||||
|
subtitle="Thank you for contributing!"
|
||||||
|
description="Since you've contributed to Vencord you now have a cool new badge!"
|
||||||
|
cardImage={COZY_CONTRIB_IMAGE}
|
||||||
|
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
|
||||||
|
backgroundColor="#EDCC87"
|
||||||
|
buttonTitle="See what you've contributed to"
|
||||||
|
buttonOnClick={() => openContributorModal(user)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Forms.FormSection title="Quick Actions">
|
||||||
|
<QuickActionCard>
|
||||||
|
<QuickAction
|
||||||
|
Icon={LogIcon}
|
||||||
|
text="Notification Log"
|
||||||
|
action={openNotificationLogModal}
|
||||||
|
/>
|
||||||
|
<QuickAction
|
||||||
|
Icon={PaintbrushIcon}
|
||||||
|
text="Edit QuickCSS"
|
||||||
|
action={() => VencordNative.quickCss.openEditor()}
|
||||||
|
/>
|
||||||
|
{!IS_WEB && (
|
||||||
|
<QuickAction
|
||||||
|
Icon={RestartIcon}
|
||||||
|
text="Relaunch Discord"
|
||||||
|
action={relaunch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!IS_WEB && (
|
||||||
|
<QuickAction
|
||||||
|
Icon={FolderIcon}
|
||||||
|
text="Open Settings Folder"
|
||||||
|
action={() => VencordNative.settings.openFolder()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<QuickAction
|
||||||
|
Icon={GithubIcon}
|
||||||
|
text="View Source Code"
|
||||||
|
action={() => VencordNative.native.openExternal("https://github.com/" + gitRemote)}
|
||||||
|
/>
|
||||||
|
</QuickActionCard>
|
||||||
|
</Forms.FormSection>
|
||||||
|
|
||||||
|
<Forms.FormDivider />
|
||||||
|
|
||||||
|
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||||
|
<Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
|
||||||
|
Hint: You can change the position of this settings section in the
|
||||||
|
{" "}<Button
|
||||||
|
look={Button.Looks.BLANK}
|
||||||
|
style={{ color: "var(--text-link)", display: "inline-block" }}
|
||||||
|
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
||||||
|
>
|
||||||
|
settings of the Settings plugin
|
||||||
|
</Button>!
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
{Switches.map(s => s && (
|
||||||
|
<Switch
|
||||||
|
key={s.key}
|
||||||
|
value={settings[s.key]}
|
||||||
|
onChange={v => settings[s.key] = v}
|
||||||
|
note={s.note}
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
</Switch>
|
||||||
|
))}
|
||||||
|
</Forms.FormSection>
|
||||||
|
|
||||||
|
|
||||||
|
{needsVibrancySettings && <>
|
||||||
|
<Forms.FormTitle tag="h5">Window vibrancy style (requires restart)</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
className={Margins.bottom20}
|
||||||
|
placeholder="Window vibrancy style"
|
||||||
|
options={[
|
||||||
|
// Sorted from most opaque to most transparent
|
||||||
|
{
|
||||||
|
label: "No vibrancy", value: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Under Page (window tinting)",
|
||||||
|
value: "under-page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Content",
|
||||||
|
value: "content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window",
|
||||||
|
value: "window"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Selection",
|
||||||
|
value: "selection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Titlebar",
|
||||||
|
value: "titlebar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Header",
|
||||||
|
value: "header"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sidebar",
|
||||||
|
value: "sidebar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tooltip",
|
||||||
|
value: "tooltip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Menu",
|
||||||
|
value: "menu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Popover",
|
||||||
|
value: "popover"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Fullscreen UI (transparent but slightly muted)",
|
||||||
|
value: "fullscreen-ui"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "HUD (Most transparent)",
|
||||||
|
value: "hud"
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
select={v => settings.macosVibrancyStyle = v}
|
||||||
|
isSelected={v => settings.macosVibrancyStyle === v}
|
||||||
|
serialize={identity} />
|
||||||
|
</>}
|
||||||
|
|
||||||
|
<Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
|
||||||
|
<Flex>
|
||||||
|
<Button onClick={openNotificationSettingsModal}>
|
||||||
|
Notification Settings
|
||||||
|
</Button>
|
||||||
|
<Button onClick={openNotificationLogModal}>
|
||||||
|
View Notification Log
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DonateButtonComponent() {
|
||||||
|
return (
|
||||||
|
<DonateButton
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
color={Button.Colors.WHITE}
|
||||||
|
style={{ marginTop: "1em" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDonor(userId: string): boolean {
|
||||||
|
const donorBadges = BadgeAPI.getDonorBadges(userId);
|
||||||
|
return GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wrapTab(VencordSettings, "Vencord Settings");
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
.vc-addon-card {
|
.vc-addon-card {
|
||||||
background-color: var(--card-primary-bg);
|
background-color: var(--background-base-lower-alt);
|
||||||
color: var(--interactive-active);
|
color: var(--interactive-active);
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
@ -12,15 +11,26 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visual-refresh .vc-addon-card {
|
||||||
|
background-color: var(--card-primary-bg);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
.vc-addon-card-disabled {
|
.vc-addon-card-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-addon-card:hover {
|
.vc-addon-card:hover {
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: var(--elevation-high);
|
box-shadow: var(--elevation-high);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visual-refresh .vc-addon-card:hover {
|
||||||
|
/* same as non-hover, here to overwrite the non-refresh hover background */
|
||||||
|
background-color: var(--card-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.vc-addon-header {
|
.vc-addon-header {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
.vc-settings-quickActions-pill {
|
.vc-settings-quickActions-pill {
|
||||||
all: unset;
|
all: unset;
|
||||||
background: var(--button-secondary-background);
|
background: var(--background-base-lower);
|
||||||
color: var(--header-secondary);
|
color: var(--header-secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-quickActions-pill:hover {
|
.vc-settings-quickActions-pill:hover {
|
||||||
background: var(--button-secondary-background-hover);
|
background: var(--background-base-lower-alt);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: var(--elevation-high);
|
box-shadow: var(--elevation-high);
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +36,14 @@
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visual-refresh .vc-settings-quickActions-pill {
|
||||||
|
background: var(--button-secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-refresh .vc-settings-quickActions-pill:hover {
|
||||||
|
background: var(--button-secondary-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.vc-settings-quickActions-img {
|
.vc-settings-quickActions-img {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./QuickAction.css";
|
import "./quickActions.css";
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Card } from "@webpack/common";
|
import { Card } from "@webpack/common";
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-theme-links:focus {
|
.vc-settings-theme-links:focus {
|
||||||
background-color: var(--background-base-lowest);
|
background-color: var(--background-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-cloud-settings-sync-grid {
|
.vc-cloud-settings-sync-grid {
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
* 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 "./settingsStyles.css";
|
||||||
|
import "./themesStyles.css";
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./Badge";
|
||||||
export * from "./CheckedTextInput";
|
export * from "./CheckedTextInput";
|
||||||
export * from "./CodeBlock";
|
export * from "./CodeBlock";
|
||||||
|
export * from "./DonateButton";
|
||||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||||
export * from "./ErrorCard";
|
export * from "./ErrorCard";
|
||||||
export * from "./Flex";
|
export * from "./Flex";
|
||||||
export * from "./Grid";
|
|
||||||
export * from "./Heart";
|
export * from "./Heart";
|
||||||
export * from "./Icons";
|
export * from "./Icons";
|
||||||
export * from "./Link";
|
export * from "./Link";
|
||||||
export * from "./settings";
|
export * from "./Switch";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./AddonCard";
|
|
||||||
export * from "./DonateButton";
|
|
||||||
export * from "./PluginBadge";
|
|
||||||
export * from "./QuickAction";
|
|
||||||
export * from "./SpecialCard";
|
|
||||||
export * from "./Switch";
|
|
||||||
export * from "./tabs";
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./styles.css";
|
|
||||||
|
|
||||||
export * from "./BaseTab";
|
|
||||||
export { default as PatchHelperTab } from "./patchHelper";
|
|
||||||
export { default as PluginsTab } from "./plugins";
|
|
||||||
export { openContributorModal } from "./plugins/ContributorModal";
|
|
||||||
export { openPluginModal } from "./plugins/PluginModal";
|
|
||||||
export { default as BackupAndRestoreTab } from "./sync/BackupAndRestoreTab";
|
|
||||||
export { default as CloudTab } from "./sync/CloudTab";
|
|
||||||
export { default as ThemesTab } from "./themes";
|
|
||||||
export { openUpdaterModal, default as UpdaterTab } from "./updater";
|
|
||||||
export { default as VencordTab } from "./vencord";
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { Patch, ReplaceFn } from "@utils/types";
|
|
||||||
import { Forms, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
export interface FullPatchInputProps {
|
|
||||||
setFind(v: string): void;
|
|
||||||
setParsedFind(v: string | RegExp): void;
|
|
||||||
setMatch(v: string): void;
|
|
||||||
setReplacement(v: string | ReplaceFn): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
|
||||||
const [patch, setPatch] = useState<string>("");
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
|
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
if (patch === "") {
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
setFind("");
|
|
||||||
setParsedFind("");
|
|
||||||
setMatch("");
|
|
||||||
setReplacement("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let { find, replacement } = (0, eval)(`([${patch}][0])`) as Patch;
|
|
||||||
|
|
||||||
if (!find) throw new Error("No 'find' field");
|
|
||||||
if (!replacement) throw new Error("No 'replacement' field");
|
|
||||||
|
|
||||||
if (replacement instanceof Array) {
|
|
||||||
if (replacement.length === 0) throw new Error("Invalid replacement");
|
|
||||||
|
|
||||||
// Only test the first replacement
|
|
||||||
replacement = replacement[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!replacement.match) throw new Error("No 'replacement.match' field");
|
|
||||||
if (!replacement.replace) throw new Error("No 'replacement.replace' field");
|
|
||||||
|
|
||||||
setFind(find instanceof RegExp ? `/${find.source}/` : find);
|
|
||||||
setParsedFind(find);
|
|
||||||
setMatch(replacement.match instanceof RegExp ? replacement.match.source : replacement.match);
|
|
||||||
setReplacement(replacement.replace);
|
|
||||||
setError("");
|
|
||||||
} catch (e) {
|
|
||||||
setError((e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { current: textArea } = textAreaRef;
|
|
||||||
if (textArea) {
|
|
||||||
textArea.style.height = "auto";
|
|
||||||
textArea.style.height = `${textArea.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}, [patch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
|
||||||
Paste your full JSON patch here to fill out the fields
|
|
||||||
</Forms.FormText>
|
|
||||||
<TextArea
|
|
||||||
inputRef={textAreaRef}
|
|
||||||
value={patch}
|
|
||||||
onChange={setPatch}
|
|
||||||
onBlur={update}
|
|
||||||
/>
|
|
||||||
{error !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
|
||||||
import { makeCodeblock } from "@utils/text";
|
|
||||||
import { ReplaceFn } from "@utils/types";
|
|
||||||
import { Button, Forms, Parser, useMemo, useState } from "@webpack/common";
|
|
||||||
import type { Change } from "diff";
|
|
||||||
|
|
||||||
// Do not include diff in non dev builds (side effects import)
|
|
||||||
if (IS_DEV) {
|
|
||||||
var differ = require("diff") as typeof import("diff");
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PatchPreviewProps {
|
|
||||||
module: [id: number, factory: Function];
|
|
||||||
match: string;
|
|
||||||
replacement: string | ReplaceFn;
|
|
||||||
setReplacementError(error: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
|
|
||||||
if (!match || original === patched) return null;
|
|
||||||
|
|
||||||
const changeSize = patched.length - original.length;
|
|
||||||
|
|
||||||
// Use 200 surrounding characters of context
|
|
||||||
const start = Math.max(0, match.index! - 200);
|
|
||||||
const end = Math.min(original.length, match.index! + match[0].length + 200);
|
|
||||||
// (changeSize may be negative)
|
|
||||||
const endPatched = end + changeSize;
|
|
||||||
|
|
||||||
const context = original.slice(start, end);
|
|
||||||
const patchedContext = patched.slice(start, endPatched);
|
|
||||||
|
|
||||||
return differ.diffWordsWithSpace(context, patchedContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Match({ matchResult }: { matchResult: RegExpMatchArray | null; }) {
|
|
||||||
if (!matchResult)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const fullMatch = matchResult[0]
|
|
||||||
? makeCodeblock(matchResult[0], "js")
|
|
||||||
: "";
|
|
||||||
const groups = matchResult.length > 1
|
|
||||||
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Match</Forms.FormTitle>
|
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
|
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Diff({ diff }: { diff: Change[] | null; }) {
|
|
||||||
if (!diff?.length)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const diffLines = diff.map((p, idx) => {
|
|
||||||
const color = p.added
|
|
||||||
? "lime"
|
|
||||||
: p.removed
|
|
||||||
? "red"
|
|
||||||
: "grey";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}
|
|
||||||
>
|
|
||||||
{p.value}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Diff</Forms.FormTitle>
|
|
||||||
{diffLines}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PatchPreview({ module, match, replacement, setReplacementError }: PatchPreviewProps) {
|
|
||||||
const [id, fact] = module;
|
|
||||||
const [compileResult, setCompileResult] = useState<[boolean, string]>();
|
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = useMemo<[string, RegExpMatchArray | null, Change[] | null]>(() => {
|
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
|
||||||
|
|
||||||
try {
|
|
||||||
new RegExp(match);
|
|
||||||
} catch (e) {
|
|
||||||
return ["", null, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
|
||||||
try {
|
|
||||||
const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
|
|
||||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
|
||||||
setReplacementError(void 0);
|
|
||||||
} catch (e) {
|
|
||||||
setReplacementError((e as Error).message);
|
|
||||||
return ["", null, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
const m = src.match(canonicalMatch);
|
|
||||||
return [patched, m, makeDiff(src, patched, m)];
|
|
||||||
}, [id, match, replacement]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Module {id}</Forms.FormTitle>
|
|
||||||
|
|
||||||
<Match matchResult={matchResult} />
|
|
||||||
<Diff diff={diff} />
|
|
||||||
|
|
||||||
{!!diff?.length && (
|
|
||||||
<Button
|
|
||||||
className={Margins.top20}
|
|
||||||
onClick={() => {
|
|
||||||
try {
|
|
||||||
Function(patchedCode.replace(/^(?=function\()/, "0,"));
|
|
||||||
setCompileResult([true, "Compiled successfully"]);
|
|
||||||
} catch (err) {
|
|
||||||
setCompileResult([false, (err as Error).message]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Compile
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{compileResult && (
|
|
||||||
<Forms.FormText style={{ color: compileResult[0] ? "var(--status-positive)" : "var(--text-danger)" }}>
|
|
||||||
{compileResult[1]}
|
|
||||||
</Forms.FormText>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { Forms, Parser, Switch, TextInput, useEffect, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
const RegexGuide = {
|
|
||||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
|
||||||
"$$": "Insert a $",
|
|
||||||
"$&": "Insert the entire match",
|
|
||||||
"$`\u200b": "Insert the substring before the match",
|
|
||||||
"$'": "Insert the substring after the match",
|
|
||||||
"$n": "Insert the nth capturing group ($1, $2...)",
|
|
||||||
"$self": "Insert the plugin instance",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|
||||||
const [isFunc, setIsFunc] = useState(false);
|
|
||||||
const [error, setError] = useState<string>();
|
|
||||||
|
|
||||||
function onChange(v: string) {
|
|
||||||
setError(void 0);
|
|
||||||
|
|
||||||
if (isFunc) {
|
|
||||||
try {
|
|
||||||
const func = (0, eval)(v);
|
|
||||||
if (typeof func === "function")
|
|
||||||
setReplacement(() => func);
|
|
||||||
|
|
||||||
else
|
|
||||||
setError("Replacement must be a function");
|
|
||||||
} catch (e) {
|
|
||||||
setReplacement(v);
|
|
||||||
setError((e as Error).message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setReplacement(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFunc)
|
|
||||||
onChange(replacement);
|
|
||||||
else
|
|
||||||
setError(void 0);
|
|
||||||
}, [isFunc]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
|
||||||
<Forms.FormTitle className="">Replacement</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
value={replacement?.toString()}
|
|
||||||
onChange={onChange}
|
|
||||||
error={error ?? replacementError}
|
|
||||||
/>
|
|
||||||
{!isFunc && (
|
|
||||||
<div>
|
|
||||||
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
|
||||||
|
|
||||||
{Object.entries(RegexGuide).map(([placeholder, desc]) => (
|
|
||||||
<Forms.FormText key={placeholder}>
|
|
||||||
{Parser.parse("`" + placeholder + "`")}: {desc}
|
|
||||||
</Forms.FormText>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
className={Margins.top16}
|
|
||||||
value={isFunc}
|
|
||||||
onChange={setIsFunc}
|
|
||||||
note="'replacement' will be evaled if this is toggled"
|
|
||||||
hideBorder
|
|
||||||
>
|
|
||||||
Treat as Function
|
|
||||||
</Switch>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
|
|
||||||
import { debounce } from "@shared/debounce";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { copyWithToast } from "@utils/misc";
|
|
||||||
import { stripIndent } from "@utils/text";
|
|
||||||
import { ReplaceFn } from "@utils/types";
|
|
||||||
import { search } from "@webpack";
|
|
||||||
import { Button, Forms, React, TextInput, useMemo, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
import { FullPatchInput } from "./FullPatchInput";
|
|
||||||
import { PatchPreview } from "./PatchPreview";
|
|
||||||
import { ReplacementInput } from "./ReplacementInput";
|
|
||||||
|
|
||||||
const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|
||||||
const candidates = search(find);
|
|
||||||
const keys = Object.keys(candidates);
|
|
||||||
const len = keys.length;
|
|
||||||
|
|
||||||
if (len === 0)
|
|
||||||
setError("No match. Perhaps that module is lazy loaded?");
|
|
||||||
else if (len !== 1)
|
|
||||||
setError("Multiple matches. Please refine your filter");
|
|
||||||
else
|
|
||||||
setModule([keys[0], candidates[keys[0]]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function PatchHelper() {
|
|
||||||
const [find, setFind] = useState("");
|
|
||||||
const [match, setMatch] = useState("");
|
|
||||||
const [replacement, setReplacement] = useState<string | ReplaceFn>("");
|
|
||||||
|
|
||||||
const [parsedFind, setParsedFind] = useState<string | RegExp>("");
|
|
||||||
|
|
||||||
const [findError, setFindError] = useState<string>();
|
|
||||||
const [matchError, setMatchError] = useState<string>();
|
|
||||||
const [replacementError, setReplacementError] = useState<string>();
|
|
||||||
|
|
||||||
const [module, setModule] = useState<[number, Function]>();
|
|
||||||
|
|
||||||
const code = useMemo(() => {
|
|
||||||
const find = parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind);
|
|
||||||
const replace = typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement);
|
|
||||||
|
|
||||||
return stripIndent`
|
|
||||||
{
|
|
||||||
find: ${find},
|
|
||||||
replacement: {
|
|
||||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
|
||||||
replace: ${replace}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}, [parsedFind, match, replacement]);
|
|
||||||
|
|
||||||
function onFindChange(v: string) {
|
|
||||||
setFind(v);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let parsedFind = v as string | RegExp;
|
|
||||||
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
|
||||||
|
|
||||||
setFindError(void 0);
|
|
||||||
setParsedFind(parsedFind);
|
|
||||||
|
|
||||||
if (v.length) {
|
|
||||||
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
setFindError((e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMatchChange(v: string) {
|
|
||||||
setMatch(v);
|
|
||||||
|
|
||||||
try {
|
|
||||||
new RegExp(v);
|
|
||||||
setMatchError(void 0);
|
|
||||||
} catch (e: any) {
|
|
||||||
setMatchError((e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsTab title="Patch Helper">
|
|
||||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
|
||||||
<FullPatchInput
|
|
||||||
setFind={onFindChange}
|
|
||||||
setParsedFind={setParsedFind}
|
|
||||||
setMatch={onMatchChange}
|
|
||||||
setReplacement={setReplacement}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
value={find}
|
|
||||||
onChange={onFindChange}
|
|
||||||
error={findError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
value={match}
|
|
||||||
onChange={onMatchChange}
|
|
||||||
error={matchError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={Margins.top8} />
|
|
||||||
<ReplacementInput
|
|
||||||
replacement={replacement}
|
|
||||||
setReplacement={setReplacement}
|
|
||||||
replacementError={replacementError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormDivider />
|
|
||||||
{module && (
|
|
||||||
<PatchPreview
|
|
||||||
module={module}
|
|
||||||
match={match}
|
|
||||||
replacement={replacement}
|
|
||||||
setReplacementError={setReplacementError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!(find && match && replacement) && (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
|
||||||
<CodeBlock lang="js" content={code} />
|
|
||||||
<Flex className={Margins.top16}>
|
|
||||||
<Button onClick={() => copyWithToast(code)}>
|
|
||||||
Copy to Clipboard
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => copyWithToast("```ts\n" + code + "\n```")}>
|
|
||||||
Copy as Codeblock
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SettingsTab>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { showNotice } from "@api/Notices";
|
|
||||||
import { CogWheel, InfoIcon } from "@components/Icons";
|
|
||||||
import { AddonCard } from "@components/settings/AddonCard";
|
|
||||||
import { proxyLazy } from "@utils/lazy";
|
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
|
||||||
import { Plugin } from "@utils/types";
|
|
||||||
import { findByPropsLazy } from "@webpack";
|
|
||||||
import { React, showToast, Toasts } from "@webpack/common";
|
|
||||||
import { Settings } from "Vencord";
|
|
||||||
|
|
||||||
import { cl, logger } from ".";
|
|
||||||
import { openPluginModal } from "./PluginModal";
|
|
||||||
|
|
||||||
// Avoid circular dependency
|
|
||||||
const { startDependenciesRecursive, startPlugin, stopPlugin, isPluginEnabled } = proxyLazy(() => require("plugins") as typeof import("plugins"));
|
|
||||||
|
|
||||||
export const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
|
||||||
|
|
||||||
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|
||||||
plugin: Plugin;
|
|
||||||
disabled: boolean;
|
|
||||||
onRestartNeeded(name: string, key: string): void;
|
|
||||||
isNew?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
|
||||||
const settings = Settings.plugins[plugin.name];
|
|
||||||
|
|
||||||
const isEnabled = () => isPluginEnabled(plugin.name);
|
|
||||||
|
|
||||||
function toggleEnabled() {
|
|
||||||
const wasEnabled = isEnabled();
|
|
||||||
|
|
||||||
// If we're enabling a plugin, make sure all deps are enabled recursively.
|
|
||||||
if (!wasEnabled) {
|
|
||||||
const { restartNeeded, failures } = startDependenciesRecursive(plugin);
|
|
||||||
|
|
||||||
if (failures.length) {
|
|
||||||
logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`);
|
|
||||||
showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restartNeeded) {
|
|
||||||
// If any dependencies have patches, don't start the plugin yet.
|
|
||||||
settings.enabled = true;
|
|
||||||
onRestartNeeded(plugin.name, "enabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
|
||||||
if (plugin.patches?.length) {
|
|
||||||
settings.enabled = !wasEnabled;
|
|
||||||
onRestartNeeded(plugin.name, "enabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
|
||||||
if (wasEnabled && !plugin.started) {
|
|
||||||
settings.enabled = !wasEnabled;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
settings.enabled = false;
|
|
||||||
|
|
||||||
const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`;
|
|
||||||
showToast(msg, Toasts.Type.FAILURE, {
|
|
||||||
position: Toasts.Position.BOTTOM,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.enabled = !wasEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AddonCard
|
|
||||||
name={plugin.name}
|
|
||||||
description={plugin.description}
|
|
||||||
isNew={isNew}
|
|
||||||
enabled={isEnabled()}
|
|
||||||
setEnabled={toggleEnabled}
|
|
||||||
disabled={disabled}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
infoButton={
|
|
||||||
<button
|
|
||||||
role="switch"
|
|
||||||
onClick={() => openPluginModal(plugin, onRestartNeeded)}
|
|
||||||
className={classes(ButtonClasses.button, cl("info-button"))}
|
|
||||||
>
|
|
||||||
{plugin.options && !isObjectEmpty(plugin.options)
|
|
||||||
? <CogWheel className={cl("info-icon")} />
|
|
||||||
: <InfoIcon className={cl("info-icon")} />
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Switch } from "@components/settings/Switch";
|
|
||||||
import { PluginOptionBoolean } from "@utils/types";
|
|
||||||
import { React, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
|
||||||
|
|
||||||
export function BooleanSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionBoolean>) {
|
|
||||||
const def = pluginSettings[id] ?? option.default;
|
|
||||||
|
|
||||||
const [state, setState] = useState(def ?? false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
function handleChange(newValue: boolean): void {
|
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
|
||||||
|
|
||||||
setState(newValue);
|
|
||||||
setError(resolveError(isValid));
|
|
||||||
|
|
||||||
if (isValid === true) {
|
|
||||||
onChange(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsSection name={id} description={option.description} error={error} inlineSetting>
|
|
||||||
<Switch checked={state} onChange={handleChange} />
|
|
||||||
</SettingsSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
|
||||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
|
||||||
import { Text } from "@webpack/common";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export const cl = classNameFactory("vc-plugins-setting-");
|
|
||||||
|
|
||||||
interface SettingBaseProps<T> {
|
|
||||||
option: T;
|
|
||||||
onChange(newValue: any): void;
|
|
||||||
pluginSettings: {
|
|
||||||
[setting: string]: any;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
id: string;
|
|
||||||
definedSettings?: DefinedSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SettingProps<T extends PluginOptionBase> = SettingBaseProps<T>;
|
|
||||||
export type ComponentSettingProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = SettingBaseProps<T>;
|
|
||||||
|
|
||||||
export function resolveError(isValidResult: boolean | string) {
|
|
||||||
if (typeof isValidResult === "string") return isValidResult;
|
|
||||||
|
|
||||||
return isValidResult ? null : "Invalid input provided";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsSectionProps extends PropsWithChildren {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
error?: string | null;
|
|
||||||
inlineSetting?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsSection({ name, description, error, inlineSetting, children }: SettingsSectionProps) {
|
|
||||||
return (
|
|
||||||
<div className={cl("section")}>
|
|
||||||
<div className={classes(cl("content"), inlineSetting && cl("inline"))}>
|
|
||||||
<div className={cl("label")}>
|
|
||||||
{name && <Text className={cl("title")} variant="text-md/medium">{wordsToTitle(wordsFromCamel(name))}</Text>}
|
|
||||||
{description && <Text className={cl("description")} variant="text-sm/normal">{description}</Text>}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{error && <Text className={cl("error")} variant="text-sm/normal">{error}</Text>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./styles.css";
|
|
||||||
|
|
||||||
import { OptionType } from "@utils/types";
|
|
||||||
import { ComponentType } from "react";
|
|
||||||
|
|
||||||
import { BooleanSetting } from "./BooleanSetting";
|
|
||||||
import { ComponentSettingProps, SettingProps } from "./Common";
|
|
||||||
import { ComponentSetting } from "./ComponentSetting";
|
|
||||||
import { NumberSetting } from "./NumberSetting";
|
|
||||||
import { SelectSetting } from "./SelectSetting";
|
|
||||||
import { SliderSetting } from "./SliderSetting";
|
|
||||||
import { TextSetting } from "./TextSetting";
|
|
||||||
|
|
||||||
export const OptionComponentMap: Record<OptionType, ComponentType<SettingProps<any> | ComponentSettingProps<any>>> = {
|
|
||||||
[OptionType.STRING]: TextSetting,
|
|
||||||
[OptionType.NUMBER]: NumberSetting,
|
|
||||||
[OptionType.BIGINT]: NumberSetting,
|
|
||||||
[OptionType.BOOLEAN]: BooleanSetting,
|
|
||||||
[OptionType.SELECT]: SelectSetting,
|
|
||||||
[OptionType.SLIDER]: SliderSetting,
|
|
||||||
[OptionType.COMPONENT]: ComponentSetting,
|
|
||||||
[OptionType.CUSTOM]: () => null,
|
|
||||||
};
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
.vc-plugins-setting-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-setting-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-setting-inline {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-setting-label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-setting-title {
|
|
||||||
color: var(--header-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-setting-description {
|
|
||||||
color: var(--header-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-setting-error {
|
|
||||||
color: var(--text-danger);
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { relaunch } from "@utils/native";
|
|
||||||
import { useForceUpdater } from "@utils/react";
|
|
||||||
import { Alerts, Button, Forms } from "@webpack/common";
|
|
||||||
|
|
||||||
export function CspErrorCard() {
|
|
||||||
if (IS_WEB) return null;
|
|
||||||
|
|
||||||
const errors = useCspErrors();
|
|
||||||
const forceUpdate = useForceUpdater();
|
|
||||||
|
|
||||||
if (!errors.length) return null;
|
|
||||||
|
|
||||||
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
|
|
||||||
|
|
||||||
const allowUrl = async (url: string) => {
|
|
||||||
const { origin: baseUrl, host } = new URL(url);
|
|
||||||
|
|
||||||
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
|
|
||||||
if (result !== "ok") return;
|
|
||||||
|
|
||||||
CspBlockedUrls.forEach(url => {
|
|
||||||
if (new URL(url).host === host) {
|
|
||||||
CspBlockedUrls.delete(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
forceUpdate();
|
|
||||||
|
|
||||||
Alerts.show({
|
|
||||||
title: "Restart Required",
|
|
||||||
body: "A restart is required to apply this change",
|
|
||||||
confirmText: "Restart now",
|
|
||||||
cancelText: "Later!",
|
|
||||||
onConfirm: relaunch
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorCard className="vc-settings-card">
|
|
||||||
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
|
|
||||||
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
|
|
||||||
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
|
|
||||||
<Forms.FormText>
|
|
||||||
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
|
|
||||||
</Forms.FormText>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
|
|
||||||
<div className="vc-settings-csp-list">
|
|
||||||
{errors.map((url, i) => (
|
|
||||||
<div key={url}>
|
|
||||||
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
|
|
||||||
<div className="vc-settings-csp-row">
|
|
||||||
<Link href={url}>{url}</Link>
|
|
||||||
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
|
|
||||||
Allow
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasImgurHtmlDomain && (
|
|
||||||
<>
|
|
||||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
|
|
||||||
<Forms.FormText>
|
|
||||||
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
|
|
||||||
</Forms.FormText>
|
|
||||||
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ErrorCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings, useSettings } from "@api/Settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import { FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
|
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
|
|
||||||
import { openPluginModal } from "@components/settings/tabs/plugins/PluginModal";
|
|
||||||
import { UserThemeHeader } from "@main/themes";
|
|
||||||
import { findLazy } from "@webpack";
|
|
||||||
import { Card, Forms, useEffect, useRef, useState } from "@webpack/common";
|
|
||||||
import ClientThemePlugin from "plugins/clientTheme";
|
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
|
||||||
|
|
||||||
import { ThemeCard } from "./ThemeCard";
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-settings-theme-");
|
|
||||||
|
|
||||||
type FileInput = ComponentType<{
|
|
||||||
ref: Ref<HTMLInputElement>;
|
|
||||||
onChange: (e: SyntheticEvent<HTMLInputElement>) => void;
|
|
||||||
multiple?: boolean;
|
|
||||||
filters?: { name?: string; extensions: string[]; }[];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
|
|
||||||
|
|
||||||
// When a local theme is enabled/disabled, update the settings
|
|
||||||
function onLocalThemeChange(fileName: string, value: boolean) {
|
|
||||||
if (value) {
|
|
||||||
if (Settings.enabledThemes.includes(fileName)) return;
|
|
||||||
Settings.enabledThemes = [...Settings.enabledThemes, fileName];
|
|
||||||
} else {
|
|
||||||
Settings.enabledThemes = Settings.enabledThemes.filter(f => f !== fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!e.currentTarget?.files?.length) return;
|
|
||||||
const { files } = e.currentTarget;
|
|
||||||
|
|
||||||
const uploads = Array.from(files, file => {
|
|
||||||
const { name } = file;
|
|
||||||
if (!name.endsWith(".css")) return;
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
VencordNative.themes.uploadTheme(name, reader.result as string)
|
|
||||||
.then(resolve)
|
|
||||||
.catch(reject);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(uploads);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LocalThemesTab() {
|
|
||||||
const settings = useSettings(["enabledThemes"]);
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refreshLocalThemes();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function refreshLocalThemes() {
|
|
||||||
const themes = await VencordNative.themes.getThemesList();
|
|
||||||
setUserThemes(themes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className="vc-settings-card">
|
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
|
||||||
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
|
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
|
||||||
BetterDiscord Themes
|
|
||||||
</Link>
|
|
||||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
|
||||||
</div>
|
|
||||||
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="vc-settings-card">
|
|
||||||
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
|
|
||||||
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
|
|
||||||
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Forms.FormSection title="Local Themes">
|
|
||||||
<QuickActionCard>
|
|
||||||
<>
|
|
||||||
{IS_WEB ?
|
|
||||||
(
|
|
||||||
<QuickAction
|
|
||||||
text={
|
|
||||||
<span style={{ position: "relative" }}>
|
|
||||||
Upload Theme
|
|
||||||
<FileInput
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={async e => {
|
|
||||||
await onFileUpload(e);
|
|
||||||
refreshLocalThemes();
|
|
||||||
}}
|
|
||||||
multiple={true}
|
|
||||||
filters={[{ extensions: ["css"] }]}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
Icon={PlusIcon}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<QuickAction
|
|
||||||
text="Open Themes Folder"
|
|
||||||
action={() => VencordNative.themes.openFolder()}
|
|
||||||
Icon={FolderIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<QuickAction
|
|
||||||
text="Load missing Themes"
|
|
||||||
action={refreshLocalThemes}
|
|
||||||
Icon={RestartIcon}
|
|
||||||
/>
|
|
||||||
<QuickAction
|
|
||||||
text="Edit QuickCSS"
|
|
||||||
action={() => VencordNative.quickCss.openEditor()}
|
|
||||||
Icon={PaintbrushIcon}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{Vencord.Plugins.isPluginEnabled(ClientThemePlugin.name) && (
|
|
||||||
<QuickAction
|
|
||||||
text="Edit ClientTheme"
|
|
||||||
action={() => openPluginModal(ClientThemePlugin)}
|
|
||||||
Icon={PencilIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</QuickActionCard>
|
|
||||||
|
|
||||||
<div className={cl("grid")}>
|
|
||||||
{userThemes?.map(theme => (
|
|
||||||
<ThemeCard
|
|
||||||
key={theme.fileName}
|
|
||||||
enabled={settings.enabledThemes.includes(theme.fileName)}
|
|
||||||
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
|
||||||
onDelete={async () => {
|
|
||||||
onLocalThemeChange(theme.fileName, false);
|
|
||||||
await VencordNative.themes.deleteTheme(theme.fileName);
|
|
||||||
refreshLocalThemes();
|
|
||||||
}}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Forms.FormSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useSettings } from "@api/Settings";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { Card, Forms, TextArea, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
export function OnlineThemesTab() {
|
|
||||||
const settings = useSettings(["themeLinks"]);
|
|
||||||
|
|
||||||
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
|
||||||
|
|
||||||
// When the user leaves the online theme textbox, update the settings
|
|
||||||
function onBlur() {
|
|
||||||
settings.themeLinks = [...new Set(
|
|
||||||
themeText
|
|
||||||
.trim()
|
|
||||||
.split(/\n+/)
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className={classes("vc-warning-card", Margins.bottom16)}>
|
|
||||||
<Forms.FormText>
|
|
||||||
This section is for advanced users. If you are having difficulties using it, use the
|
|
||||||
Local Themes tab instead.
|
|
||||||
</Forms.FormText>
|
|
||||||
</Card>
|
|
||||||
<Card className="vc-settings-card">
|
|
||||||
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
|
||||||
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
|
|
||||||
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Forms.FormSection title="Online Themes" tag="h5">
|
|
||||||
<TextArea
|
|
||||||
value={themeText}
|
|
||||||
onChange={setThemeText}
|
|
||||||
className={"vc-settings-theme-links"}
|
|
||||||
placeholder="Enter Theme Links..."
|
|
||||||
spellCheck={false}
|
|
||||||
onBlur={onBlur}
|
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
</Forms.FormSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2025 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { DeleteIcon } from "@components/Icons";
|
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { AddonCard } from "@components/settings/AddonCard";
|
|
||||||
import { UserThemeHeader } from "@main/themes";
|
|
||||||
import { openInviteModal } from "@utils/discord";
|
|
||||||
import { showToast } from "@webpack/common";
|
|
||||||
|
|
||||||
interface ThemeCardProps {
|
|
||||||
theme: UserThemeHeader;
|
|
||||||
enabled: boolean;
|
|
||||||
onChange: (enabled: boolean) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
|
|
||||||
return (
|
|
||||||
<AddonCard
|
|
||||||
name={theme.name}
|
|
||||||
description={theme.description}
|
|
||||||
author={theme.author}
|
|
||||||
enabled={enabled}
|
|
||||||
setEnabled={onChange}
|
|
||||||
infoButton={
|
|
||||||
IS_WEB && (
|
|
||||||
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
|
||||||
<DeleteIcon />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
footer={
|
|
||||||
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
|
|
||||||
{!!theme.website && <Link href={theme.website}>Website</Link>}
|
|
||||||
{!!(theme.website && theme.invite) && " • "}
|
|
||||||
{!!theme.invite && (
|
|
||||||
<Link
|
|
||||||
href={`https://discord.gg/${theme.invite}`}
|
|
||||||
onClick={async e => {
|
|
||||||
e.preventDefault();
|
|
||||||
theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite"));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Discord Server
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue