mirror of
https://github.com/Equicord/Equicord.git
synced 2025-03-30 12:11:58 -04:00
Merge branch 'Equicord:main' into main
This commit is contained in:
commit
32a99bfc99
147 changed files with 24320 additions and 960 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -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
|
||||
|
|
38
.github/workflows/codeberg-mirror.yml
vendored
38
.github/workflows/codeberg-mirror.yml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/reportBrokenPlugins.yml
vendored
2
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -17,7 +17,7 @@ on:
|
|||
jobs:
|
||||
TestPlugins:
|
||||
name: Test Patches
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
5
.github/workflows/syncDev.yml
vendored
5
.github/workflows/syncDev.yml
vendored
|
@ -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
|
||||
|
|
5
.github/workflows/syncMain.yml
vendored
5
.github/workflows/syncMain.yml
vendored
|
@ -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
|
||||
|
|
18
README.md
18
README.md
|
@ -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:
|
||||
|
|
148
misc/install.sh
148
misc/install.sh
|
@ -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
18735
misc/keys-mapping.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
110
pnpm-lock.yaml
generated
|
@ -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
|
||||
|
|
|
@ -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}`)]
|
||||
))),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -114,7 +114,7 @@ const DefaultSettings: Settings = {
|
|||
|
||||
cloud: {
|
||||
authenticated: false,
|
||||
url: "https://cloud.equicord.fyi/",
|
||||
url: "https://cloud.equicord.org/",
|
||||
settingsSync: false,
|
||||
settingsSyncVersion: 0
|
||||
},
|
||||
|
|
|
@ -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 />
|
||||
|
|
49
src/components/ThemeSettings/themesStyles.css
Normal file
49
src/components/ThemeSettings/themesStyles.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
{
|
|
@ -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;
|
|
@ -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"',
|
||||
|
|
90
src/equicordplugins/betterBlockedUsers/index.tsx
Normal file
90
src/equicordplugins/betterBlockedUsers/index.tsx
Normal 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[];
|
||||
}
|
||||
});
|
3
src/equicordplugins/betterBlockedUsers/styles.css
Normal file
3
src/equicordplugins/betterBlockedUsers/styles.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
[class*="usersList_"] [class*="text_"] {
|
||||
user-select: text !important;
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
114
src/equicordplugins/clipsEnhancements/index.tsx
Normal file
114
src/equicordplugins/clipsEnhancements/index.tsx
Normal 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;
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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},$&)"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -147,6 +147,5 @@ export default definePlugin({
|
|||
},
|
||||
stop() {
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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}
|
||||
|
|
119
src/equicordplugins/followVoiceUser/index.tsx
Normal file
119
src/equicordplugins/followVoiceUser/index.tsx
Normal 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
|
||||
}
|
||||
});
|
131
src/equicordplugins/friendCodes/FriendCodesPanel.tsx
Normal file
131
src/equicordplugins/friendCodes/FriendCodesPanel.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
29
src/equicordplugins/friendCodes/index.tsx
Normal file
29
src/equicordplugins/friendCodes/index.tsx
Normal 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 />;
|
||||
}
|
||||
});
|
34
src/equicordplugins/friendCodes/styles.css
Normal file
34
src/equicordplugins/friendCodes/styles.css
Normal 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;
|
||||
}
|
26
src/equicordplugins/friendCodes/types.ts
Normal file
26
src/equicordplugins/friendCodes/types.ts
Normal 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;
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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 [];
|
||||
}
|
||||
|
|
112
src/equicordplugins/iconViewer/IconModal.tsx
Normal file
112
src/equicordplugins/iconViewer/IconModal.tsx
Normal 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} />);
|
||||
}
|
||||
|
117
src/equicordplugins/iconViewer/IconsTab.css
Normal file
117
src/equicordplugins/iconViewer/IconsTab.css
Normal 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;
|
||||
}
|
88
src/equicordplugins/iconViewer/IconsTab.tsx
Normal file
88
src/equicordplugins/iconViewer/IconsTab.tsx
Normal 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");
|
54
src/equicordplugins/iconViewer/index.tsx
Normal file
54
src/equicordplugins/iconViewer/index.tsx
Normal 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);
|
||||
},
|
||||
});
|
150
src/equicordplugins/iconViewer/names.ts
Normal file
150
src/equicordplugins/iconViewer/names.ts
Normal 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;
|
||||
}
|
70
src/equicordplugins/iconViewer/rawModal.tsx
Normal file
70
src/equicordplugins/iconViewer/rawModal.tsx
Normal 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} />);
|
||||
}
|
||||
|
153
src/equicordplugins/iconViewer/saveModal.tsx
Normal file
153
src/equicordplugins/iconViewer/saveModal.tsx
Normal 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} />);
|
||||
}
|
||||
|
118
src/equicordplugins/iconViewer/subComponents.tsx
Normal file
118
src/equicordplugins/iconViewer/subComponents.tsx
Normal 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>
|
||||
</>;
|
||||
}
|
||||
|
100
src/equicordplugins/iconViewer/utils.tsx
Normal file
100
src/equicordplugins/iconViewer/utils.tsx
Normal 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;
|
||||
});
|
||||
|
97
src/equicordplugins/ignoreCalls/index.tsx
Normal file
97
src/equicordplugins/ignoreCalls/index.tsx
Normal 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,
|
||||
}
|
||||
});
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
</>,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
</>,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
</>,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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()),$&"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 &&
|
||||
(
|
||||
|
|
54
src/equicordplugins/remix/RemixModal.tsx
Normal file
54
src/equicordplugins/remix/RemixModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
44
src/equicordplugins/remix/editor/Editor.tsx
Normal file
44
src/equicordplugins/remix/editor/Editor.tsx
Normal 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>
|
||||
);
|
||||
};
|
82
src/equicordplugins/remix/editor/components/Canvas.tsx
Normal file
82
src/equicordplugins/remix/editor/components/Canvas.tsx
Normal 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);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
141
src/equicordplugins/remix/editor/components/Toolbar.tsx
Normal file
141
src/equicordplugins/remix/editor/components/Toolbar.tsx
Normal 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>
|
||||
);
|
||||
};
|
19
src/equicordplugins/remix/editor/components/colorStyles.css
Normal file
19
src/equicordplugins/remix/editor/components/colorStyles.css
Normal 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;
|
||||
}
|
57
src/equicordplugins/remix/editor/input.ts
Normal file
57
src/equicordplugins/remix/editor/input.ts
Normal 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);
|
||||
});
|
||||
}
|
31
src/equicordplugins/remix/editor/tools/brush.ts
Normal file
31
src/equicordplugins/remix/editor/tools/brush.ts
Normal 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);
|
||||
},
|
||||
};
|
151
src/equicordplugins/remix/editor/tools/crop.ts
Normal file
151
src/equicordplugins/remix/editor/tools/crop.ts
Normal 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);
|
||||
},
|
||||
};
|
36
src/equicordplugins/remix/editor/tools/eraser.ts
Normal file
36
src/equicordplugins/remix/editor/tools/eraser.ts
Normal 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);
|
||||
},
|
||||
};
|
109
src/equicordplugins/remix/editor/tools/shape.ts
Normal file
109
src/equicordplugins/remix/editor/tools/shape.ts
Normal 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);
|
||||
},
|
||||
};
|
63
src/equicordplugins/remix/editor/utils/canvas.ts
Normal file
63
src/equicordplugins/remix/editor/utils/canvas.ts
Normal 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" }));
|
||||
});
|
||||
});
|
||||
}
|
56
src/equicordplugins/remix/editor/utils/eventEmitter.ts
Normal file
56
src/equicordplugins/remix/editor/utils/eventEmitter.ts
Normal 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);
|
||||
}
|
||||
}
|
11
src/equicordplugins/remix/icons/SendIcon.tsx
Normal file
11
src/equicordplugins/remix/icons/SendIcon.tsx
Normal 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>);
|
||||
};
|
130
src/equicordplugins/remix/index.tsx
Normal file
130
src/equicordplugins/remix/index.tsx
Normal 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);
|
||||
},
|
||||
});
|
57
src/equicordplugins/remix/styles.css
Normal file
57
src/equicordplugins/remix/styles.css
Normal 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;
|
||||
}
|
132
src/equicordplugins/spotifyLyrics/api.tsx
Normal file
132
src/equicordplugins/spotifyLyrics/api.tsx
Normal 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", {});
|
||||
}
|
76
src/equicordplugins/spotifyLyrics/components/ctxMenu.tsx
Normal file
76
src/equicordplugins/spotifyLyrics/components/ctxMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
94
src/equicordplugins/spotifyLyrics/components/lyrics.tsx
Normal file
94
src/equicordplugins/spotifyLyrics/components/lyrics.tsx
Normal 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 />;
|
||||
}
|
78
src/equicordplugins/spotifyLyrics/components/modal.tsx
Normal file
78
src/equicordplugins/spotifyLyrics/components/modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
92
src/equicordplugins/spotifyLyrics/components/util.tsx
Normal file
92
src/equicordplugins/spotifyLyrics/components/util.tsx
Normal 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 };
|
||||
}
|
61
src/equicordplugins/spotifyLyrics/index.tsx
Normal file
61
src/equicordplugins/spotifyLyrics/index.tsx
Normal 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();
|
||||
},
|
||||
});
|
|
@ -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
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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
Loading…
Add table
Reference in a new issue