Merge branch 'Equicord:main' into main

This commit is contained in:
StealTech 2025-03-16 04:57:17 -05:00 committed by GitHub
commit 32a99bfc99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
147 changed files with 24320 additions and 960 deletions

View file

@ -16,7 +16,7 @@ permissions: write-all
jobs:
Build:
name: Build Equicord
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/checkout@v4

View file

@ -11,12 +11,38 @@ on:
jobs:
codeberg:
name: Sync Codeberg and Github
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
with:
target_repo_url: "git@codeberg.org:thororen/Equicord.git"
- name: Set up SSH private key
run: |
mkdir -p ~/.ssh
echo "${{ env.ssh_private_key }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
env:
ssh_private_key: ${{ secrets.CODEBERG }}
- name: Add SSH known host
run: |
mkdir -p ~/.ssh
ssh-keyscan -t rsa codeberg.org >> ~/.ssh/known_hosts
- name: Set up SSH username
run: |
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
- name: Add remote mirror and push
run: |
if git remote get-url mirror > /dev/null 2>&1; then
git remote remove mirror
fi
git remote add mirror "${{ env.target_repo_url}}"
git fetch --unshallow
git push --tags --force --prune mirror "refs/remotes/origin/*:refs/heads/*"
env:
target_repo_url: "git@codeberg.org:thororen/Equicord.git"
- name: Clean up
run: git remote remove mirror

View file

@ -17,7 +17,7 @@ on:
jobs:
TestPlugins:
name: Test Patches
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/checkout@v4

View file

@ -1,4 +1,7 @@
name: Sync Vencord Dev
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
env:
WORKFLOW_TOKEN: ${{ secrets.ETOKEN }}
@ -13,7 +16,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Sync Vencord Dev
id: sync

View file

@ -1,4 +1,7 @@
name: Sync Vencord Main
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
env:
WORKFLOW_TOKEN: ${{ secrets.ETOKEN }}
@ -13,7 +16,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Sync Vencord Main
id: sync

View file

@ -10,7 +10,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
### Extra included plugins
<details>
<summary>151 additional plugins</summary>
<summary>157 additional plugins</summary>
### All Platforms
- AllCallTimers by MaxHerbold & D3SOX
@ -34,6 +34,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- CharacterCounter by Creations & Panniku
- CleanChannelName by AutumnVN
- ClientSideBlock by Samwich
- ClipsEnhancements by niko
- CommandPalette by Ethan
- CopyUserMention by Cortex & castdrian
- CustomSounds by TheKodeToad & SpikeHD
@ -44,8 +45,8 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- DecodeBase64 by ThePirateStoner
- DeadMembers by Kyuuhachi
- Demonstration by Samwich
- DisableAnimations by S€th
- DisableCameras by Joona
- DoNotLeak by Perny
- DontFilterMe by Samwich
- EmojiDumper by Cortex, Samwich, Woosh
- Encryptcord by Inbestigator
@ -54,7 +55,9 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- FakeProfileThemesAndEffects by ryan
- FindReply by newwares
- FixFileExtensions by thororen
- FollowVoiceUser by TheArmagan
- FrequentQuickSwitcher by Samwich
- FriendCodes by HypedDomi
- FriendshipRanks by Samwich
- FullVcPfp by mochie
- FriendTags by Samwich
@ -63,16 +66,16 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- GifRoulette by Samwich
- Glide by Samwich
- GlobalBadges by HypedDomi & Hosted by Wolfie
- GodMode by Tolgchu
- GoogleThat by Samwich
- HideChatButtons by iamme
- HideMessage by Hanzy
- HideServers by bepvte
- HolyNotes by Wolfie
- HomeTyping by Samwich
- HopOn by ImLvna
- Husk by nin0dev
- IconViewer by iamme
- Identity by Samwich
- IgnoreCalls by TheArmagan
- IgnoreTerms by D3SOX
- ImagePreview by Creations
- ImgToGif by zyqunix
@ -112,6 +115,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- QuestCompleter by Amia
- QuestionMarkReplacement by nyx
- Quoter by Samwich
- Remix by MrDiamond
- RemixMe by kvba
- RepeatMessage by Tolgchu
- ReplyPingControl by ant0n & MrDiamond
@ -126,6 +130,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- StatsfmRPC by Crxaw & vmohammad
- Slap by Korbo
- SoundBoardLogger by Moxxie, fres, echo, maintained by thororen
- SpotifyLyrics by Joona
- StatusPresets by iamme
- SteamStatusSync by niko
- StickerBlocker by Samwich
@ -145,6 +150,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- UwUifier by echo
- VCSupport by thororen
- VCNarratorCustom by Loukios, ported by example-git
- VCPanelSettings by nin0dev
- VencordRPC by AutumnVN
- VideoSpeed by Samwich
- ViewRawVariant (ViewRaw2) by Kyuuhachi
@ -186,6 +192,8 @@ MacOS
- [GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl.MacOS.zip)
Linux
- [GUI-X11](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-x11)
- [GUI-Wayland](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-wayland)
- [CLI](https://github.com/Equicord/Equilotl/releases/latest/download/EquilotlCli-Linux)
- [AUR](https://aur.archlinux.org/packages?O=0&K=equicord)
```shell
@ -216,7 +224,7 @@ cd Equicord
Install dependencies:
```shell
pnpm install --frozen-lockfile
pnpm install --no-frozen-lockfile
```
Build Equicord:

View file

@ -1,50 +1,116 @@
#!/bin/sh
set -e
#!/bin/bash
# Constants
# Configuration
INSTALLER_PATH="$HOME/.equilotl"
GITHUB_URL="https://github.com/Equicord/Equilotl/releases/latest/download/EquilotlCli-Linux"
PRIVILEGE_CMDS=("sudo" "doas")
DEBUG=false
LOG_FILE="$(dirname "$(realpath "$0")")/equicordinstalldebug.log"
# Check for root
if [ "$(id -u)" -eq 0 ]; then
echo "Run me as a normal user, not root!"
exit 1
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
download_installer() {
curl -sSL "$GITHUB_URL" --output "$INSTALLER_PATH"
chmod +x "$INSTALLER_PATH"
# Debug logging
debug_log() {
if $DEBUG; then
set -euo pipefail
local timestamp
timestamp=$(date +"%Y-%m-%d %T")
echo -e "[$timestamp] $1" | tee -a "$LOG_FILE"
fi
}
echo "Checking if the installer needs updating..."
if [ -f "$INSTALLER_PATH" ]; then
latest_modified=$(curl -sI "$GITHUB_URL" | grep -i "last-modified" | cut -d' ' -f2-)
local_modified=$(stat -c "%y" "$INSTALLER_PATH" | cut -d' ' -f1-2)
if [ "$local_modified" = "$latest_modified" ]; then
echo "The installer is up-to-date."
else
echo "The installer is outdated. Downloading the latest version..."
download_installer
fi
else
echo "Installer not found. Downloading it..."
download_installer
fi
# Try to run the installer with sudo or doas
if command -v sudo >/dev/null; then
echo "Running installer with sudo..."
sudo "$INSTALLER_PATH"
elif command -v doas >/dev/null; then
echo "Running installer with doas..."
doas "$INSTALLER_PATH"
else
echo "Neither sudo nor doas were found. Please install one to proceed."
# Error handling
error() {
echo -e "${RED}Error: $1${NC}" >&2
exit 1
fi
}
# Credits
echo "Original script forked from Vencord"
echo "Modified by PhoenixAceVFX & Crxaw for Equicord Updater"
# Check for root
check_root() {
if [ "$(id -u)" -eq 0 ]; then
error "This script should not be run as root. Please run as a normal user."
fi
}
# Download the installer
download_installer() {
echo -e "${YELLOW}Downloading installer...${NC}"
if ! curl -sSL "$GITHUB_URL" --output "$INSTALLER_PATH"; then
error "Failed to download installer from GitHub"
fi
chmod +x "$INSTALLER_PATH" || error "Failed to make installer executable"
}
# Check if installer needs update
check_for_updates() {
if [ ! -f "$INSTALLER_PATH" ]; then
echo -e "${YELLOW}Installer not found. Downloading...${NC}"
download_installer
return
fi
local latest_modified local_modified
if ! latest_modified=$(curl -sI "$GITHUB_URL" | grep -i "last-modified" | cut -d' ' -f2-); then
echo -e "${YELLOW}Warning: Could not fetch last modified date from GitHub. Using existing installer.${NC}"
return
fi
local_modified=$(stat -c "%y" "$INSTALLER_PATH" | cut -d' ' -f1-2) || error "Failed to get local modified date"
if [ "$local_modified" != "$latest_modified" ]; then
echo -e "${YELLOW}Installer is outdated. Updating...${NC}"
download_installer
else
echo -e "${GREEN}Installer is up-to-date.${NC}"
fi
}
# Find privilege escalation command
find_privilege_cmd() {
for cmd in "${PRIVILEGE_CMDS[@]}"; do
if command -v "$cmd" >/dev/null; then
echo "$cmd"
return
fi
done
error "Neither sudo nor doas found. Please install one to proceed."
}
# Main execution
main() {
# Check for debug flag
if [[ "${1:-}" == "-debug" ]]; then
DEBUG=true
> "$LOG_FILE" # Clear previous log
debug_log "Debug mode enabled"
fi
debug_log "Starting installation process"
check_root
check_for_updates
local priv_cmd
priv_cmd=$(find_privilege_cmd)
debug_log "Using privilege command: $priv_cmd"
echo -e "${YELLOW}Running installer with $priv_cmd...${NC}"
debug_log "Executing installer: $priv_cmd $INSTALLER_PATH"
if ! "$priv_cmd" "$INSTALLER_PATH"; then
debug_log "Installer failed"
error "Installer failed to run"
fi
debug_log "Installation completed successfully"
echo -e "\n${GREEN}Installation completed successfully!${NC}"
echo -e "\nCredits:"
echo "Original script forked from Vencord"
echo "Modified by PhoenixAceVFX & Crxaw for Equicord Updater"
echo "Rewrite by PhoenixAceVFX"
}
# Pass arguments to main
main "$@"

18735
misc/keys-mapping.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.11.5",
"version": "1.11.6",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {

110
pnpm-lock.yaml generated
View file

@ -6,10 +6,10 @@ settings:
patchedDependencies:
'@types/less@3.0.6':
hash: krcufrsfhsuxuoj7hocqugs6zi
hash: 641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076
path: patches/@types__less@3.0.6.patch
eslint@9.20.1:
hash: xm46kqcmdgzlmm4aifkfpxaho4
hash: 4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215
path: patches/eslint@9.20.1.patch
importers:
@ -30,7 +30,7 @@ importers:
version: 0.3.5
'@types/less':
specifier: ^3.0.6
version: 3.0.6(patch_hash=krcufrsfhsuxuoj7hocqugs6zi)
version: 3.0.6(patch_hash=641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076)
'@types/stylus':
specifier: ^0.48.42
version: 0.48.42
@ -79,7 +79,7 @@ importers:
version: 3.2.10
'@stylistic/eslint-plugin':
specifier: ^4.0.0
version: 4.0.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
version: 4.0.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
'@types/chrome':
specifier: ^0.0.304
version: 0.0.304
@ -112,22 +112,22 @@ importers:
version: 0.25.0
eslint:
specifier: ^9.20.1
version: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
version: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-import-resolver-alias:
specifier: ^1.1.2
version: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)))
version: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)))
eslint-plugin-react:
specifier: ^7.37.3
version: 7.37.4(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
version: 7.37.4(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
eslint-plugin-simple-header:
specifier: ^1.2.1
version: 1.2.2(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
version: 1.2.2(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
eslint-plugin-simple-import-sort:
specifier: ^12.1.1
version: 12.1.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
version: 12.1.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
eslint-plugin-unused-imports:
specifier: ^4.1.4
version: 4.1.4(@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
version: 4.1.4(@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
highlight.js:
specifier: 11.11.1
version: 11.11.1
@ -169,7 +169,7 @@ importers:
version: 5.7.3
typescript-eslint:
specifier: ^8.19.0
version: 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
version: 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
typescript-transform-paths:
specifier: ^3.5.3
version: 3.5.3(typescript@5.7.3)
@ -2887,9 +2887,9 @@ snapshots:
'@esbuild/win32-x64@0.25.0':
optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))':
'@eslint-community/eslint-utils@4.4.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))':
dependencies:
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@ -3013,10 +3013,10 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {}
'@stylistic/eslint-plugin@4.0.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)':
'@stylistic/eslint-plugin@4.0.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-visitor-keys: 4.2.0
espree: 10.3.0
estraverse: 5.3.0
@ -3061,7 +3061,7 @@ snapshots:
dependencies:
'@types/node': 22.13.4
'@types/less@3.0.6(patch_hash=krcufrsfhsuxuoj7hocqugs6zi)': {}
'@types/less@3.0.6(patch_hash=641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076)': {}
'@types/lodash@4.17.15': {}
@ -3123,15 +3123,15 @@ snapshots:
dependencies:
'@types/node': 22.13.4
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)':
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
'@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/type-utils': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/type-utils': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
'@typescript-eslint/visitor-keys': 8.24.1
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
graphemer: 1.4.0
ignore: 5.3.1
natural-compare: 1.4.0
@ -3140,14 +3140,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)':
'@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/types': 8.24.1
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3)
'@typescript-eslint/visitor-keys': 8.24.1
debug: 4.4.0
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@ -3157,12 +3157,12 @@ snapshots:
'@typescript-eslint/types': 8.24.1
'@typescript-eslint/visitor-keys': 8.24.1
'@typescript-eslint/type-utils@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)':
'@typescript-eslint/type-utils@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
debug: 4.4.0
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
ts-api-utils: 2.0.1(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
@ -3184,13 +3184,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)':
'@typescript-eslint/utils@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
'@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
'@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/types': 8.24.1
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3)
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@ -3816,9 +3816,9 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))):
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))):
dependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
eslint-import-resolver-node@0.3.9:
dependencies:
@ -3828,17 +3828,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@ -3847,9 +3847,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -3861,13 +3861,13 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-react@7.37.4(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
eslint-plugin-react@7.37.4(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
dependencies:
array-includes: 3.1.8
array.prototype.findlast: 1.2.5
@ -3875,7 +3875,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.1
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
@ -3889,19 +3889,19 @@ snapshots:
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
eslint-plugin-simple-header@1.2.2(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
eslint-plugin-simple-header@1.2.2(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
dependencies:
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-plugin-simple-import-sort@12.1.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
eslint-plugin-simple-import-sort@12.1.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
dependencies:
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
dependencies:
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
eslint-scope@8.2.0:
dependencies:
@ -3912,9 +3912,9 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4):
eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
'@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.2
'@eslint/core': 0.11.0
@ -5268,12 +5268,12 @@ snapshots:
typed-query-selector@2.12.0: {}
typescript-eslint@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3):
typescript-eslint@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.3)
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.7.3)
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
typescript: 5.7.3
transitivePeerDependencies:
- supports-color

View file

@ -77,14 +77,14 @@ const buildConfigs = [
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/vendor/monaco"
outdir: "dist/browser/vendor/monaco"
},
{
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/vendor/monaco/index.js",
outfile: "dist/browser/vendor/monaco/index.js",
loader: {
".ttf": "file"
}
@ -96,7 +96,7 @@ const buildConfigs = [
},
{
...commonOptions,
outfile: "dist/extension.js",
outfile: "dist/browser/extension.js",
define: {
...commonOptions.define,
IS_EXTENSION: "true"
@ -160,9 +160,9 @@ async function loadDir(dir, basePath = "") {
*/
async function buildExtension(target, files) {
const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
...await loadDir("dist/vendor/monaco", "dist/"),
"dist/Vencord.js": await readFile("dist/browser/extension.js"),
"dist/Vencord.css": await readFile("dist/browser/extension.css"),
...await loadDir("dist/browser/vendor/monaco", "dist/browser/"),
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
))),

View file

@ -44,11 +44,16 @@ export let OptionalMessageOption: Option = OptPlaceholder;
*/
export let RequiredMessageOption: Option = ReqPlaceholder;
// Discords command list has random gaps for some reason, which can cause issues while rendering the commands
// add this offset too every added command to keep them unique
let commandIdOffset: number;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
commandIdOffset = Math.abs(BUILT_IN.map(x => +x.id!).toSorted((x, y) => x - y)[0]) - BUILT_IN.length;
} catch (e) {
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
}
@ -142,7 +147,7 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
command.isVencordCommand = true;
command.untranslatedName ??= command.name;
command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`;
command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;

View file

@ -114,7 +114,7 @@ const DefaultSettings: Settings = {
cloud: {
authenticated: false,
url: "https://cloud.equicord.fyi/",
url: "https://cloud.equicord.org/",
settingsSync: false,
settingsSyncVersion: 0
},

View file

@ -32,7 +32,7 @@ export default function DonateButton({
{...props}
look={look}
color={color}
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/verticalsync")}
onClick={() => VencordNative.native.openExternal("https://ko-fi.com/authenticators")}
innerClassName="vc-donate-button"
>
<Heart />

View file

@ -0,0 +1,49 @@
.vc-settings-theme-grid {
display: grid;
grid-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.vc-settings-theme-card {
display: flex;
flex-direction: column;
background-color: var(--background-secondary-alt);
color: var(--interactive-active);
border-radius: 8px;
padding: 1em;
width: 100%;
transition: 0.1s ease-out;
transition-property: box-shadow, transform, background, opacity;
}
.vc-settings-theme-card-text {
text-overflow: ellipsis;
height: 1.2em;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
}
.vc-settings-theme-author::before {
content: "by ";
}
.vc-settings-theme-link-input {
width: 100%;
}
.vc-settings-theme-add-card {
padding: 1em;
margin-bottom: 16px;
}
.vc-settings-usercss-ie-buttons>div {
color: var(--interactive-normal);
opacity: .5;
padding: 4px;
}
.vc-settings-usercss-ie-buttons>div:hover {
color: var(--interactive-hover);
opacity: 1;
}

View file

@ -48,8 +48,6 @@ function EquicordSettings() {
});
const settings = useSettings();
const discordInvite = "bFp57wxCkv";
const vcDiscordInvite = "https://discord.gg/KGgvd6jPFu";
const donateImage = React.useMemo(
() => (Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE),
[],
@ -122,14 +120,14 @@ function EquicordSettings() {
return (
<SettingsTab title="Equicord Settings">
{(isDonor(user?.id) || isVCDonor(user?.id)) ? (
{(isEquicordDonor(user?.id) || isVencordDonor(user?.id)) ? (
<SpecialCard
title="Donations"
subtitle="Thank you for donating!"
description={
isDonor(user?.id) && isVCDonor(user?.id)
isEquicordDonor(user?.id) && isVencordDonor(user?.id)
? "All Vencord users can see your Vencord donor badge, and Equicord users can see your Equicord donor badge. To change your Vencord donor badge, contact @vending.machine. For your Equicord donor badge, make a ticket in Equicord's server."
: isVCDonor(user?.id)
: isVencordDonor(user?.id)
? "All Vencord users can see your badge! You can change it at any time by messaging @vending.machine."
: "All Equicord users can see your badge! You can change it at any time by making a ticket in Equicord's server."
}
@ -347,13 +345,13 @@ function DonateButtonComponent() {
);
}
function isVCDonor(userId: string): boolean {
function isVencordDonor(userId: string): boolean {
const donorBadges = BadgeAPI.getDonorBadges(userId);
return GuildMemberStore.getMember(VC_GUILD_ID, userId)?.roles.includes(VC_DONOR_ROLE_ID) || !!donorBadges;
}
function isDonor(userId: string): boolean {
const donorBadges = BadgeAPI.getDonorBadges(userId);
export function isEquicordDonor(userId: string): boolean {
const donorBadges = BadgeAPI.getEquicordDonorBadges(userId);
return GuildMemberStore.getMember(GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
}

View file

@ -36,14 +36,3 @@
padding: 1em;
margin-bottom: 16px;
}
.vc-settings-usercss-ie-buttons>div {
color: var(--interactive-normal);
opacity: .5;
padding: 4px;
}
.vc-settings-usercss-ie-buttons>div:hover {
color: var(--interactive-hover);
opacity: 1;
}

View file

@ -16,13 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "@equicordplugins/_misc/styles.css";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Forms } from "@webpack/common";
export default definePlugin({
name: "EquicordHelper",
description: "Fixes some misc issues with discord",
authors: [EquicordDevs.thororen, EquicordDevs.nyx],
settingsAboutComponent: () => <>
<Forms.FormText className="plugin-warning">
This Plugin is used for fixing misc issues with discord such as some crashes
</Forms.FormText>
</>,
required: true,
patches: [
{

View file

@ -1,4 +1,4 @@
.quests-warning{
.plugin-warning {
font-size: 16px !important;
background-color: var(--info-warning-background) !important;
color: var(--info-warning-text) !important;

View file

@ -123,17 +123,6 @@ const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: A
);
};
function getActivityApplication(activity: Activity | null) {
if (!activity) return undefined;
const { application_id } = activity;
if (!application_id) return undefined;
let application = ApplicationStore.getApplication(application_id);
if (!application && fetchedApplications.has(application_id)) {
application = fetchedApplications.get(application_id) ?? null;
}
return application ?? undefined;
}
function getApplicationIcons(activities: Activity[], preferSmall = false) {
const applicationIcons: ApplicationIcon[] = [];
const applications = activities.filter(activity => activity.application_id || activity.platform);
@ -308,6 +297,13 @@ export default definePlugin({
},
patches: [
{
find: "activity_status_cleanup",
replacement: {
match: /activityStatusCleanupEnabled:!0/,
replace: "activityStatusCleanupEnabled:!1",
}
},
{
// Patch activity icons
find: '"activity-status-web"',

View file

@ -0,0 +1,90 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { EquicordDevs } from "@utils/constants";
import { getIntlMessage, openUserProfile } from "@utils/discord";
import definePlugin from "@utils/types";
import { Button, React, RelationshipStore, TextInput, UserStore } from "@webpack/common";
let lastSearch = "";
let updateFunc = (v: any) => { };
export default definePlugin({
name: "BetterBlockedUsers",
description: "Allows you to search in blocked users list and makes names selectable in settings.",
authors: [EquicordDevs.TheArmagan],
patches: [
{
find: '"],{numberOfBlockedUsers:',
replacement: [
{
match: /(?<=\}=(\i).*?\]\}\))/,
replace: ",$1.listType==='blocked'?$self.renderSearchInput():null"
},
{
match: /(?<=userId:(\i).*?\}\)\]\}\),)(\(.*?\)\}\))/,
replace: "$self.renderUser($1,$2),",
},
{
match: /(?<=\}=(\i).{0,10}(\i).useState\(.{0,1}\);)/,
replace: "let [searchResults,setSearchResults]=$2.useState([]);$self.setUpdateFunc($1,setSearchResults);"
},
{
match: /(usersList,children:)(\i)/,
replace: "$1(searchResults.length?searchResults:$2)"
},
]
}
],
renderSearchInput() {
const [value, setValue] = React.useState(lastSearch);
React.useEffect(() => {
const searchResults = this.getFilteredUsers(lastSearch);
updateFunc(searchResults);
}, []);
return <TextInput
placeholder="Search users..."
style={{ width: "200px" }}
onInput={e => {
const search = (e.target as HTMLInputElement).value.toLowerCase().trim();
setValue(search);
lastSearch = search;
const searchResults = this.getFilteredUsers(search);
updateFunc(searchResults);
}} value={value}
></TextInput>;
},
renderUser(userId: string, rest: any) {
return (
<div style={{ display: "flex", gap: "8px" }}>
<Button color={Button.Colors.PRIMARY} onClick={() => openUserProfile(userId)}>
{getIntlMessage("SHOW_USER_PROFILE")}
</Button>
{rest}
</div>
);
},
getSearchResults() {
return !!lastSearch;
},
setUpdateFunc(e, setResults) {
if (e.listType !== "blocked") return;
updateFunc = setResults;
return true;
},
getFilteredUsers(search: string) {
search = search.toLowerCase();
return (RelationshipStore as any).getBlockedIDs().filter(id => {
const user = UserStore.getUser(id) as any;
if (!user) return id === search;
return id === search || user?.username?.toLowerCase()?.includes(search) || user?.globalName?.toLowerCase()?.includes(search);
}) as string[];
}
});

View file

@ -0,0 +1,3 @@
[class*="usersList_"] [class*="text_"] {
user-select: text !important;
}

View file

@ -44,7 +44,7 @@ export default definePlugin({
replace: ",($1||((!$1)&&arguments[0].invite.expires_at)) && $2$self.RenderTip($1, $3, arguments[0].invite.expires_at)"
},
{
match: /(\.jsx\)\(\i.\i.Info,{.+onClick:\i\?.{0,5}:null)/,
match: /(\.jsx\)\(\i.\i.Info,{.+onClick:\i)/,
replace: "$& || $self.Lurkable(arguments[0].invite.guild.id, arguments[0].invite.guild.features)"
},
{
@ -73,4 +73,3 @@ export default definePlugin({
},
startAt: StartAt.WebpackReady
});

View file

@ -48,8 +48,8 @@ export default definePlugin({
{
find: ".COLLECTIBLES_SHOP_FULLSCREEN))",
replacement: {
match: /(\?void 0:(\i)\.channelId.{0,300})\i\.Fragment,{/,
replace: "$1$self.render,{currentChannel:$2,"
match: /(\?void 0:(\i)\.channelId.{0,300}return)((.{0,15})"div",{.*?\])(\}\)\}\})/,
replace: "$1$4$self.render,{currentChannel:$2,children:$3})$5"
}
},
// ctrl click to open in new tab in inbox unread

View file

@ -0,0 +1,114 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { ModalAPI } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { PresenceStore, UserStore } from "@webpack/common";
import { SelectOption } from "@webpack/types";
interface Activity {
name: string;
application_id?: string;
type: number;
}
const settings = definePluginSettings({
richPresenceTagging: {
type: OptionType.SELECT,
description: "When should clips be tagged with the current Rich Presence?",
options: [
{ label: "Always", value: "always" },
{ label: "Only when beginning or end of activity name matches", value: "whenMatched", default: true },
{ label: "Never", value: "never" },
]
},
clipsLink: {
type: OptionType.COMPONENT,
description: "",
component: () => {
return (
<>
<Link
href={findStoreLazy("ExperimentStore").getUserExperimentBucket("2024-04_game_settings_merge") === 1 ? "discord://-/settings/games/clips" : "discord://-/settings/clips"}
onClick={() => {
ModalAPI.closeAllModals();
}}
>
Change FPS and duration options in Clips settings!
</Link >
</>
);
}
},
});
export default definePlugin({
name: "ClipsEnhancements",
description: "Add more Clip FPS and duration options, plus RPC tagging!",
authors: [Devs.niko],
settings,
patches: [
{
find: "clips_recording_settings",
replacement: [
{
match: /\[\{.{0,10}\i.\i.FPS_15.{0,250}\}\]/,
replace: "$self.patchFramerates($&)"
},
{
match: /\[\{.{0,10}\i.\i.SECONDS_30.{0,250}\}\]/,
replace: "$self.patchTimeslots($&)"
},
]
},
{
find: "clipMethod:\"manual\"",
replacement: {
match: /(applicationName:)(.{0,50})(,applicationId:)(\i)/,
replace: "$1$2$3$self.getApplicationId($2)??$4"
}
}
],
patchTimeslots(timeslots: SelectOption[]) {
const newTimeslots = [...timeslots];
const extraTimeslots = [3, 5, 7, 10];
extraTimeslots.forEach(timeslot => newTimeslots.push({ value: timeslot * 60000, label: `${timeslot} Minutes` }));
return newTimeslots;
},
patchFramerates(framerates: SelectOption[]) {
const newFramerates = [...framerates];
const extraFramerates = [45, 90, 120, 144, 165, 240];
// Lower framerates than 15FPS have adverse affects on compression, 3 minute clips at 10FPS skyrocket the filesize to 200mb!!
extraFramerates.forEach(framerate => newFramerates.push({ value: framerate, label: `${framerate}FPS` }));
return newFramerates.toSorted();
},
getApplicationId(activityName: string) {
if (settings.store.richPresenceTagging === "never") {
return null;
}
const activities: Activity[] = PresenceStore.getActivities(UserStore.getCurrentUser().id);
const validActivities = activities.filter(activity => activity.type === 0 && activity.application_id !== null);
const splitName = activityName.split(" ");
// Try to match activity by it's start and end
const matchedActivities = validActivities.filter(activity => activity.name.endsWith(splitName.at(-1)!) || activity.name.startsWith(splitName.at(0)!));
return (matchedActivities ?? (settings.store.richPresenceTagging === "whenMatched" ? null : validActivities))[0]?.application_id;
}
});

View file

@ -92,16 +92,30 @@ export default definePlugin({
},
{
predicate: () => settings.store.dmList,
find: "!1,wrapContent",
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(nameAndDecorators,)/,
replace: "$1style:{color:$self.colorDMList(arguments[0])},"
match: /(highlighted:\i,)/,
replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`},"
},
},
{
predicate: () => settings.store.dmList,
find: "!1,wrapContent",
replacement: [
{
match: /(\}=\i)/,
replace: ",style$1"
},
{
match: /(?<=nameAndDecorators,)/,
replace: "style:style||{},"
},
],
},
],
colorDMList(a: any): string | undefined {
const userId = a?.subText?.props?.user?.id;
const userId = a?.user?.id;
if (!userId) return;
const colorString = getCustomColorString(userId, true);
if (colorString) return colorString;
@ -109,11 +123,11 @@ export default definePlugin({
},
colorIfServer(a: any): string | undefined {
const roleColor = a.author?.colorString ?? "inherit";
const roleColor = a.author?.colorString;
if (a?.channel?.guild_id && !settings.store.colorInServers) return roleColor;
const color = getCustomColorString(a.message.author.id, true);
return color ?? roleColor;
return color ?? roleColor ?? undefined;
}
});

View file

@ -24,7 +24,7 @@ export default definePlugin({
{
find: "#{intl::FORUM_POST_AUTHOR_A11Y_LABEL}",
replacement: {
match: /(?<=\}=(\i),\{(user:\i,author:\i)\}=.{0,400}?\(\i\.Fragment,{children:)\i(?=}\),)/,
match: /(?<=\}=(\i),\{(user:\i,author:\i)\}=.{0,400}?\i:void 0\)\()\i/,
replace: "$self.wrapForumAuthor({...$1,$2},$&)"
}
},

View file

@ -147,6 +147,5 @@ export default definePlugin({
},
stop() {
document.removeEventListener("keydown", handleKeydown);
},
}
});

View file

@ -18,16 +18,36 @@
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { PermissionStore } from "@webpack/common";
import { findAll } from "@webpack";
export default definePlugin({
name: "GodMode",
description: "Get all permissions (client-side).",
authors: [EquicordDevs.Tolgchu],
name: "DisableAnimations",
description: "Disables most of Discord's animations.",
authors: [EquicordDevs.seth],
start() {
this.springs = findAll(mod => {
if (!mod.Globals) return false;
return true;
});
start: () => {
["can", "canAccessMemberSafetyPage", "canAccessGuildSettings", "canBasicChannel", "canImpersonateRole", "canManageUser", "canWithPartialContext", "getGuildVersion", "getChannelsVersion", "getChannelPermissions", "getHighestRole", "initialize", "constructor", "isRoleHigher"].forEach(a => PermissionStore.__proto__[a] = () => !0);
for (const spring of this.springs) {
spring.Globals.assign({
skipAnimation: true,
});
}
this.css = document.createElement("style");
this.css.innerText = "* { transition: none !important; animation: none !important; }";
document.head.appendChild(this.css);
},
stop() {
for (const spring of this.springs) {
spring.Globals.assign({
skipAnimation: false,
});
}
stop: () => { }
});
if (this.css) this.css.remove();
}
});

View file

@ -1,85 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { getStyle } from "./style";
const settings = definePluginSettings({
hoverToView: {
type: OptionType.BOOLEAN,
description: "When hovering over a message, show the contents.",
default: false,
onChange: () => {
console.log(settings.store.hoverToView);
updateClassList("hover-to-view", settings.store.hoverToView);
},
},
keybind: {
type: OptionType.STRING,
description: "The keybind to show the contents of a message.",
default: "Insert",
restartNeeded: false,
},
enableForStream: {
type: OptionType.BOOLEAN,
description: "Blur all messages in streamer mode.",
default: false,
onChange: () => {
console.log(settings.store.enableForStream);
updateClassList(
"hide-in-streamer-mode",
settings.store.enableForStream
);
},
},
});
export default definePlugin({
name: "DoNotLeak",
tags: ["DontLeak"],
description: "Hide all message contents and attachments when you're streaming or sharing your screen.",
authors: [EquicordDevs.Perny],
settings,
start() {
const styles = getStyle();
const style = document.createElement("style");
style.setAttribute("id", "vc-dont-leak-style");
style.innerHTML = styles;
document.head.appendChild(style);
document.addEventListener("keyup", keyUpHandler);
document.addEventListener("keydown", keyDownHandler);
updateClassList("hover-to-view", settings.store.hoverToView);
updateClassList("hide-in-streamer-mode", settings.store.enableForStream);
},
stop() {
document.removeEventListener("keyup", keyUpHandler);
document.removeEventListener("keydown", keyDownHandler);
document.getElementById("vc-dont-leak-style")?.remove();
},
});
function updateClassList(className, condition) {
if (condition) {
document.body.classList.add(`vc-dnl-${className}`);
return;
}
document.body.classList.remove(`vc-dnl-${className}`);
}
function keyUpHandler(e: KeyboardEvent) {
if (e.key !== settings.store.keybind) return;
updateClassList("show-messages", false);
}
function keyDownHandler(e: KeyboardEvent) {
if (e.key !== settings.store.keybind) return;
updateClassList("show-messages", true);
}

View file

@ -1,116 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByProps } from "@webpack";
const CssFormatCode: string = `body:has(
div.{sidebar}
> section
div.{wrapper}
div.{actionButtons}
> button:nth-child(2).{buttonActive}
)
.{messageContent} {
filter: blur(12px);
}
body:has(
div.{sidebar}
> section
div.{wrapper}
div.{actionButtons}
> button:nth-child(2).{buttonActive}
)
.{visualMediaItemContainer} {
filter: blur(50px) brightness(0.1);
}
body:has(
div.{sidebar}
> section
div.{wrapper}
div.{actionButtons}
> button:nth-child(2).{buttonActive}
)
.{embedWrapper} {
filter: blur(50px);
}
body.vc-dnl-hide-in-streamer-mode:has(.{notice}.{colorStreamerMode})
.{visualMediaItemContainer} {
filter: blur(50px) brightness(0.1);
}
body.vc-dnl-hide-in-streamer-mode:has(.{notice}.{colorStreamerMode})
.{messageContent} {
filter: blur(12px);
}
body.vc-dnl-hide-in-streamer-mode:has(.{notice}.{colorStreamerMode})
.{embedWrapper} {
filter: blur(50px);
}
body.vc-dnl-show-messages .{visualMediaItemContainer} {
filter: blur(0px) brightness(1) !important;
}
body.vc-dnl-show-messages .{messageContent} {
filter: blur(0px) !important;
}
body.vc-dnl-show-messages .{embedWrapper} {
filter: blur(0px) !important;
}
body.vc-dnl-hover-to-view .{messageContent}:hover {
filter: blur(0px) brightness(1) !important;
}
body.vc-dnl-hover-to-view .{embedWrapper}:hover {
filter: blur(0px) brightness(1) !important;
}
body.vc-dnl-hover-to-view .{visualMediaItemContainer}:hover {
filter: blur(0px) brightness(1) !important;
}`;
/*
[
"sidebar",
"wrapper",
"actionButtons",
"buttonActive",
"messageContent",
"visualMediaItemContainer",
"embedWrapper",
"notice",
"colorStreamerMode",
]
*/
export function getStyle(): string {
const messageContent = findByProps("messageContent", "titleCase"); // ["messageContent","wrapper"]
const embedWrapper = findByProps("embedWrapper");
const mediaContainer = findByProps("visualMediaItemContainer");
const notice = findByProps("colorStreamerMode", "notice");
const actionBar = findByProps("actionButtons", "buttonActive", "wrapper");
const sidebar = findByProps("sidebar", "panels");
const Classes = Object.assign(
{},
actionBar,
notice,
mediaContainer,
embedWrapper,
messageContent,
sidebar
);
let CssCode = CssFormatCode;
for (const className in Classes) {
CssCode = CssCode.replaceAll(`{${className}}`, Classes[className]);
}
return CssCode;
}

View file

@ -44,6 +44,25 @@ function VencordPopout(onClose: () => void) {
{Object.entries(plugin.toolboxActions).map(([text, action]) => {
const key = `vc-toolbox-${plugin.name}-${text}`;
if (plugin.name === "Demonstration") {
const [demonstrationToggled, setToggled] = useState(false);
return (
<Menu.MenuCheckboxItem
id="vc-toolbox-demonstration-toggle"
key={key}
checked={!!demonstrationToggled}
label={text}
action={
() => {
action();
setToggled(!demonstrationToggled);
}
}
/>
);
}
return (
<Menu.MenuItem
id={key}

View file

@ -0,0 +1,119 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "@equicordplugins/_misc/styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Forms, Menu, React } from "@webpack/common";
import { VoiceState } from "@webpack/types";
import { Channel, User } from "discord-types/general";
type TFollowedUserInfo = {
lastChannelId: string;
userId: string;
} | null;
interface UserContextProps {
channel: Channel;
user: User;
guildId?: string;
}
let followedUserInfo: TFollowedUserInfo = null;
const voiceChannelAction = findByPropsLazy("selectVoiceChannel");
const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserStore = findStoreLazy("UserStore");
const RelationshipStore = findStoreLazy("RelationshipStore");
const settings = definePluginSettings({
onlyWhenInVoice: {
type: OptionType.BOOLEAN,
default: true,
description: "Only follow the user when you are in a voice channel"
},
leaveWhenUserLeaves: {
type: OptionType.BOOLEAN,
default: false,
description: "Leave the voice channel when the user leaves. (That can cause you to sometimes enter infinite leave/join loop)"
}
});
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, user }: UserContextProps) => {
if (UserStore.getCurrentUser().id === user.id || !RelationshipStore.getFriendIDs().includes(user.id)) return;
const [checked, setChecked] = React.useState(followedUserInfo?.userId === user.id);
children.push(
<Menu.MenuSeparator />,
<Menu.MenuCheckboxItem
id="fvu-follow-user"
label="Follow User"
checked={checked}
action={() => {
if (followedUserInfo?.userId === user.id) {
followedUserInfo = null;
setChecked(false);
return;
}
followedUserInfo = {
lastChannelId: UserStore.getCurrentUser().id,
userId: user.id
};
setChecked(true);
}}
></Menu.MenuCheckboxItem>
);
};
export default definePlugin({
name: "FollowVoiceUser",
description: "Follow a friend in voice chat.",
authors: [EquicordDevs.TheArmagan],
settings,
settingsAboutComponent: () => <>
<Forms.FormText className="plugin-warning">
This Plugin is used to follow a Friend/Friends into voice chat(s).
</Forms.FormText>
</>,
flux: {
async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
if (!followedUserInfo) return;
if (!RelationshipStore.getFriendIDs().includes(followedUserInfo.userId)) return;
if (
settings.store.onlyWhenInVoice
&& VoiceStateStore.getVoiceStateForUser(UserStore.getCurrentUser().id) === null
) return;
voiceStates.forEach(voiceState => {
if (
voiceState.userId === followedUserInfo!.userId
&& voiceState.channelId
&& voiceState.channelId !== followedUserInfo!.lastChannelId
) {
followedUserInfo!.lastChannelId = voiceState.channelId;
voiceChannelAction.selectVoiceChannel(followedUserInfo!.lastChannelId);
} else if (
voiceState.userId === followedUserInfo!.userId
&& !voiceState.channelId
&& settings.store.leaveWhenUserLeaves
) {
voiceChannelAction.selectVoiceChannel(null);
}
});
}
},
contextMenus: {
"user-context": UserContextMenuPatch
}
});

View file

@ -0,0 +1,131 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { findByPropsLazy } from "@webpack";
import { Button, Clipboard, Flex, Forms, Parser, Text, useEffect, useState } from "@webpack/common";
import { FriendInvite } from "./types";
const FormStyles = findByPropsLazy("header", "title", "emptyState");
const { createFriendInvite, getAllFriendInvites, revokeFriendInvites } = findByPropsLazy("createFriendInvite");
function CopyButton({ copyText, copiedText, onClick }) {
const [copied, setCopied] = useState(false);
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
setCopied(true);
setTimeout(() => setCopied(false), 1000);
onClick(e);
};
return (
<Button
onClick={handleButtonClick}
color={copied ? Button.Colors.GREEN : Button.Colors.BRAND}
size={Button.Sizes.SMALL}
look={Button.Looks.FILLED}
>
{copied ? copiedText : copyText}
</Button>
);
}
function FriendInviteCard({ invite }: { invite: FriendInvite; }) {
return (
<div className="vc-friend-codes-card">
<Flex justify={Flex.Justify.START}>
<div className="vc-friend-codes-card-title">
<Forms.FormTitle tag="h4" style={{ textTransform: "none" }}>
{invite.code}
</Forms.FormTitle>
<span>
Expires {Parser.parse(`<t:${new Date(invite.expires_at).getTime() / 1000}:R>`)} {invite.uses}/{invite.max_uses} uses
</span>
</div>
<Flex justify={Flex.Justify.END}>
<CopyButton
copyText="Copy"
copiedText="Copied!"
onClick={() => Clipboard.copy(`https://discord.gg/${invite.code}`)}
/>
</Flex>
</Flex>
</div>
);
}
export default function FriendCodesPanel() {
const [invites, setInvites] = useState<FriendInvite[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
getAllFriendInvites()
.then(setInvites)
.then(() => setLoading(false));
}, []);
return (
<>
<header className={FormStyles.header}>
<Forms.FormTitle
tag="h2"
className={FormStyles.title}
>
Your Friend Codes
</Forms.FormTitle>
<Flex
style={{ marginBottom: "16px" }}
justify={Flex.Justify.BETWEEN}
>
<h2 className="vc-friend-codes-info-header">{`Friend Codes - ${invites.length}`}</h2>
<Flex justify={Flex.Justify.END}>
<Button
color={Button.Colors.GREEN}
look={Button.Looks.FILLED}
onClick={() => createFriendInvite().then((invite: FriendInvite) => setInvites([...invites, invite]))}
>
Create Friend Code
</Button>
<Button
style={{ marginLeft: "8px" }}
color={Button.Colors.RED}
look={Button.Looks.OUTLINED}
disabled={!invites.length}
onClick={() => revokeFriendInvites().then(setInvites([]))}
>
Revoke all Friend Codes
</Button>
</Flex>
</Flex>
</header>
{loading ? (
<Text
variant="heading-md/semibold"
className="vc-friend-codes-text"
>
Loading...
</Text>
) : invites.length === 0 ? (
<Text
variant="heading-md/semibold"
className="vc-friend-codes-text"
>
You don't have any friend codes yet
</Text>
) : (
<div style={{ marginTop: "16px", display: "flex", flexWrap: "wrap", gap: "16px", justifyContent: "space-evenly" }}>
{invites.map(invite => (
<FriendInviteCard key={invite.code} invite={invite} />
))}
</div>
)}
</>
);
}

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import definePlugin from "@utils/types";
import FriendCodesPanel from "./FriendCodesPanel";
import { Devs } from "@utils/constants";
export default definePlugin({
name: "FriendCodes",
description: "Generate FriendCodes to easily add friends",
authors: [Devs.HypedDomi],
patches: [
{
find: "#{intl::ADD_FRIEND})}),(",
replacement: {
match: /\.Fragment[^]*?children:\[[^]*?}\)/,
replace: "$&,$self.FriendCodesPanel"
}
}
],
get FriendCodesPanel() {
return <FriendCodesPanel />;
}
});

View file

@ -0,0 +1,34 @@
.vc-friend-codes-card {
padding: 20px;
margin-bottom: var(--custom-margin-margin-small);
border-width: 1px;
border-style: solid;
border-radius: 5px;
border-color: var(--background-tertiary);
background-color: var(--background-secondary);
}
.vc-friend-codes-card-title span {
color: var(--header-secondary);
font-family: var(--font-primary);
font-size: 14px;
font-weight: 400;
}
.vc-friend-codes-info-header {
margin-top: 16px;
margin-bottom: 8px;
color: var(--header-secondary);
text-transform: uppercase;
font-size: 12px;
line-height: 16px;
letter-spacing: .02em;
font-family: var(--font-display);
font-weight: 600;
}
.vc-friend-codes-text {
display: flex;
justify-content: center;
align-items: center;
}

View file

@ -0,0 +1,26 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface FriendInvite {
channel: null;
code: string;
created_at: string;
expires_at: string;
inviter: {
avatar: string;
avatar_decoration_data: unknown;
clan: unknown;
discriminator: string;
global_name: string;
id: string;
public_flags: number;
username: string;
};
max_age: number;
max_uses: number;
type: number;
uses: number;
}

View file

@ -34,9 +34,9 @@ export default definePlugin({
getVoiceBackgroundStyles({ className, participantUserId }: any) {
if (!className.includes("tile_")) return;
if (!participantUserId) return;
const user = UserStore.getUser(participantUserId);
const avatarUrl = IconUtils.getUserAvatarURL(user, false, 1024);
if (Settings.plugins.USRBG.enabled && Settings.plugins.USRBG.voiceBackground) {

View file

@ -6,7 +6,7 @@
import "./style.css";
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
@ -317,15 +317,14 @@ export default definePlugin({
},
],
settings,
contextMenus: {
"message": addCollectionContextMenuPatch
},
start() {
refreshCacheCollection();
addContextMenuPatch("message", addCollectionContextMenuPatch);
GIF_COLLECTION_PREFIX = settings.store.collectionPrefix;
GIF_ITEM_PREFIX = settings.store.itemPrefix;
},
stop() {
removeContextMenuPatch("message", addCollectionContextMenuPatch);
},
get collections() {
refreshCacheCollection();
return this.sortedCollections();

View file

@ -40,7 +40,7 @@ interface BadgeCache {
let badgeImages;
// const API_URL = "https://clientmodbadges-api.herokuapp.com/";
const API_URL = "https://globalbadges.equicord.fyi/";
const API_URL = "https://globalbadges.equicord.org/";
const cache = new Map<string, BadgeCache>();
const EXPIRES = 1000 * 60 * 15;

View file

@ -1,14 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function EyeIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) {
return (
<svg className={className} height={height} width={width} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" >
<path fill="currentColor" d="M15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z" />
<path fill="currentColor" fillRule="evenodd" d="M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08Z" clipRule="evenodd" />
</svg>
);
}

View file

@ -1,14 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function HideIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) {
return (
<svg className={className} height={height} width={width} viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8.06358 19.4719C7.8226 19.7129 7.88837 20.119 8.20282 20.2504C9.31861 20.7166 10.5806 21 12 21C19.1 21 22.27 13.9 22.89 12.3C22.96 12.1 22.96 11.9 22.89 11.7C22.6279 11.0278 21.912 9.36931 20.6355 7.67842C20.4569 7.44184 20.1113 7.42419 19.9017 7.63379L19.1844 8.35115C19.0057 8.52986 18.9892 8.81332 19.139 9.01682C19.7777 9.88405 20.3225 10.8178 20.7633 11.8026C20.8195 11.9282 20.8197 12.0714 20.7631 12.1969C20.3546 13.104 19.5796 14.5725 18.39 15.92C16.87 17.62 14.8 19 12 19C11.0499 19 10.1838 18.8411 9.39472 18.5647C9.20611 18.4986 8.99473 18.5408 8.85341 18.6821L8.06358 19.4719ZM12.2958 15.2397C11.9993 15.5362 12.1829 16.0178 12.5975 15.9551C12.9163 15.907 13.2297 15.8202 13.5307 15.6955C14.2616 15.3928 14.8864 14.8801 15.3259 14.2223C15.7654 13.5645 16 12.7911 16 12C16 11.9755 15.9967 11.9523 15.9907 11.9305C15.9432 11.7599 15.7142 11.8214 15.589 11.9465L12.2958 15.2397ZM12.0655 8.00857C12.2382 8.05521 12.1766 8.28779 12.0502 8.41427L8.76028 11.7042C8.46375 12.0007 7.98224 11.8171 8.04488 11.4025C8.09303 11.0837 8.1798 10.7703 8.30448 10.4693C8.60723 9.73836 9.11992 9.11365 9.77772 8.67412C10.4355 8.2346 11.2089 8 12 8C12.023 8 12.0449 8.00299 12.0655 8.00857ZM15.1466 5.31789C15.0053 5.4592 14.7939 5.50137 14.6053 5.4353C13.8162 5.15891 12.9501 5 12 5C9.2 5 7.13 6.38 5.61 8.08C4.42045 9.42754 3.64546 10.896 3.23687 11.8031C3.18035 11.9286 3.18047 12.0718 3.23671 12.1974C3.67759 13.1822 4.22228 14.116 4.86096 14.9832C5.01083 15.1867 4.99433 15.4702 4.81562 15.6489L4.09827 16.3662C3.88867 16.5758 3.54305 16.5582 3.36446 16.3216C2.08807 14.6307 1.37209 12.9722 1.11 12.3C1.03518 12.107 1.03518 11.893 1.11 11.7C1.73 10.1 4.9 3 12 3C13.4194 3 14.6814 3.28337 15.7972 3.74961C16.1117 3.88101 16.1774 4.28705 15.9365 4.52802L15.1466 5.31789Z" fill="currentColor" />
<path d="M4 20L20 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
</svg>
);
}

View file

@ -1,20 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { cl, revealMessage } from "./";
import { HideIcon } from "./HideIcon";
export const HideMessageAccessory = ({ id }: { id: string; }) => {
return (
<span className={cl("accessory")}>
<HideIcon width={16} height={16} />
This message is hidden {" "}
<button onClick={() => revealMessage(id)} className={cl("reveal")}>
Reveal
</button>
</span>
);
};

View file

@ -1,166 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { get, set } from "@api/DataStore";
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Menu } from "@webpack/common";
import { EyeIcon } from "./EyeIcon";
import { HideIcon } from "./HideIcon";
import { HideMessageAccessory } from "./HideMessageAccessory";
let style: HTMLStyleElement;
const KEY = "HideMessage_hiddenMessages";
let hiddenMessages = new Map<string, {
id: string;
channel_id: string;
}>();
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, { message }) => {
const { deleted, id, channel_id } = message;
if (deleted || message.state !== "SENT") return;
const isHidden = hiddenMessages?.has(id) ?? false;
if (isHidden) {
return children.push(
<Menu.MenuItem
id={cl("reveal")}
label="Reveal Message"
icon={EyeIcon}
action={() => revealMessage(id)}
/>
);
}
children.push(<Menu.MenuItem
id={cl("hide")}
label="Hide Message"
color="danger"
icon={HideIcon}
action={() => {
hiddenMessages.set(id, { id, channel_id });
if (settings.store.saveHiddenMessages) set(KEY, hiddenMessages);
buildCss();
}}
/>);
};
const buildCss = () => {
const elements = [...hiddenMessages.values()].map(m => `#chat-messages-${m.channel_id}-${m.id}`).join(",");
style.textContent = settings.store.showNotice ? `
:is(${elements}):not(.messagelogger-deleted) > div {
position: relative;
background: var(--brand-experiment-05a);
}
:is(${elements}):not(.messagelogger-deleted) > div:hover {
background: var(--brand-experiment-10a);
}
:is(${elements}):not(.messagelogger-deleted) > div:before {
background: var(--brand-500);
content: "";
position: absolute;
display: block;
top: 0;
left: 0;
bottom: 0;
pointer-events: none;
width: 2px;
}
:is(${elements}) [id^='message-accessories'] > *:not(.vc-hide-message-accessory),
:is(${elements}) [id^='message-content']:not([class^='repliedTextContent']) > * {
display: none !important;
}
:is(${elements}) [id^='message-content']:empty {
display: block !important;
}
:is(${elements}) [class^='contents'] [id^='message-content']:after {
content: "Hidden content";
}
` : `
:is(${elements}) {
display: none !important;
}
`;
};
export const revealMessage = (id: string) => {
const isHidden = hiddenMessages?.has(id) ?? false;
if (isHidden) {
hiddenMessages.delete(id);
buildCss();
if (settings.store.saveHiddenMessages) set(KEY, hiddenMessages);
}
};
export const cl = classNameFactory("vc-hide-message-");
export const settings = definePluginSettings({
showNotice: {
type: OptionType.BOOLEAN,
description: "Shows a notice when a message is hidden",
default: true,
onChange: buildCss
},
saveHiddenMessages: {
type: OptionType.BOOLEAN,
description: "Persist restarts",
default: false,
onChange: async (value: boolean) => {
if (value) set(KEY, hiddenMessages);
else (hiddenMessages = await get(KEY) || hiddenMessages);
}
},
});
export default definePlugin({
name: "HideMessage",
description: "Adds a context menu option to hide messages",
authors: [EquicordDevs.Hanzy],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI"],
settings,
contextMenus: {
"message": patchMessageContextMenu
},
async start() {
style = document.createElement("style");
style.id = "VencordHideMessage";
document.head.appendChild(style);
if (settings.store.saveHiddenMessages) {
hiddenMessages = await get(KEY) || hiddenMessages;
buildCss();
}
addMessageAccessory("vc-hide-message", ({ message }) => {
const isHidden = hiddenMessages?.has(message.id) ?? false;
if (isHidden && settings.store.showNotice) return <HideMessageAccessory id={message.id} />;
return null;
});
},
async stop() {
for (const id of hiddenMessages.keys()) revealMessage(id);
removeMessageAccessory("vc-hide-message");
style.remove();
hiddenMessages.clear();
},
});

View file

@ -1,21 +0,0 @@
.vc-hide-message-accessory {
margin-top: 4px;
font-size: 12px;
font-weight: 400;
color: var(--text-muted);
}
.vc-hide-message-accessory svg {
margin-right: 4px;
vertical-align: text-bottom;
}
.vc-hide-message-reveal {
all: unset;
cursor: pointer;
color: var(--text-link);
}
.vc-hide-message-reveal:is(:hover, :focus) {
text-decoration: underline;
}

View file

@ -113,6 +113,7 @@ export default definePlugin({
useFilteredGuilds(guilds: guildsNode[]): guildsNode[] {
const hiddenGuilds = useStateFromStores([HiddenServersStore], () => HiddenServersStore.hiddenGuilds, undefined, (old, newer) => old.size === newer.size);
return guilds.flatMap(guild => {
if (!(hiddenGuilds instanceof Set)) return [guild];
if (hiddenGuilds.has(guild.id.toString())) {
return [];
}

View file

@ -0,0 +1,112 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeBlock } from "@components/CodeBlock";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { Button, FluxDispatcher, TooltipContainer, useCallback, useEffect, useState } from "@webpack/common";
import * as t from "@webpack/types";
import { IconsFinds } from "./names";
import { openRawModal } from "./rawModal";
import { openSaveModal } from "./saveModal";
import { ModalHeaderTitle } from "./subComponents";
import { _cssColors, cssColors, iconSizes } from "./utils";
const defaultColor = 209;
function ModalComponent(props: { iconName: string; Icon: t.Icon; } & ModalProps) {
const [color, SetColor] = useState(defaultColor);
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
e.preventDefault();
if (e.key === "ArrowLeft") {
SetColor(color + -1);
} else if (e.key === "ArrowRight") {
SetColor(color + 1);
}
}
}, [color]);
const onColorChange = useCallback((e: { type: string; color: string; }) => {
SetColor(_cssColors.indexOf(e.color));
}, [color]);
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
// @ts-ignore
FluxDispatcher.subscribe("ICONVIEWER_COLOR_CHANGE", onColorChange);
return () => {
document.removeEventListener("keydown", onKeyDown);
// @ts-ignore
FluxDispatcher.unsubscribe("ICONVIEWER_COLOR_CHANGE", onColorChange);
};
}, [onKeyDown]);
if (color < 0 || color >= cssColors.length) {
SetColor(0);
}
const { iconName, Icon } = props;
return (<ModalRoot {...props} size={ModalSize.DYNAMIC} className="vc-ic-modals-root vc-ic-icon-modal-root">
<ModalHeader>
<ModalHeaderTitle iconName={iconName} color={color} name="icon" />
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
{IconsFinds[iconName] ?
<div className="vc-icon-modal-codeblock">
<CodeBlock lang="ts" content={`const ${iconName + "Icon"} = findComponentByCode(${JSON.stringify(IconsFinds[iconName])})`} />
</div>
: null
}
<div className="vc-icon-modal-main-container">
<div className="vc-icon-display-box" aria-label={cssColors[color]?.name}>
<Icon className="vc-icon-modal-icon" color={cssColors[color]?.css} />
</div>
<div className="vc-icon-other-icon-sizes">
{iconSizes.map((size, idx) =>
<TooltipContainer text={`${size} size`} key={`vc-iv-size-${size}-${idx}`}>
<Icon className="vc-icon-modal-size-ex-icon" size={size} color={cssColors[color]?.css} style={{
marginLeft: "25px"
}} />
</TooltipContainer>
)}
</div>
</div>
</ModalContent>
<ModalFooter className="vc-ic-modals-footer">
<Button
color={Button.Colors.BRAND}
onClick={() => openSaveModal(iconName, Icon, color)}
>
Save as
</Button>
<Button
color={Button.Colors.YELLOW}
className={classes(Margins.right8, "vc-iv-raw-modal-button")}
onClick={() => openRawModal(iconName, Icon, color)}
>
Raw
</Button>
</ModalFooter>
</ModalRoot>);
}
export function openIconModal(iconName: string, Icon: t.Icon) {
openModal(props => <ModalComponent iconName={iconName} Icon={Icon} {...props} />);
}

View file

@ -0,0 +1,117 @@
.vc-icon-modal-codeblock {
margin-left: 10%;
margin-top: 30px;
}
.vc-icon-icon {
margin-left: 5%;
}
.vc-icon-modal-main-container {
display: flex;
}
.vc-ic-unordered-list li {
margin-left: 5%;
list-style: disc;
}
.vc-icon-other-icon-sizes {
height: 32px;
display: flex;
margin-top: 15%;
margin-left: 5%;
}
.vc-ic-icon-modal-root {
height: 450px;
width: 700px;
}
.vc-icons-tab-grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
gap: 8px;
}
.vc-icon-modal-size-ex-icon {
margin-right: 5%;
}
.vc-icon-modal-icon {
height: 164px;
width: 164px;
}
.vc-icon-tab-search-bar-grid {
display: grid;
height: 50px;
gap: 10px;
grid-template-columns: 1fr 10px;
}
.vc-icon-display-box {
height: 164px;
width: 164px;
margin-top: 5%;
margin-left: 15%;
background-image: repeating-linear-gradient(
45deg,
#ffffff1a 0,
#ffffff1a 10px,
#0000001a 10px,
#0000001a 20px
);
border-radius: 10px;
}
.vc-icon-container {
margin-top: 5px;
padding: 15px;
border-radius: 5px;
border: 3px solid transparent;
box-sizing: border-box;
}
.vc-icon-container:hover {
border-radius: 5px;
border: 3px solid var(--background-tertiary);
box-sizing: border-box;
}
.vc-icon-title {
font-size: 0.8em;
margin-top: 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vc-icon-modal-color-tooltip:hover {
background-color: var(--info-help-background);
}
.vc-save-modal {
margin-top: 10%;
display: grid;
grid-template-columns: auto 1fr;
gap: 10px;
}
.vc-save-select-option-1 {
margin-bottom: 5%;
}
.vc-save-select-option-2 {
margin-top: 5%;
}
.vc-ic-modals-root {
border-radius: 25px;
}
.vc-ic-modals-footer {
border-bottom-left-radius: 25px;
border-bottom-right-radius: 25px;
}

View file

@ -0,0 +1,88 @@
/*
* 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 "./IconsTab.css";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { Button, Clickable, Forms, React, TextInput, TooltipContainer } from "@webpack/common";
import * as t from "@webpack/types";
import { openIconModal } from "./IconModal";
import { getNameByIcon } from "./names";
import { findAllByCode, IconsDef } from "./utils";
export let Icons: IconsDef | null = null;
function searchMatch(search: string, name: string, Icon: t.Icon, searchbyFunction: boolean): boolean {
if (search === "") return true;
if (searchbyFunction) {
return String(Icon).includes(search);
}
const words = name.replace(/([A-Z]([a-z]+)?)/g, " $1").toLowerCase().split(" ");
const searchKeywords = search.toLowerCase().split(" ");
return searchKeywords.every(keyword => words.includes(keyword)) || words.every(keyword => searchKeywords.includes(keyword)) || name.toLowerCase().includes(search.toLowerCase());
}
function RenderIcons({ search, searchbyFunction }: { search: string; searchbyFunction: boolean; }) {
if (Icons === null) {
const OrgIcons = Array.from(new Set(findAllByCode("[\"size\",\"width\",\"height\",\"color\",\"colorClass\"]")));
Icons = Object.fromEntries(Object.keys(OrgIcons).map(k => [String(getNameByIcon(OrgIcons[k], k)), OrgIcons[k]])) as IconsDef;
}
return <div className="vc-icons-tab-grid-container">
{Object.entries(Icons).map(([iconName, Icon], index) =>
searchMatch(search, iconName, Icon, searchbyFunction) && <React.Fragment key={`iv-${iconName}`}>
<div className="vc-icon-box">
<Clickable onClick={() => openIconModal(iconName, Icon)}>
<div className="vc-icon-container">
<Icon className="vc-icon-icon" size="xxl" />
</div>
</Clickable>
<Forms.FormTitle className="vc-icon-title" tag="h3">{iconName}</Forms.FormTitle>
</div>
</React.Fragment>
)}</div>;
}
function IconsTab() {
const [search, setSearch] = React.useState<string>("");
const [searchByFunction, setSearchByFunction] = React.useState<boolean>(false);
const MemoRenderIcons = React.memo(RenderIcons);
return (
<SettingsTab title="Icons">
<div className={classes(Margins.top16, "vc-icon-tab-search-bar-grid")}>
<TextInput autoFocus value={search} placeholder="Search for an icon..." onChange={setSearch} />
<TooltipContainer text="Search by function context">
<Button
size={Button.Sizes.SMALL}
aria-label="Search by function context"
style={{ marginTop: "50%" }}
color={searchByFunction ? Button.Colors.GREEN : Button.Colors.PRIMARY}
onClick={() => setSearchByFunction(!searchByFunction)}
>Func</Button>
</TooltipContainer>
</div>
<MemoRenderIcons search={search} searchbyFunction={searchByFunction} />
</SettingsTab>
);
}
export default wrapTab(IconsTab, "IconsTab");

View file

@ -0,0 +1,54 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { SettingsRouter } from "@webpack/common";
import IconsTab from "./IconsTab";
import { SettingsAbout } from "./subComponents";
export default definePlugin({
name: "IconViewer",
description: "Adds a new tab to settings, to preview all icons",
authors: [EquicordDevs.iamme],
dependencies: ["Settings"],
startAt: StartAt.WebpackReady,
toolboxActions: {
"Open Icons Tab"() {
SettingsRouter.open("VencordDiscordIcons");
},
},
settingsAboutComponent: SettingsAbout,
start() {
const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as {
customSections: ((ID: Record<string, unknown>) => any)[];
}
).customSections;
const IconViewerSection = () => ({
section: "VencordDiscordIcons",
label: "Icons",
element: IconsTab,
className: "vc-discord-icons",
id: "IconViewer"
});
customSettingsSections.push(IconViewerSection);
},
stop() {
const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as {
customSections: ((ID: Record<string, unknown>) => any)[];
}
).customSections;
const i = customSettingsSections.findIndex(section => section({}).id === "IconViewer");
if (i !== -1) customSettingsSections.splice(i, 1);
},
});

View file

@ -0,0 +1,150 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as t from "@webpack/types";
// name: pattern
export const IconsFinds = {
Discord: "1.6 5.64-2.87",
XboxNeutral: "8.68-.62c.89-.81 1.5",
PlaystationNeutral: "2.04Zm-9.35",
TwitterNeutral: "M13.86 10.47", // even discord calls it twitter lmao
InstagramNeutral: "4.12.07Zm.1 2c-",
YoutubeNeutral: "11.5s0 3.95.5 5.85a3.",
FacebookNeutral: "2.8V12h2.8V9",
NintendoSwitchNeutral: "14V2.32c0",
Pencil: "0-2.82 0l-1.38 1.38a1",
AngleBrackets: "0-.4.8v1.98c0",
NitroWheel: "2h3a1 1 0 1 1 0 2H5.5",
Bill: "75.34.75.75C6 7.99 5 9",
Chat: "2.2 22H12Z\",",
ChatVoice: "22H12Zm2-5.26c0",
ChatX: ".23.46-.48.47L12 22H2.",
ChatSmile: "04-.61A10 10 0 1 1 22 ",
ChatRetry: "14.07.3.09.44.04a7",
ChatPlus: "1.24-.37V12a10 10 0 1 ",
Bug: "1.1.27.1.37 0a6.66 6.6",
B: "9V2.9c0-.5.4-.9.9-.9h7",
Eye: "19.1 21 12 21c-7.11 0-",
EyeSlash: "2.63-2.63c",
EyePlus: "3.32ZM19 14a1 ",
Heart: "0C9.43 20.48 1 15.09 1",
Star: ".73-2.25h6.12l1.9-5.83Z",
StarOutline: "3.1h5.26l1.62",
StarShooting: "1.35l2.95 2.14",
QrCode: "0v3ZM20",
Friends: "12h1a8",
PlusSmall: "0v-5h5a1",
CircleQuestion: "10.58l-3.3-3.3a1",
Pin: "1-.06-.63L6.16",
PinUpright: "5H8v4.35l-3.39",
PinUprightSlash: "1.56ZM11.08 ",
ArrowsLeftRight: "18.58V3a1",
XSmall: "13.42l5.3",
XLarge: "13.42l7.3 7.3Z",
XSmallBold: "12l4.94-4.94a1.5",
XLargeBold: "12l6.94-6.94a1.5",
Lock: "3Zm9-3v3H9V6a3",
LockUnlocked: "1-1.33-1.5ZM14",
Video: "1.45-.9V7.62a1",
VideoSlash: "1.4l20-20ZM9.2",
VideoLock: "1.32-.5V7.62a1",
Fire: "14Zm9.26-.84a.57.57",
Warning: "3.15H3.29c-1.74",
Download: "1.42l3.3 3.3V3a1",
Upload: "0ZM3 20a1 1",
// QuestionMark: "0ZM5.5 7a1.5" Unknown name
Quest: "10.47a.76.76",
Play: "4.96v14.08c0",
Emoji: " 0 0 0 0 22ZM6.5",
Gif: "3H5Zm2.18 13.8",
Trash: "2.81h8.36a3",
Bell: "9.5v2.09c0",
Screen: "0-3-3H5ZM13.5",
ScreenArrow: "3V5Zm16",
ScreenStream: " 2-2h3a2",
ScreenSystemRequirements: "3V5Zm3", // a guess
ScreenSlash: "5.8ZM17.15",
ScreenX: "1-3-3V5Zm6.3.3a1",
Plus: "0v8H3a1 1 0 1 0 0 2h8v8a1",
Id: "15h2.04V7.34H6V17Zm4",
Tv: "0-3-3H4ZM6 20a1",
Crown: "1.18l.82.82-3.61",
React: "04-4ZM16.96 4.08c",
Camera: "1.34 1.71 1.34H20a3",
Sticker: "1-.58.82l-4.24",
StageX: "13.07-1.38ZM16.7",
StageLock: "7.14-3.85ZM18.98",
Stage: "20.03c-.25.72.12",
ConnectionFine: "1 0 1 1-2 0A17 17 ",
ConnectionAverage: "\"M3 7a1 1 0 0",
ConnectionBad: "\"M2 13a1 1 0 0",
ConnectionUnknown: "15.86-.6.9-.2.02",
ChatWarning: ".54.5H2.2a1",
ChatCheck: "22H12c.22",
Hammer: "1.42ZM7.76",
StickerSmall: "1-.5.5H7a4",
StickerSad: "1.66-1.12 5.5",
StickerDeny: "\"M21.76 14.83a", // a guess
MagnifyingGlassPlus: "M11 7a1 1 0",
MagnifyingGlassMinus: "3v12H5.5a1.5 1.5",
// MagnifyingGlass: "???", // not quite possible
ChatArrowRight: "2.43l.06",
Bookmark: "1-1.67.74l",
ChannelList: "1-1-1ZM2 8a1",
ChannelListMagnifyingGlass: "2h18a1 1 0 1 0 0-2H3ZM2",
Activities: "1h3a3 3 0 0 0 3-3Z\"",
ActivitiesPlus: "14.35v1.29a",
AnnouncementsLock: "1-2.46-1.28 3.86",
AnnouncementsWarning: "1-2.46-1.28 3.85",
Announcements: ".42.27.79.62",
ShieldLock: "2.83v2.67a.5.5",
ShieldUser: "9.77V6.75c0-.57.17",
ShieldAt: "14.42-.35.75",
Shield: "M4.27 5.22A2.66", // a guess
Slash: "1-.43-.76L15.78",
SlashBox: "0-3-3H5Zm12.79",
Apps: "2.95H20a2 2 0",
CheckmarkLarge: "1.4l-12 12a1",
CheckmarkLargeBold: "2.12-2.12L9",
CheckmarkSmallBold: "13.88l6.94-6.94a1.5",
CheckmarkSmall: "1-1.4 0l-4-4a1",
DoubleCheckmark: "1.4l4.5 4.5a1",
NewUser: "0-.92h-.03a2", // a guess
UserCheck: "0l1.8-1.8c.17",
User: "2.9.06.24.26.",
UserMinus: "3-3h5.02c.38",
UserPlus: "2.07ZM12",
UserPlay: "0-3.61-.71h-.94Z",
UserBox: "0-3-3H5Zm10 6a3", // a guess
Settings: "0ZM16 12a4",
SettingsInfo: "10Zm1-4a1",
Hashtag: "8 4.84a1", // a guess
HashtagLocked: "2.02.31.03", // a guess
HashtagWarning: "8h1.26Z", // a guess
HashtagPlay: "52.88H9.85l", // a guess
Flag: "5.85v7.3a2",
Language: "5.43h3.85l",
Lightbulb: "8.5ZM15.1 19c.5",
Key: "23-.24ZM10 16a2",
InBox: "3H5ZM4 5.5C4",
BookmarkOutline: "0-1-1ZM7 2a3",
Food: "7.58V8a1 1"
};
// 13l4.91-8.05a1.8
export const namePatterns = new Map(Object.entries(IconsFinds).map(([name, pattern]) => [name, pattern]));
export function getNameByIcon(Icon: t.Icon, defaultName: any) {
for (const [name, pattern] of namePatterns) {
if (String(Icon).includes(pattern)) {
namePatterns.delete(name); // remove pattern from map after being found prevent overshadowing
return name;
}
}
return defaultName;
}

View file

@ -0,0 +1,70 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeBlock } from "@components/CodeBlock";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { Button, Toasts } from "@webpack/common";
import * as t from "@webpack/types";
import { ModalHeaderTitle } from "./subComponents";
function ModalComponent(props: { func: Function; iconName: string; color: number; } & ModalProps) {
const { func, iconName, color } = props;
return (<ModalRoot {...props} size={ModalSize.LARGE} className="vc-ic-modals-root vc-ic-raw-modal-root">
<ModalHeader>
<ModalHeaderTitle iconName={iconName} color={color} name="raw" />
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<div className="vc-iv-raw-modal">
<CodeBlock content={String(func)} lang="js" />
</div>
</ModalContent>
<ModalFooter className="vc-ic-modals-footer">
<Button
color={Button.Colors.PRIMARY}
className={"vc-iv-raw-modal-copy-button"}
onClick={() => {
// silly typescript
// @ts-ignore
Clipboard.copy(String(func));
Toasts.show({
id: Toasts.genId(),
message: `Copied raw \`${iconName}\` to clipboard`,
type: Toasts.Type.SUCCESS
});
}}
>
Copy
</Button>
<Button
color={Button.Colors.YELLOW}
className={classes(Margins.right8, "vc-iv-log-to-console-button")}
onClick={() => { console.log(func); }}
>
log to console
</Button>
</ModalFooter>
</ModalRoot>);
}
export function openRawModal(iconName: string, Icon: t.Icon, colorIndex: number) {
openModal(props => <ModalComponent iconName={iconName} func={Icon} color={colorIndex} {...props} />);
}

View file

@ -0,0 +1,153 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { Button, Forms, Select, TextInput, useCallback, useEffect, useState } from "@webpack/common";
import * as t from "@webpack/types";
import { ModalHeaderTitle } from "./subComponents";
import { convertComponentToHtml, cssColors, iconSizesInPx, saveIcon } from "./utils";
type IDivElement = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export function NumericComponent({ onChange, value, className, style }: { onChange: (value: number) => void, value: number; className?: string; style?: React.CSSProperties; }) {
const handleChange = (value: string) => {
const newValue = Number(value);
if (isNaN(newValue)) return;
onChange(newValue);
};
return (
<div className={className} style={style}>
<TextInput
type="number"
pattern="-?[0-9]+"
value={value}
placeholder="Enter a number"
onChange={handleChange}
/>
</div>
);
}
export function SelectComponent({ option, onChange, onError, className }: IDivElement & { option: any, onChange: (value: any) => void, onError: (msg: string | null) => void; className?: string; }) {
const [state, setState] = useState(option.options?.find(o => o.default)?.value ?? null);
const [error, setError] = useState<string | null>(null);
useEffect(() => onError(error), [error]);
const handleChange = (newValue: any) => {
const isValid = option.isValid?.call({}, newValue) ?? true;
if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
};
return (<div className={className}>
<Select
options={option.options}
placeholder={"Select an option"}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</div>);
}
function ModalComponent(props: { iconName: string, Icon: t.Icon; color: number; } & ModalProps) {
const [color, SetColor] = useState((props.color ?? 187));
const [iconSize, SetIconSize] = useState("lg");
const [saveType, SetSaveType] = useState("png");
const [customSize, SetCustomSize] = useState(32);
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
e.preventDefault();
if (e.key === "ArrowLeft") {
SetColor(color + -1);
} else if (e.key === "ArrowRight") {
SetColor(color + 1);
}
}
}, [color]);
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown]);
const { iconName, Icon } = props;
return (<ModalRoot {...props} size={ModalSize.MEDIUM} className="vc-ic-modals-root vc-ic-save-modal-root">
<ModalHeader>
<ModalHeaderTitle iconName={iconName} color={color} name="save" />
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<div className="vc-save-modal">
<div className="vc-icon-display-box vc-save-modal-icon-display-box" aria-label={cssColors[color]?.name} style={{ marginLeft: "0", marginTop: "0" }}>
<Icon className="vc-icon-modal-icon" color={cssColors[color].css} />
</div>
<div className="vc-save-options" style={{ marginTop: "0", marginLeft: "0" }}>
<SelectComponent className="vc-save-select-option-1"
option={{
options: [
{ "label": "large", "value": "lg", "default": true },
{ "label": "medium", "value": "md" },
{ "label": "small", "value": "sm" },
{ "label": "extra small", "value": "xs" },
{ "label": "extra extra small", "value": "xxs" },
{ "label": "custom", "value": "custom" }
]
}} onChange={newValue => SetIconSize(newValue)} onError={() => { }} />
<NumericComponent style={{ visibility: iconSize === "custom" ? "visible" : "hidden" }} value={customSize} onChange={(value: number) => SetCustomSize(value)} />
<SelectComponent className="vc-save-select-option-2"
option={{
options: [
{ "label": "png", "value": "image/png", "default": true },
{ "label": "jpeg", "value": "image/jpeg" },
{ "label": "gif", "value": "image/gif" },
{ "label": "avif", "value": "image/avif" },
{ "label": "webp", "value": "image/webp" },
{ "label": "svg", "value": "image/svg+xml" },
]
}} onChange={newValue => SetSaveType(newValue)} onError={() => { }} />
</div>
</div>
</ModalContent>
<ModalFooter className="vc-ic-modals-footer">
<Button
color={Button.Colors.BRAND}
onClick={() => saveIcon(iconName,
saveType === "image/svg+xml" || document.querySelector(".vc-icon-modal-icon") == null ?
convertComponentToHtml(<Icon className="vc-icon-modal-icon" color={cssColors[color].css} />) :
document.querySelector(".vc-icon-modal-icon") as Element,
color, iconSizesInPx[iconSize] ?? customSize, saveType)}
>
Save
</Button>
</ModalFooter>
</ModalRoot>);
}
export function openSaveModal(iconName: string, Icon: t.Icon, colorIndex: number) {
openModal(props => <ModalComponent iconName={iconName} Icon={Icon} color={colorIndex} {...props} />);
}

View file

@ -0,0 +1,118 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { Clickable, ContextMenuApi, FluxDispatcher, Forms, Menu, Text, TooltipContainer, useState } from "@webpack/common";
import type { ComponentPropsWithRef, PropsWithChildren } from "react";
import { _cssColors, cssColors } from "./utils";
function searchMatch(search: string, name: string): boolean {
if (search === "") return true;
const words = name.toLowerCase().split("_");
const searchKeywords = search.toLowerCase().split(" ").filter(keyword => keyword !== "");
return searchKeywords.every(keyword => words.includes(keyword)) || words.every(keyword => searchKeywords.includes(keyword)) || name.toLowerCase().includes(search.toLowerCase());
}
export type ClickableProps<T extends "a" | "div" | "span" | "li" = "div"> = PropsWithChildren<ComponentPropsWithRef<T>> & {
tag?: T;
};
export function IconTooltip({ children, copy, className, ...props }: ClickableProps & { children: string; copy: string; }) {
return <TooltipContainer text={"Click to copy"} className={className}>
<Clickable onClick={() => {
// @ts-ignore
Clipboard.copy(copy);
}} {...props}>{children}</Clickable>
</TooltipContainer>;
}
export const ModalHeaderTitle = ({ iconName, color, name }: { iconName: string; color: number; name: string; }) => {
return <Text variant="heading-lg/semibold"
style={{ flexGrow: 1, display: "flex" }}
className={classes("vc-ic-modal-header-title", `vc-ic-${name}-modal-header-title`)}>
<IconTooltip copy={iconName} className={classes(Margins.right8, "vc-icon-modal-color-tooltip")}>
{iconName}
</IconTooltip>
{" - "}
<IconTooltip copy={cssColors[color]?.css} className={classes(Margins.left8, "vc-icon-modal-color-tooltip")}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => {
const [query, setQuery] = useState("");
return (<Menu.Menu
navId="vc-ic-colors-header-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
color="danger"
aria-label="Icon Viewer Colors"
>
<Menu.MenuControlItem
id="vc-ic-colors-search"
control={(props, ref) => (
<Menu.MenuSearchControl
{...props}
query={query}
onChange={setQuery}
ref={ref}
placeholder={getIntlMessage("SEARCH")}
autoFocus
/>
)}
/>
{!!_cssColors.length && <Menu.MenuSeparator />}
{_cssColors.map(p => (
searchMatch(query, p) && <Menu.MenuItem
key={p}
id={p}
label={p}
action={() => {
// @ts-ignore
FluxDispatcher.dispatch({ type: "ICONVIEWER_COLOR_CHANGE", color: p });
}}
/>
))}
</Menu.Menu>);
});
}}>
{cssColors[color]?.name}
</IconTooltip>
</Text >;
};
export function SettingsAbout() {
return <>
<Forms.FormTitle tag="h3">Features</Forms.FormTitle>
<Forms.FormText>
<Text variant="heading-sm/normal">
<ul className="vc-ic-unordered-list">
<li>Preview icons</li>
<li>Copy icon names and CSS variables</li>
<li>Ability to download icons in different formats (SVG, PNG, GIF, etc.)</li>
<li>Copy pre-made icon finds for your plugins (Only some icons have this, submit finds either in a server or DMs)</li>
<li>Find icons by function context (helpful when creating finds)</li>
<li>Search for colors by right-clicking the color name in the modal title</li>
</ul>
</Text>
</Forms.FormText>
<Forms.FormTitle tag="h3">Special thanks</Forms.FormTitle>
<Forms.FormText>
<Text variant="heading-sm/normal" className="vc-ic-unordered-list">
<ul>
<li>krystalskullofficial._.</li>
<li>davr1</li>
<li>suffocate</li>
</ul>
</Text>
</Forms.FormText>
</>;
}

View file

@ -0,0 +1,100 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { saveFile } from "@utils/web";
import { filters, findAll, findByPropsLazy, waitFor } from "@webpack";
import { React, ReactDOM } from "@webpack/common";
import * as t from "@webpack/types";
export let _cssColors: string[] = [];
export type IconsDef = { [k: string]: t.Icon; };
export const iconSizesInPx = findByPropsLazy("md", "lg", "xxs");
export const Colors = findByPropsLazy("colors", "layout");
export const cssColors = new Proxy(
{
},
{
get: (target, key) => {
const colorKey = _cssColors[key];
return key in target
? target[key]
: Colors.colors[colorKey]?.css != null ? (target[key] = { name: colorKey.split("_").map((x: string) => x[0].toUpperCase() + x.toLowerCase().slice(1)).join(" "), css: Colors.colors[colorKey].css, key: colorKey }) : undefined;
},
set: (target, key, value) => {
target[key] = value;
return true;
}
}
) as unknown as Array<{ name: string; css: string; key: string; }>;
export const iconSizes = ["xxs", "xs", "sm", "md", "lg"];
const CrosspendingTypes: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpeg",
"image/gif": "gif",
"image/bmp": "bmp",
"image/tiff": "tiff",
"image/webp": "webp",
"image/svg+xml": "svg",
"image/avif": "avif"
};
export function saveIcon(iconName: string, icon: EventTarget & SVGSVGElement | Element | string, color: number, size: number, type = "image/png") {
const filename = `${iconName}-${cssColors[color]?.name ?? "unknown"}-${size}px.${CrosspendingTypes[type] ?? "png"}`;
if (typeof icon === "string") {
const file = new File([icon], filename, { type: "text/plain" });
saveFile(file);
return;
}
const innerElements = icon.children;
for (const el of innerElements) {
const fill = el.getAttribute("fill");
if (fill && fill.startsWith("var(")) {
el.setAttribute("fill", getComputedStyle(icon).getPropertyValue(fill.replace("var(", "").replace(")", "")));
}
}
// save svg as the given type
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, size, size);
const link = document.createElement("a");
link.download = filename;
link.href = canvas.toDataURL(type);
link.click();
};
img.src = `data:image/svg+xml;base64,${btoa(icon.outerHTML)}`;
}
export function convertComponentToHtml(component?: React.ReactElement): string {
const container = document.createElement("div");
const root = ReactDOM.createRoot(container);
ReactDOM.flushSync(() => root.render(component));
const content = container.innerHTML;
root.unmount();
return content;
}
export const findAllByCode = (code: string) => findAll(filters.byCode(code));
waitFor(["colors", "layout"], m => {
_cssColors = Object.keys(m.colors);
cssColors.length = _cssColors.length;
});

View file

@ -0,0 +1,97 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { ErrorBoundary } from "@components/index";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, Menu, React, UserStore } from "@webpack/common";
import { Channel } from "discord-types/general";
const ignoredChannelIds = new Set<string>();
const ContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { channel: Channel; }) => {
if (!channel || (!channel.isDM() && !channel.isGroupDM())) return;
const [checked, setChecked] = React.useState(ignoredChannelIds.has(channel.id));
children.push(
<Menu.MenuSeparator />,
<Menu.MenuCheckboxItem
id="ic-ignore-calls"
label="Ignore Calls"
checked={checked}
action={() => {
if (checked)
ignoredChannelIds.delete(channel.id);
else
ignoredChannelIds.add(channel.id);
setChecked(!checked);
}}
></Menu.MenuCheckboxItem>
);
};
const settings = definePluginSettings({
ignoreTimeout: {
type: OptionType.SLIDER,
description: "Timeout to click ignore",
markers: [0, 1000, 2000, 2500, 5000, 10000],
default: 2500,
stickToMarkers: false,
}
});
export default definePlugin({
name: "IgnoreCalls",
description: "Allows you to ignore calls from specific users or dm groups.",
authors: [EquicordDevs.TheArmagan],
patches: [
{
find: "#{intl::INCOMING_CALL_ELLIPSIS}",
replacement: {
match: /(?<=channel:(\i).{0,50}INCOMING_CALL_MODAL\).*?\}\)\]\}\))\]/,
replace: ",$self.renderIgnore($1)]"
}
}
],
settings,
flux: {
async CALL_UPDATE({ channelId, ringing, messageId, region }: { channelId: string; ringing: string[]; messageId: string; region: string; }) {
setTimeout(() => {
if (!ignoredChannelIds.has(channelId)) return;
const currentUserId = UserStore.getCurrentUser().id;
if (ringing.includes(currentUserId)) {
return FluxDispatcher.dispatch({
type: "CALL_UPDATE",
channelId,
ringing: ringing.filter((id: string) => id !== currentUserId),
messageId,
region
});
}
}, settings.store.ignoreTimeout);
}
},
renderIgnore(channel) {
const handeClick = () => {
ignoredChannelIds.add(channel.id);
};
return (
<ErrorBoundary>
<span onClick={handeClick} style={{ textAlign: "center", color: "var(--text-danger)" }}>Ignore</span>
</ErrorBoundary >
);
},
contextMenus: {
"user-context": ContextMenuPatch,
"gdm-context": ContextMenuPatch,
}
});

View file

@ -68,7 +68,7 @@ export default definePlugin({
replacement: {
match: /role:"tablist",.*?,?"aria-label":.+?\),children:(\[.*?\)\]}\)}\):null,)(.*?closePopout:\w.*?:null)/s,
replace: m => {
const stickerTabRegex = /(\w+?)\?(\([^()]+?\))\((.{1,2}),{.{0,128},isActive:(.{1,2})===.{1,6}\.STICKER.{1,140},children:(.{1,2}\.intl\.string\(.+?\)).*?:null/s;
const stickerTabRegex = /(\w+?)\?(\([^()]+?\))\((.{1,2}),{.{0,128},isActive:(.{1,2})===.{1,6}\.STICKER.{1,140},children:(.{1,5}\.string\(.+?\)).*?:null/s;
const res = m.replace(stickerTabRegex, (_m, canUseStickers, jsx, tabHeaderComp, currentTab, stickerText) => {
const isActive = `${currentTab}==="stickers+"`;
return (

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import "@equicordplugins/_misc/styles.css";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -27,7 +27,7 @@ export default definePlugin({
description: "Prevents in-call/PiP previews (screenshare, streams, etc) from pausing even if the client loses focus",
authors: [EquicordDevs.vappstar],
settingsAboutComponent: () => <>
<Forms.FormText className="platform-warning">
<Forms.FormText className="plugin-warning">
This plugin will cause discord to use more resources than normal
</Forms.FormText>
</>,

View file

@ -1,11 +0,0 @@
.platform-warning {
font-size: 16px !important;
background-color: rgb(240 71 71 / 10%);
color: rgb(240 71 71) !important;
border: 1px solid rgb(240 71 71 / 60%) !important;
border-radius: 5px !important;
font-weight: bold;
padding: 6px 10px;
text-align: center;
margin-top: 10px;
}

View file

@ -39,11 +39,13 @@ export default definePlugin({
authors: [EquicordDevs.thororen],
patches: [
{
find: "ProductCatalog",
replacement: {
match: /\i\.warn\("Cannot find the corresponding SKU to the user's premium type "\.concat\(\i\.premiumType\)\),/,
replace: ""
}
find: "#{intl::USER_PROFILE_ENTRY_POINTS_AMP_UP_YOUR_PROFILE}",
replacement: [
{
match: /}\);return \i\?.*?}\)}}/,
replace: "});return null}}"
}
],
}
],
start() {

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import "@equicordplugins/_misc/styles.css";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
@ -43,7 +43,7 @@ export default definePlugin({
description: "Spoof what platform or device you're on",
authors: [EquicordDevs.Drag],
settingsAboutComponent: () => <>
<Forms.FormText className="platform-warning">
<Forms.FormText className="plugin-warning">
We can't guarantee this plugin won't get you warned or banned.
</Forms.FormText>
</>,

View file

@ -1,11 +0,0 @@
.platform-warning {
font-size: 16px !important;
background-color: rgb(240 71 71 / 10%);
color: rgb(240 71 71) !important;
border: 1px solid rgb(240 71 71 / 60%) !important;
border-radius: 5px !important;
font-weight: bold;
padding: 6px 10px;
text-align: center;
margin-top: 10px;
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import "@equicordplugins/_misc/styles.css";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { Devs, EquicordDevs } from "@utils/constants";
@ -48,7 +48,7 @@ export default definePlugin({
description: "Purges messages from a channel",
authors: [EquicordDevs.bhop, Devs.nyx],
settingsAboutComponent: () => <>
<Forms.FormText className="purge-warning">
<Forms.FormText className="plugin-warning">
We can't guarantee this plugin won't get you warned or banned.
</Forms.FormText>
</>,

View file

@ -1,11 +0,0 @@
.purge-warning {
font-size: 16px !important;
background-color: rgb(240 71 71 / 10%) !important;
color: rgb(240 71 71) !important;
border: 1px solid rgb(240 71 71 / 60%) !important;
border-radius: 5px !important;
font-weight: bold;
padding: 6px 10px;
text-align: center;
margin-top: 10px;
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import "@equicordplugins/_misc/styles.css";
import { showNotification } from "@api/Notifications";
import ErrorBoundary from "@components/ErrorBoundary";
@ -229,7 +229,7 @@ export default definePlugin({
description: "A plugin to complete quests without having the game installed.",
authors: [Devs.amia],
settingsAboutComponent: () => <>
<Forms.FormText className="quests-warning">
<Forms.FormText className="plugin-warning">
Game Quests do not work on Equibop/Web Platforms. Only Video Quests do.
</Forms.FormText>
</>,
@ -237,8 +237,8 @@ export default definePlugin({
{
find: "\"invite-button\"",
replacement: {
match: /(\i\.Fragment,{children:)(\i\i)/,
replace: "$1[$self.renderQuestButton(),...$2]"
match: /\i&&(\i\i\.push).{0,50}"current-speaker"/,
replace: "$1($self.renderQuestButton()),$&"
}
},
{

View file

@ -195,7 +195,7 @@ function registerStyleChange(style) {
}
async function setIsUserCustomCapable() {
const allowList: string[] = await fetch("https://raw.githubusercontent.com/Equicord/Equibored/main/misc/quoterusers.json").then(e => e.json());
const allowList: string[] = await fetch("https://equicord.org/quoter").then(e => e.json()); // Override for memes - IF THIS IS ABUSED WILL WE TAKEN AWAY
isUserCustomCapable = allowList.includes(UserStore.getCurrentUser().id);
}
@ -225,7 +225,7 @@ function QuoteModal(props: ModalProps) {
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent scrollbarType="none">
<img src={""} id={"quoterPreview"} style={{ borderRadius: "20px", width: "100%" }}></img>
<img alt="" src="" id={"quoterPreview"} style={{ borderRadius: "20px", width: "100%" }}></img>
<br></br><br></br>
{isUserCustomCapable &&
(

View file

@ -0,0 +1,54 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { Button, Text } from "@webpack/common";
import { sendRemix } from ".";
import { brushCanvas, canvas, cropCanvas, ctx, exportImg, shapeCanvas } from "./editor/components/Canvas";
import { Editor } from "./editor/Editor";
import { resetBounds } from "./editor/tools/crop";
import { SendIcon } from "./icons/SendIcon";
type Props = {
modalProps: ModalProps;
close: () => void;
url?: string;
};
function reset() {
resetBounds();
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
brushCanvas.clearRect(0, 0, canvas.width, canvas.height);
shapeCanvas.clearRect(0, 0, canvas.width, canvas.height);
cropCanvas.clearRect(0, 0, canvas.width, canvas.height);
}
async function closeModal(closeFunc: () => void, save?: boolean) {
if (save) sendRemix(await exportImg());
reset();
closeFunc();
}
export default function RemixModal({ modalProps, close, url }: Props) {
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Remix</Text>
<ModalCloseButton onClick={() => closeModal(close)} />
</ModalHeader>
<ModalContent>
<Editor url={url} />
</ModalContent>
<ModalFooter className="vc-remix-modal-footer">
<Button onClick={() => closeModal(close, true)} className="vc-remix-send"><SendIcon /> Send</Button>
<Button onClick={() => closeModal(close)} color={Button.Colors.RED}>Close</Button>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -0,0 +1,44 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findComponentByCodeLazy } from "@webpack";
import { useEffect, useState } from "@webpack/common";
import { Canvas } from "./components/Canvas";
import { Toolbar } from "./components/Toolbar";
import { imageToBlob, urlToImage } from "./utils/canvas";
const FileUpload = findComponentByCodeLazy("fileUploadInput,");
export const Editor = (props: { url?: string; }) => {
const [file, setFile] = useState<File | undefined>(undefined);
useEffect(() => {
if (!props.url) return;
urlToImage(props.url).then(img => {
imageToBlob(img).then(blob => {
setFile(new File([blob], "remix.png"));
});
});
}, []);
return (
<div className="vc-remix-editor">
{!file && <FileUpload
filename={undefined}
placeholder="Choose an image"
buttonText="Browse"
filters={[{ name: "Image", extensions: ["png", "jpeg"] }]}
onFileSelect={(file: File) => setFile(file)}
/>}
{file && (<>
<Toolbar />
<Canvas file={file!} />
</>)}
</div>
);
};

View file

@ -0,0 +1,82 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useEffect, useRef } from "@webpack/common";
import { initInput } from "../input";
import { bounds } from "../tools/crop";
import { heightFromBounds, widthFromBounds } from "../utils/canvas";
export let canvas: HTMLCanvasElement | null = null;
export let ctx: CanvasRenderingContext2D | null = null;
export const brushCanvas = document.createElement("canvas")!.getContext("2d")!;
export const shapeCanvas = document.createElement("canvas")!.getContext("2d")!;
export const cropCanvas = document.createElement("canvas")!.getContext("2d")!;
export let image: HTMLImageElement;
export function exportImg(): Promise<Blob> {
return new Promise<Blob>(resolve => {
if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
ctx.drawImage(brushCanvas.canvas, 0, 0);
if (bounds.right === -1) bounds.right = canvas.width;
if (bounds.bottom === -1) bounds.bottom = canvas.height;
const renderCanvas = document.createElement("canvas");
renderCanvas.width = widthFromBounds(bounds);
renderCanvas.height = heightFromBounds(bounds);
const renderCtx = renderCanvas.getContext("2d")!;
renderCtx.drawImage(canvas, -bounds.left, -bounds.top);
renderCanvas.toBlob(blob => resolve(blob!));
render();
});
}
export const Canvas = ({ file }: { file: File; }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
image = new Image();
image.src = URL.createObjectURL(file);
image.onload = () => {
canvas = canvasRef.current;
if (!canvas) return;
canvas.width = image.width;
canvas.height = image.height;
brushCanvas.canvas.width = image.width;
brushCanvas.canvas.height = image.height;
shapeCanvas.canvas.width = image.width;
shapeCanvas.canvas.height = image.height;
cropCanvas.canvas.width = image.width;
cropCanvas.canvas.height = image.height;
ctx = canvas.getContext("2d")!;
ctx.drawImage(image, 0, 0);
initInput();
};
});
return (<canvas ref={canvasRef} className="vc-remix-canvas"></canvas>);
};
export function render() {
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
ctx.drawImage(brushCanvas.canvas, 0, 0);
ctx.drawImage(shapeCanvas.canvas, 0, 0);
ctx.drawImage(cropCanvas.canvas, 0, 0);
}

View file

@ -0,0 +1,52 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// brutally ripped out of usercss
// (remove when usercss is merged)
import "./colorStyles.css";
import { classNameFactory } from "@api/Styles";
import { findComponentByCodeLazy } from "@webpack";
import { Forms } from "@webpack/common";
interface ColorPickerProps {
color: number | null;
showEyeDropper?: boolean;
onChange(value: number | null): void;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".BACKGROUND_PRIMARY).hex");
const cl = classNameFactory("vc-remix-settings-color-");
interface Props {
name: string;
color: number;
onChange(value: string): void;
}
function hexToColorString(color: number): string {
return `#${color.toString(16).padStart(6, "0")}`;
}
export function SettingColorComponent({ name, onChange, color }: Props) {
function handleChange(newColor: number) {
onChange(hexToColorString(newColor));
}
return (
<Forms.FormSection>
<div className={cl("swatch-row")}>
<ColorPicker
key={name}
color={color}
onChange={handleChange}
/>
</div>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,141 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Switch } from "@components/Switch";
import { Button, Forms, Select, Slider, useEffect, useState } from "@webpack/common";
import { BrushTool } from "../tools/brush";
import { CropTool, resetBounds } from "../tools/crop";
import { EraseTool } from "../tools/eraser";
import { currentShape, setShape, setShapeFill, Shape, ShapeTool } from "../tools/shape";
import { brushCanvas, canvas, cropCanvas, render, shapeCanvas } from "./Canvas";
import { SettingColorComponent } from "./SettingColorComponent";
export type Tool = "none" | "brush" | "erase" | "crop" | "shape";
export type ToolDefinition = {
selected: () => void;
unselected: () => void;
[key: string]: any;
};
export const tools: Record<Tool, ToolDefinition | undefined> = {
none: undefined,
brush: BrushTool,
erase: EraseTool,
crop: CropTool,
shape: ShapeTool,
};
export let currentTool: Tool = "none";
export let currentColor = "#ff0000";
export let currentSize = 20;
export let currentFill = false;
function colorStringToHex(color: string): number {
return parseInt(color.replace("#", ""), 16);
}
export const Toolbar = () => {
const [tool, setTool] = useState<Tool>(currentTool);
const [color, setColor] = useState(currentColor);
const [size, setSize] = useState(currentSize);
const [fill, setFill] = useState(currentFill);
function changeTool(newTool: Tool) {
const oldTool = tool;
setTool(newTool);
onChangeTool(oldTool, newTool);
}
function onChangeTool(old: Tool, newTool: Tool) {
tools[old]?.unselected();
tools[newTool]?.selected();
}
useEffect(() => {
currentTool = tool;
currentColor = color;
currentSize = size;
currentFill = fill;
brushCanvas.fillStyle = color;
shapeCanvas.fillStyle = color;
brushCanvas.strokeStyle = color;
shapeCanvas.strokeStyle = color;
brushCanvas.lineWidth = size;
shapeCanvas.lineWidth = size;
brushCanvas.lineCap = "round";
brushCanvas.lineJoin = "round";
setShapeFill(currentFill);
}, [tool, color, size, fill]);
function clear() {
if (!canvas) return;
brushCanvas.clearRect(0, 0, canvas.width, canvas.height);
shapeCanvas.clearRect(0, 0, canvas.width, canvas.height);
resetBounds();
if (tool !== "crop") cropCanvas.clearRect(0, 0, canvas.width, canvas.height);
render();
}
return (
<div className="vc-remix-toolbar">
<div className="vc-remix-tools">
<Button className={(tool === "brush" ? "tool-active" : "")} onClick={() => changeTool("brush")}>Brush</Button>
<Button className={(tool === "erase" ? "tool-active" : "")} onClick={() => changeTool("erase")}>Erase</Button>
<Button className={(tool === "crop" ? "tool-active" : "")} onClick={() => changeTool("crop")}>Crop</Button>
<Button className={(tool === "shape" ? "tool-active" : "")} onClick={() => changeTool("shape")}>Shape</Button>
</div>
<div className="vc-remix-settings">
<div className="vc-remix-setting-section">
{(tool === "brush" || tool === "shape") &&
<SettingColorComponent name="vc-remix-color-picker" onChange={setColor} color={colorStringToHex(color)} />
}
{(tool === "brush" || tool === "erase" || tool === "shape") &&
<Slider
minValue={1}
maxValue={500}
initialValue={size}
onValueChange={setSize}
markers={[1, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500]}
hideBubble
/>
}
</div>
{(tool === "crop") && <Button onClick={resetBounds}>Reset</Button>}
<div className="vc-remix-setting-section">
{(tool === "shape") && (<>
<Select
select={setShape}
isSelected={v => v === currentShape}
serialize={v => String(v)}
placeholder="Shape"
options={
["Rectangle", "Ellipse", "Line", "Arrow"].map(v => ({
label: v,
value: v.toLowerCase() as Shape,
}))
}
/>
<Forms.FormText className="vc-remix-setting-switch">Fill <Switch checked={fill} onChange={setFill} /></Forms.FormText>
</>)}
</div>
</div>
<div className="vc-remix-misc">
<Button onClick={clear}>Clear</Button>
</div>
</div>
);
};

View file

@ -0,0 +1,19 @@
.vc-remix-settings-color-swatch-row {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.vc-remix-settings-color-swatch-row > span {
display: block;
flex: 1;
overflow: hidden;
margin-top: 0;
margin-bottom: 0;
color: var(--header-primary);
line-height: 24px;
font-size: 16px;
font-weight: 500;
word-wrap: break-word;
}

View file

@ -0,0 +1,57 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { canvas } from "./components/Canvas";
import { EventEmitter } from "./utils/eventEmitter";
export const Mouse = {
x: 0,
y: 0,
down: false,
dx: 0,
dy: 0,
prevX: 0,
prevY: 0,
event: new EventEmitter<MouseEvent>()
};
export function initInput() {
if (!canvas) return;
canvas.addEventListener("mousemove", e => {
Mouse.prevX = Mouse.x;
Mouse.prevY = Mouse.y;
const rect = canvas!.getBoundingClientRect();
const scaleX = canvas!.width / rect.width;
const scaleY = canvas!.height / rect.height;
Mouse.x = (e.clientX - rect.left) * scaleX;
Mouse.y = (e.clientY - rect.top) * scaleY;
Mouse.dx = Mouse.x - Mouse.prevX;
Mouse.dy = Mouse.y - Mouse.prevY;
Mouse.event.emit("move", e);
});
canvas.addEventListener("mousedown", e => {
Mouse.down = true;
Mouse.event.emit("down", e);
});
canvas.addEventListener("mouseup", e => {
Mouse.down = false;
Mouse.event.emit("up", e);
});
canvas.addEventListener("mouseleave", e => {
Mouse.down = false;
Mouse.event.emit("up", e);
});
}

View file

@ -0,0 +1,31 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { brushCanvas, ctx, render } from "../components/Canvas";
import { currentSize, ToolDefinition } from "../components/Toolbar";
import { Mouse } from "../input";
import { line } from "../utils/canvas";
export const BrushTool: ToolDefinition = {
onMouseMove() {
if (!Mouse.down || !ctx) return;
ctx.lineCap = "round";
ctx.lineJoin = "round";
brushCanvas.lineWidth = currentSize;
line(Mouse.prevX, Mouse.prevY, Mouse.x, Mouse.y);
render();
},
selected() {
Mouse.event.on("move", this.onMouseMove);
},
unselected() {
Mouse.event.off("move", this.onMouseMove);
},
};

View file

@ -0,0 +1,151 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { canvas, cropCanvas, render } from "../components/Canvas";
import { ToolDefinition } from "../components/Toolbar";
import { Mouse } from "../input";
import { dist, fillCircle } from "../utils/canvas";
export const bounds = {
top: 0,
left: 0,
right: -1,
bottom: -1,
};
export function resetBounds() {
if (!canvas) return;
bounds.top = 0;
bounds.left = 0;
bounds.right = canvas.width;
bounds.bottom = canvas.height;
CropTool.update();
}
export const CropTool: ToolDefinition = {
dragging: "",
onMouseMove() {
if (!canvas) return;
if (this.dragging !== "") {
if (this.dragging.includes("left")) bounds.left = Mouse.x;
if (this.dragging.includes("right")) bounds.right = Mouse.x;
if (this.dragging.includes("top")) bounds.top = Mouse.y;
if (this.dragging.includes("bottom")) bounds.bottom = Mouse.y;
this.update();
return;
}
if (dist(Mouse.x, Mouse.y, bounds.left, bounds.top) < 30) {
if (Mouse.down) {
bounds.left = Mouse.x;
bounds.top = Mouse.y;
this.dragging = "left top";
} else {
canvas.style.cursor = "nwse-resize";
}
}
else if (dist(Mouse.x, Mouse.y, bounds.right, bounds.top) < 30) {
if (Mouse.down) {
bounds.right = Mouse.x;
bounds.top = Mouse.y;
this.dragging = "right top";
} else {
canvas.style.cursor = "nesw-resize";
}
}
else if (dist(Mouse.x, Mouse.y, bounds.left, bounds.bottom) < 30) {
if (Mouse.down) {
bounds.left = Mouse.x;
bounds.bottom = Mouse.y;
this.dragging = "left bottom";
} else {
canvas.style.cursor = "nesw-resize";
}
}
else if (dist(Mouse.x, Mouse.y, bounds.right, bounds.bottom) < 30) {
if (Mouse.down) {
bounds.right = Mouse.x;
bounds.bottom = Mouse.y;
this.dragging = "right bottom";
} else {
canvas.style.cursor = "nwse-resize";
}
} else {
canvas.style.cursor = "default";
}
if (this.dragging !== "") this.update();
},
onMouseUp() {
this.dragging = "";
if (bounds.left > bounds.right) [bounds.left, bounds.right] = [bounds.right, bounds.left];
if (bounds.top > bounds.bottom) [bounds.top, bounds.bottom] = [bounds.bottom, bounds.top];
},
update() {
if (!canvas) return;
cropCanvas.clearRect(0, 0, canvas.width, canvas.height);
cropCanvas.fillStyle = "rgba(0, 0, 0, 0.75)";
cropCanvas.fillRect(0, 0, canvas.width, canvas.height);
cropCanvas.fillStyle = "rgba(0, 0, 0, 0.25)";
cropCanvas.clearRect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top);
cropCanvas.fillRect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top);
cropCanvas.fillStyle = "white";
cropCanvas.strokeStyle = "white";
cropCanvas.lineWidth = 3;
cropCanvas.strokeRect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top);
fillCircle(bounds.left, bounds.top, 10, cropCanvas);
fillCircle(bounds.right, bounds.top, 10, cropCanvas);
fillCircle(bounds.left, bounds.bottom, 10, cropCanvas);
fillCircle(bounds.right, bounds.bottom, 10, cropCanvas);
render();
},
onMouseMoveCallback: undefined,
onMouseUpCallback: undefined,
selected() {
if (!canvas) return;
if (bounds.right === -1) bounds.right = canvas.width;
if (bounds.bottom === -1) bounds.bottom = canvas.height;
this.update();
this.onMouseMoveCallback = this.onMouseMove.bind(this);
this.onMouseUpCallback = this.onMouseUp.bind(this);
Mouse.event.on("move", this.onMouseMoveCallback);
Mouse.event.on("up", this.onMouseUpCallback);
},
unselected() {
if (!canvas) return;
cropCanvas.clearRect(0, 0, canvas.width, canvas.height);
cropCanvas.fillStyle = "rgba(0, 0, 0, 0.75)";
cropCanvas.fillRect(0, 0, canvas.width, canvas.height);
cropCanvas.clearRect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top);
render();
Mouse.event.off("move", this.onMouseMoveCallback);
Mouse.event.off("up", this.onMouseUpCallback);
},
};

View file

@ -0,0 +1,36 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { brushCanvas, render } from "../components/Canvas";
import { currentSize, ToolDefinition } from "../components/Toolbar";
import { Mouse } from "../input";
export const EraseTool: ToolDefinition = {
onMouseMove() {
if (!Mouse.down) return;
brushCanvas.lineCap = "round";
brushCanvas.lineJoin = "round";
brushCanvas.lineWidth = currentSize;
brushCanvas.globalCompositeOperation = "destination-out";
brushCanvas.beginPath();
brushCanvas.moveTo(Mouse.prevX, Mouse.prevY);
brushCanvas.lineTo(Mouse.x, Mouse.y);
brushCanvas.stroke();
brushCanvas.globalCompositeOperation = "source-over";
render();
},
selected() {
Mouse.event.on("move", this.onMouseMove);
},
unselected() {
Mouse.event.off("move", this.onMouseMove);
},
};

View file

@ -0,0 +1,109 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { brushCanvas, render, shapeCanvas } from "../components/Canvas";
import { ToolDefinition } from "../components/Toolbar";
import { Mouse } from "../input";
import { line } from "../utils/canvas";
export type Shape = "rectangle" | "ellipse" | "line" | "arrow";
export let currentShape: Shape = "rectangle";
export function setShape(shape: Shape) {
currentShape = shape;
}
export let shapeFill = false;
export function setShapeFill(fill: boolean) {
shapeFill = fill;
}
export const ShapeTool: ToolDefinition = {
draggingFrom: { x: 0, y: 0 },
isDragging: false,
onMouseMove() {
if (!Mouse.down) return;
if (!this.isDragging) {
this.draggingFrom.x = Mouse.x;
this.draggingFrom.y = Mouse.y;
this.isDragging = true;
}
shapeCanvas.clearRect(0, 0, shapeCanvas.canvas.width, shapeCanvas.canvas.height);
this.draw();
},
onMouseUp() {
if (!this.isDragging) return;
shapeCanvas.clearRect(0, 0, shapeCanvas.canvas.width, shapeCanvas.canvas.height);
this.draw(brushCanvas);
this.isDragging = false;
},
onMouseMoveListener: null,
onMouseUpListener: null,
draw(canvas = shapeCanvas) {
canvas.lineCap = "butt";
canvas.lineJoin = "miter";
switch (currentShape) {
case "rectangle":
if (shapeFill) canvas.fillRect(this.draggingFrom.x, this.draggingFrom.y, Mouse.x - this.draggingFrom.x, Mouse.y - this.draggingFrom.y);
else canvas.strokeRect(this.draggingFrom.x, this.draggingFrom.y, Mouse.x - this.draggingFrom.x, Mouse.y - this.draggingFrom.y);
break;
case "ellipse":
const width = Mouse.x - this.draggingFrom.x;
const height = Mouse.y - this.draggingFrom.y;
const centerX = this.draggingFrom.x + width / 2;
const centerY = this.draggingFrom.y + height / 2;
const radiusX = Math.abs(width / 2);
const radiusY = Math.abs(height / 2);
canvas.beginPath();
canvas.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
if (shapeFill) canvas.fill();
else canvas.stroke();
break;
case "line":
line(this.draggingFrom.x, this.draggingFrom.y, Mouse.x, Mouse.y, canvas);
break;
case "arrow":
line(this.draggingFrom.x, this.draggingFrom.y, Mouse.x, Mouse.y, canvas);
// draw arrowhead (thanks copilot :3)
const angle = Math.atan2(Mouse.y - this.draggingFrom.y, Mouse.x - this.draggingFrom.x);
const arrowLength = 10;
canvas.beginPath();
canvas.moveTo(Mouse.x, Mouse.y);
canvas.lineTo(Mouse.x - arrowLength * Math.cos(angle - Math.PI / 6), Mouse.y - arrowLength * Math.sin(angle - Math.PI / 6));
canvas.lineTo(Mouse.x - arrowLength * Math.cos(angle + Math.PI / 6), Mouse.y - arrowLength * Math.sin(angle + Math.PI / 6));
canvas.closePath();
if (shapeFill) canvas.fill();
else canvas.stroke();
break;
}
render();
},
selected() {
this.onMouseMoveListener = this.onMouseMove.bind(this);
this.onMouseUpListener = this.onMouseUp.bind(this);
Mouse.event.on("move", this.onMouseMoveListener);
Mouse.event.on("up", this.onMouseUpListener);
},
unselected() {
shapeCanvas.clearRect(0, 0, shapeCanvas.canvas.width, shapeCanvas.canvas.height);
Mouse.event.off("move", this.onMouseMoveListener);
Mouse.event.off("up", this.onMouseUpListener);
},
};

View file

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { brushCanvas } from "../components/Canvas";
export function fillCircle(x: number, y: number, radius: number, canvas = brushCanvas) {
canvas.beginPath();
canvas.arc(x, y, radius, 0, Math.PI * 2);
canvas.fill();
}
export function strokeCircle(x: number, y: number, radius: number, canvas = brushCanvas) {
canvas.beginPath();
canvas.arc(x, y, radius, 0, Math.PI * 2);
canvas.stroke();
}
export function line(x1: number, y1: number, x2: number, y2: number, canvas = brushCanvas) {
canvas.beginPath();
canvas.moveTo(x1, y1);
canvas.lineTo(x2, y2);
canvas.stroke();
}
export function dist(x1: number, y1: number, x2: number, y2: number) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
export function widthFromBounds(bounds: { left: number, right: number, top: number, bottom: number; }) {
return bounds.right - bounds.left;
}
export function heightFromBounds(bounds: { left: number, right: number, top: number, bottom: number; }) {
return bounds.bottom - bounds.top;
}
export async function urlToImage(url: string) {
return new Promise<HTMLImageElement>(resolve => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.src = url;
});
}
export function imageToBlob(image: HTMLImageElement) {
return new Promise<File>(resolve => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
canvas.toBlob(blob => {
if (!blob) return;
resolve(new File([blob], "image.png", { type: "image/png" }));
});
});
}

View file

@ -0,0 +1,56 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export class EventEmitter<T> {
events: {
[key: string]: ((val: T) => void)[];
};
constructor() {
this.events = {};
}
on(eventName: string, callback: (val: T) => void) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName: string, val: T) {
if (!this.events[eventName]) {
return;
}
this.events[eventName].forEach(callback => {
callback(val);
});
}
off(eventName: string, callback: (val: T) => void) {
if (!this.events[eventName]) {
return;
}
this.events[eventName] = this.events[eventName].filter(cb => {
return cb !== callback;
});
}
clear() {
this.events = {};
}
once(eventName: string, callback: (val: T) => void) {
const onceCallback = (val: T) => {
callback(val);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}
}

View file

@ -0,0 +1,11 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const SendIcon = () => {
return (<svg className="sendIcon__461ff" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M6.6 10.02 14 11.4a.6.6 0 0 1 0 1.18L6.6 14l-2.94 5.87a1.48 1.48 0 0 0 1.99 1.98l17.03-8.52a1.48 1.48 0 0 0 0-2.64L5.65 2.16a1.48 1.48 0 0 0-1.99 1.98l2.94 5.88Z"></path>
</svg>);
};

View file

@ -0,0 +1,130 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { closeModal, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { extractAndLoadChunksLazy, findByPropsLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, Menu, MessageActions, RestAPI, showToast, SnowflakeUtils, Toasts } from "@webpack/common";
import RemixModal from "./RemixModal";
import css from "./styles.css?managed";
const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]);
const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);
const CloudUtils = findByPropsLazy("CloudUpload");
const PendingReplyStore = findStoreLazy("PendingReplyStore");
const validMediaTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
const UploadContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (children.find(c => c?.props?.id === "vc-remix")) return;
children.push(<Menu.MenuItem
id="vc-remix"
label="Remix"
action={() => {
const key = openModal(props =>
<RemixModal modalProps={props} close={() => closeModal(key)} />
);
}}
/>);
};
const MessageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
const url = props.itemHref ?? props.itemSrc;
if (!url) return;
if (props.attachment && !validMediaTypes.includes(props.attachment.content_type)) return;
const group = findGroupChildrenByChildId("copy-text", children);
if (!group) return;
if (group.find(c => c?.props?.id === "vc-remix")) return;
const index = group.findIndex(c => c?.props?.id === "copy-text");
group.splice(index + 1, 0, <Menu.MenuItem
id="vc-remix"
label="Remix"
action={() => {
const key = openModal(modalProps =>
<RemixModal modalProps={modalProps} close={() => closeModal(key)} url={url} />
);
}}
/>);
};
export function sendRemix(blob: Blob) {
const currentChannelId = getCurrentChannel()?.id;
const reply = PendingReplyStore.getPendingReply(currentChannelId);
if (reply) FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", currentChannelId });
const upload = new CloudUtils.CloudUpload({
file: new File([blob], "remix.png", { type: "image/png" }),
isClip: false,
isThumbnail: false,
platform: 1
}, currentChannelId, false, 0);
upload.on("complete", () => {
RestAPI.post({
url: `/channels/${currentChannelId}/messages`,
body: {
channel_id: currentChannelId,
content: "",
nonce: SnowflakeUtils.fromTimestamp(Date.now()),
sticker_ids: [],
attachments: [{
id: "0",
filename: upload.filename,
uploaded_filename: upload.uploadedFilename,
size: blob.size,
is_remix: settings.store.remixTag
}],
message_reference: reply ? MessageActions.getSendMessageOptionsForReply(reply)?.messageReference : null,
},
});
});
upload.on("error", () => showToast("Failed to upload remix", Toasts.Type.FAILURE));
upload.upload();
}
const settings = definePluginSettings({
remixTag: {
description: "Include the remix tag in remixed messages",
type: OptionType.BOOLEAN,
default: true,
},
});
export default definePlugin({
name: "Remix",
description: "Adds Remix to Desktop",
authors: [EquicordDevs.MrDiamond],
settings,
contextMenus: {
"channel-attach": UploadContextMenuPatch,
"message": MessageContextMenuPatch,
},
async start() {
await requireCreateStickerModal();
await requireSettingsMenu();
enableStyle(css);
},
stop() {
disableStyle(css);
},
});

View file

@ -0,0 +1,57 @@
.vc-remix-toolbar {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.vc-remix-tools,
.vc-remix-misc,
.vc-remix-settings {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 5px;
width: 100%;
background-color: var(--modal-footer-background);
padding: 10px 0;
border-radius: 8px;
}
.vc-remix-settings {
flex-direction: column;
}
.vc-remix-setting-section {
display: flex;
flex-direction: row;
justify-content: center;
width: 75%;
}
.vc-remix-toolbar button {
min-width: 100px;
height: 40px;
background-color: var(--brand);
color: var(--text-primary);
border-radius: 8px;
margin: 0 3px;
}
.vc-remix-canvas {
max-width: 100%;
max-height: 100%;
min-width: 50%;
min-height: 50%;
}
.vc-remix-editor {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 10px;
padding-bottom: 10px;
}

View file

@ -0,0 +1,132 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { Track } from "plugins/spotifyControls/SpotifyStore";
import { getLyricsLrclib } from "./providers/lrclibAPI";
import { getLyricsSpotify } from "./providers/SpotifyAPI";
import { LyricsData, Provider, SyncedLyric } from "./providers/types";
import settings from "./settings";
const LyricsCacheKey = "SpotifyLyricsCacheNew";
interface NullLyricCacheEntry {
[Provider.Lrclib]?: boolean;
[Provider.Spotify]?: boolean;
}
const nullLyricCache = new Map<string, NullLyricCacheEntry>();
export const lyricFetchers = {
[Provider.Spotify]: async (track: Track) => await getLyricsSpotify(track.id),
[Provider.Lrclib]: getLyricsLrclib,
};
export const providers = Object.keys(lyricFetchers) as Provider[];
export async function getLyrics(track: Track | null): Promise<LyricsData | null> {
if (!track) return null;
const cacheKey = track.id;
const cached = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
if (cached?.[cacheKey]) {
return cached[cacheKey];
}
const nullCacheEntry = nullLyricCache.get(cacheKey);
if (nullCacheEntry) {
const provider = settings.store.LyricsProvider;
if (!settings.store.FallbackProvider && nullCacheEntry[provider]) {
return null;
}
if (nullCacheEntry[Provider.Spotify] && nullCacheEntry[Provider.Lrclib]) {
return null;
}
}
const providersToTry = [settings.store.LyricsProvider, ...providers.filter(p => p !== settings.store.LyricsProvider)];
for (const provider of providersToTry) {
const lyricsInfo = await lyricFetchers[provider](track);
if (lyricsInfo) {
await DataStore.set(LyricsCacheKey, { ...cached, [cacheKey]: lyricsInfo });
return lyricsInfo;
}
const updatedNullCacheEntry = nullLyricCache.get(cacheKey) || {};
nullLyricCache.set(cacheKey, { ...updatedNullCacheEntry, [provider]: true });
}
return null;
}
export async function clearLyricsCache() {
nullLyricCache.clear();
await DataStore.set(LyricsCacheKey, {});
}
export async function getLyricsCount(): Promise<number> {
const cache = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
return Object.keys(cache).length;
}
export async function updateLyrics(trackId: string, newLyrics: SyncedLyric[], provider: Provider) {
const cache = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
const current = cache[trackId];
await DataStore.set(LyricsCacheKey,
{
...cache, [trackId]: {
...current,
useLyric: provider,
lyricsVersions: {
...current?.lyricsVersions,
[provider]: newLyrics
}
}
}
);
}
export async function removeTranslations() {
const cache = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
const newCache = {} as Record<string, LyricsData | null>;
for (const [trackId, trackData] of Object.entries(cache)) {
const { Translated, ...lyricsVersions } = trackData?.lyricsVersions || {};
const newUseLyric = !!lyricsVersions[Provider.Spotify] ? Provider.Spotify : Provider.Lrclib;
newCache[trackId] = { lyricsVersions, useLyric: newUseLyric };
}
await DataStore.set(LyricsCacheKey, newCache);
}
export async function migrateOldLyrics() {
const oldCache = await DataStore.get("SpotifyLyricsCache");
if (!oldCache || !Object.entries(oldCache).length) return;
const filteredCache = Object.entries(oldCache).filter(lrc => lrc[1]);
const result = {};
filteredCache.forEach(([trackId, lyrics]) => {
result[trackId] = {
lyricsVersions: {
// @ts-ignore
LRCLIB: lyrics.map(({ time, text }) => ({ time, text }))
},
useLyric: "LRCLIB"
};
});
await DataStore.set(LyricsCacheKey, result);
await DataStore.set("SpotifyLyricsCache", {});
}

View file

@ -0,0 +1,76 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { copyWithToast } from "@utils/misc";
import { findComponentByCodeLazy } from "@webpack";
import { FluxDispatcher, Menu } from "@webpack/common";
import { Provider } from "../providers/types";
import { useLyrics } from "./util";
const CopyIcon = findComponentByCodeLazy(" 1-.5.5H10a6");
const lyricsActualProviders = [Provider.Lrclib, Provider.Spotify];
const lyricsAlternative = [Provider.Translated, Provider.Romanized];
function ProviderMenuItem(toProvider: Provider, currentProvider?: Provider) {
return (
(!currentProvider || currentProvider !== toProvider) && (
<Menu.MenuItem
key={`switch-provider-${toProvider.toLowerCase()}`}
id={`switch-provider-${toProvider.toLowerCase()}`}
label={`Switch to ${toProvider}`}
action={() => {
FluxDispatcher.dispatch({
// @ts-ignore
type: "SPOTIFY_LYRICS_PROVIDER_CHANGE",
provider: toProvider,
});
}}
/>
)
);
}
export function LyricsContextMenu() {
const { lyricsInfo, currLrcIndex } = useLyrics();
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric];
const hasAShowingLyric = currLrcIndex !== null && currLrcIndex >= 0;
const hasLyrics = !!(lyricsInfo?.lyricsVersions[Provider.Lrclib] || lyricsInfo?.lyricsVersions[Provider.Spotify]);
return (
<Menu.Menu
navId="spotify-lyrics-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Spotify Lyrics Menu"
>
{hasAShowingLyric && (
<Menu.MenuItem
key="copy-lyric"
id="copy-lyric"
label="Copy lyric"
action={() => {
copyWithToast(currentLyrics![currLrcIndex].text!, "Lyric copied!");
}}
icon={CopyIcon}
/>
)}
<Menu.MenuItem
navId="spotify-lyrics-provider"
id="spotify-lyrics-provider"
label="Lyrics Provider"
>
{lyricsActualProviders.map(provider =>
ProviderMenuItem(provider, lyricsInfo?.useLyric)
)}
{hasLyrics && lyricsAlternative.map(provider =>
ProviderMenuItem(provider, lyricsInfo?.useLyric)
)}
</Menu.MenuItem>
</Menu.Menu>
);
}

View file

@ -0,0 +1,94 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openModal } from "@utils/modal";
import { ContextMenuApi, React, Text, TooltipContainer, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SpotifyLrcStore } from "../providers/store";
import settings from "../settings";
import { LyricsContextMenu } from "./ctxMenu";
import { LyricsModal } from "./modal";
import { cl, NoteSvg, useLyrics } from "./util";
function LyricsDisplay() {
const { ShowMusicNoteOnNoLyrics } = settings.use(["ShowMusicNoteOnNoLyrics"]);
const { lyricsInfo, lyricRefs, currLrcIndex } = useLyrics();
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null;
const NoteElement = NoteSvg(cl("music-note"));
const makeClassName = (index: number): string => {
if (currLrcIndex === null) return "";
const diff = index - currLrcIndex;
if (diff === 0) return cl("current");
return cl(diff > 0 ? "next" : "prev");
};
if (!lyricsInfo) {
return ShowMusicNoteOnNoLyrics && (
<div className="vc-spotify-lyrics"
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <LyricsContextMenu />)}
>
<TooltipContainer text="No synced lyrics found">
{NoteElement}
</TooltipContainer>
</div>
);
}
return (
<div
className="vc-spotify-lyrics"
onClick={() => openModal(props => <LyricsModal rootProps={props} />)}
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <LyricsContextMenu />)}
>
{currentLyrics?.map((line, i) => (
<div ref={lyricRefs[i]} key={i}>
<Text
variant={currLrcIndex === i ? "text-sm/normal" : "text-xs/normal"}
className={makeClassName(i)}
>
{line.text || NoteElement}
</Text>
</div>
))}
</div>
);
}
export function Lyrics() {
const track = useStateFromStores(
[SpotifyLrcStore],
() => SpotifyLrcStore.track,
null,
(prev, next) => (prev?.id ? prev.id === next?.id : prev?.name === next?.name)
);
const device = useStateFromStores(
[SpotifyLrcStore],
() => SpotifyLrcStore.device,
null,
(prev, next) => prev?.id === next?.id
);
const isPlaying = useStateFromStores([SpotifyLrcStore], () => SpotifyLrcStore.isPlaying);
const [shouldHide, setShouldHide] = useState(false);
useEffect(() => {
setShouldHide(false);
if (!isPlaying) {
const timeout = setTimeout(() => setShouldHide(true), 1000 * 60 * 5);
return () => clearTimeout(timeout);
}
}, [isPlaying]);
if (!track || !device?.is_active || shouldHide) return null;
return <LyricsDisplay />;
}

View file

@ -0,0 +1,78 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openImageModal } from "@utils/discord";
import { ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { React, Text } from "@webpack/common";
import { SpotifyStore, Track } from "plugins/spotifyControls/SpotifyStore";
import { cl, NoteSvg, scrollClasses, useLyrics } from "./util";
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
};
function ModalHeaderContent({ track }: { track: Track; }) {
return (
<ModalHeader>
<div className={cl("header-content")}>
{track?.album?.image?.url && (
<img
src={track.album.image.url}
alt={track.album.name}
className={cl("album-image")}
onClick={() => openImageModal({
url: track.album.image.url,
width: track.album.image.width,
height: track.album.image.height,
})}
/>
)}
<div>
<Text selectable variant="text-sm/semibold">{track.name}</Text>
<Text selectable variant="text-sm/normal">by {track.artists.map(a => a.name).join(", ")}</Text>
<Text selectable variant="text-sm/normal">on {track.album.name}</Text>
</div>
</div>
</ModalHeader>
);
}
export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) {
const { track, lyricsInfo, currLrcIndex } = useLyrics();
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null;
return (
<ModalRoot {...rootProps}>
<ModalHeaderContent track={track} />
<ModalContent>
<div className={cl("lyrics-modal-container") + ` ${scrollClasses.auto}`}>
{currentLyrics ? (
currentLyrics.map((line, i) => (
<Text
key={i}
variant={currLrcIndex === i ? "text-md/semibold" : "text-sm/normal"}
selectable
className={currLrcIndex === i ? cl("modal-line-current") : cl("modal-line")}
>
<span className={cl("modal-timestamp")} onClick={() => SpotifyStore.seek(line.time * 1000)}>
{formatTime(line.time)}
</span>
{line.text || NoteSvg(cl("modal-note"))}
</Text>
))
) : (
<Text variant="text-sm/normal" className={cl("modal-no-lyrics")}>
No lyrics available :(
</Text>
)}
</div>
</ModalContent>
</ModalRoot>
);
}

View file

@ -0,0 +1,92 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { findByPropsLazy } from "@webpack";
import { React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SpotifyLrcStore } from "../providers/store";
import { SyncedLyric } from "../providers/types";
import settings from "../settings";
export const scrollClasses = findByPropsLazy("auto", "customTheme");
export const cl = classNameFactory("vc-spotify-lyrics-");
export function NoteSvg(className: string) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 480 720" fill="currentColor" className={className} >
<path d="m160,-240 q -66,0 -113,-47 -47,-47 -47,-113 0,-66 47,-113 47,-47 113,-47 23,0 42.5,5.5 19.5,5.5 37.5,16.5 v -422 h 240 v 160 H 320 v 400 q 0,66 -47,113 -47,47 -113,47 z" />
</svg>
);
}
const calculateIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => {
const posInSec = position / 1000;
const currentIndex = lyrics.findIndex(l => l.time - (delay / 1000) > posInSec && l.time < posInSec + 8) - 1;
const nextLyric = lyrics.findIndex(l => l.time >= posInSec);
return [currentIndex, nextLyric];
};
export function useLyrics() {
const [track, storePosition, isPlaying, lyricsInfo] = useStateFromStores(
[SpotifyLrcStore],
() => [
SpotifyLrcStore.track!,
SpotifyLrcStore.mPosition,
SpotifyLrcStore.isPlaying,
SpotifyLrcStore.lyricsInfo
]
);
const { LyricDelay } = settings.use(["LyricDelay"]);
const [currLrcIndex, setCurrLrcIndex] = useState<number | null>(null);
const [nextLyric, setNextLyric] = useState<number | null>(null);
const [position, setPosition] = useState(storePosition);
const [lyricRefs, setLyricRefs] = useState<React.RefObject<HTMLDivElement | null>[]>([]);
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null;
useEffect(() => {
if (currentLyrics) {
setLyricRefs(currentLyrics.map(() => React.createRef()));
}
}, [currentLyrics]);
useEffect(() => {
if (currentLyrics && position) {
const [currentIndex, nextLyric] = calculateIndexes(currentLyrics, position, LyricDelay);
setCurrLrcIndex(currentIndex);
setNextLyric(nextLyric);
}
}, [currentLyrics, position]);
useEffect(() => {
if (currLrcIndex !== null) {
if (currLrcIndex >= 0) {
lyricRefs[currLrcIndex].current?.scrollIntoView({ behavior: "smooth", block: "center" });
}
if (currLrcIndex < 0 && nextLyric !== null) {
lyricRefs[nextLyric]?.current?.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}, [currLrcIndex, nextLyric]);
useEffect(() => {
if (isPlaying) {
setPosition(SpotifyLrcStore.position);
const interval = setInterval(() => {
setPosition(p => p + 1000);
}, 1000);
return () => clearInterval(interval);
}
}, [storePosition, isPlaying]);
return { track, lyricsInfo, lyricRefs, currLrcIndex, nextLyric };
}

View file

@ -0,0 +1,61 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Player } from "plugins/spotifyControls/PlayerComponent";
import { migrateOldLyrics } from "./api";
import { Lyrics } from "./components/lyrics";
import settings from "./settings";
export default definePlugin({
name: "SpotifyLyrics",
authors: [Devs.Joona],
description: "Adds lyrics to SpotifyControls",
dependencies: ["SpotifyControls"],
patches: [
{
find: "this.isCopiedStreakGodlike",
replacement: {
match: /Vencord.Plugins.plugins\["SpotifyControls"\].PanelWrapper/,
replace: "$self.FakePanelWrapper",
},
predicate: () => Settings.plugins.SpotifyControls.enabled,
noWarn: true,
},
],
FakePanelWrapper({ VencordOriginal, ...props }) {
const { LyricsPosition } = settings.use(["LyricsPosition"]);
return (
<>
<ErrorBoundary
fallback={() => (
<div className="vc-spotify-fallback">
<p>Failed to render Spotify Lyrics Modal :(</p>
<p>Check the console for errors</p>
</div>
)}
>
{LyricsPosition === "above" && <Lyrics />}
<Player />
{LyricsPosition === "below" && <Lyrics />}
</ErrorBoundary>
<VencordOriginal {...props} />
</>
);
},
settings,
async start() {
await migrateOldLyrics();
},
});

View file

@ -0,0 +1,49 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { LyricsData, Provider } from "../types";
interface LyricsAPIResp {
error: boolean;
syncType: string;
lines: Line[];
}
interface Line {
startTimeMs: string;
words: string;
syllables: any[];
endTimeMs: string;
}
export async function getLyricsSpotify(trackId: string): Promise<LyricsData | null> {
const resp = await fetch("https://spotify-lyrics-api-pi.vercel.app/?trackid=" + trackId);
if (!resp.ok) return null;
let data: LyricsAPIResp;
try {
data = await resp.json() as LyricsAPIResp;
} catch (e) {
return null;
}
const lyrics = data.lines;
if (lyrics[0].startTimeMs === "0" && lyrics[lyrics.length - 1].startTimeMs === "0") return null;
return {
useLyric: Provider.Spotify,
lyricsVersions: {
Spotify: lyrics.map(line => {
const trimmedText = line.words.trim();
return {
time: Number(line.startTimeMs) / 1000,
text: (trimmedText === "" || trimmedText === "♪") ? null : trimmedText
};
})
}
};
}

View file

@ -0,0 +1,67 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Track } from "plugins/spotifyControls/SpotifyStore";
import { LyricsData, Provider } from "../types";
const baseUrlLrclib = "https://lrclib.net/api/get";
interface LrcLibResponse {
id: number;
name: string;
trackName: string;
artistName: string;
albumName: string;
duration: number;
instrumental: boolean;
plainLyrics: string | null;
syncedLyrics: string | null;
}
function lyricTimeToSeconds(time: string) {
const [minutes, seconds] = time.slice(1, -1).split(":").map(Number);
return minutes * 60 + seconds;
}
export async function getLyricsLrclib(track: Track): Promise<LyricsData | null> {
const info = {
track_name: track.name,
artist_name: track.artists[0].name,
album_name: track.album.name,
duration: track.duration / 1000
};
const params = new URLSearchParams(info as any);
const url = `${baseUrlLrclib}?${params.toString()}`;
const response = await fetch(url, {
headers: {
"User-Agent": "https://github.com/Masterjoona/vc-spotifylyrics"
}
});
if (!response.ok) return null;
const data = await response.json() as LrcLibResponse;
if (!data.syncedLyrics) return null;
const lyrics = data.syncedLyrics;
const lines = lyrics.split("\n");
return {
useLyric: Provider.Lrclib,
lyricsVersions: {
LRCLIB: lines.map(line => {
const [lrcTime, text] = line.split("]");
const trimmedText = text.trim();
return {
time: lyricTimeToSeconds(lrcTime),
text: (trimmedText === "" || trimmedText === "♪") ? null : trimmedText
};
})
}
};
}

Some files were not shown because too many files have changed in this diff Show more