mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-07 13:43:03 -04:00
feat(quoter): add auto-send quote feature and update settings
feat(spotifyLyrics): remove plugin and associated files feat(woof): remove Woof plugin and related files fix(extensions): update User-Agent string for extension installation fix(supportHelper): update support links to new repository chore(betterforwardmeta): add initial plugin files and settings chore(moreUserTags): add user tags plugin with various roles and settings feat(forceRoleColor): implement role color customization for users
This commit is contained in:
parent
db7447a93d
commit
b5fd49a41b
63 changed files with 880 additions and 5570 deletions
|
@ -58,7 +58,7 @@ window.VencordNative = {
|
|||
},
|
||||
|
||||
updater: {
|
||||
getRepo: async () => ({ ok: true, value: "https://github.com/Equicord/Equicord" }),
|
||||
getRepo: async () => ({ ok: true, value: "https://github.com/Rayanzay/ryncord" }),
|
||||
getUpdates: async () => ({ ok: true, value: [] }),
|
||||
update: async () => ({ ok: true, value: false }),
|
||||
rebuild: async () => ({ ok: true, value: true }),
|
||||
|
|
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.2.0
|
||||
version: 4.2.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
version: 4.2.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
'@types/chrome':
|
||||
specifier: ^0.0.312
|
||||
version: 0.0.312
|
||||
|
@ -112,22 +112,22 @@ importers:
|
|||
version: 0.25.1
|
||||
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.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)))
|
||||
version: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(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.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
|
||||
version: 4.1.4(@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
|
||||
highlight.js:
|
||||
specifier: 11.11.1
|
||||
version: 11.11.1
|
||||
|
@ -169,7 +169,7 @@ importers:
|
|||
version: 5.8.2
|
||||
typescript-eslint:
|
||||
specifier: ^8.28.0
|
||||
version: 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
version: 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
typescript-transform-paths:
|
||||
specifier: ^3.5.5
|
||||
version: 3.5.5(typescript@5.8.2)
|
||||
|
@ -2665,9 +2665,9 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.5.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))':
|
||||
'@eslint-community/eslint-utils@4.5.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': {}
|
||||
|
@ -2791,10 +2791,10 @@ snapshots:
|
|||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@stylistic/eslint-plugin@4.2.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)':
|
||||
'@stylistic/eslint-plugin@4.2.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
|
||||
eslint-visitor-keys: 4.2.0
|
||||
espree: 10.3.0
|
||||
estraverse: 5.3.0
|
||||
|
@ -2839,7 +2839,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 22.13.13
|
||||
|
||||
'@types/less@3.0.6(patch_hash=krcufrsfhsuxuoj7hocqugs6zi)': {}
|
||||
'@types/less@3.0.6(patch_hash=641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076)': {}
|
||||
|
||||
'@types/lodash@4.17.15': {}
|
||||
|
||||
|
@ -2901,15 +2901,15 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 22.13.13
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)':
|
||||
'@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
'@typescript-eslint/scope-manager': 8.28.0
|
||||
'@typescript-eslint/type-utils': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/type-utils': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
'@typescript-eslint/visitor-keys': 8.28.0
|
||||
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
|
||||
|
@ -2918,14 +2918,14 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)':
|
||||
'@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.28.0
|
||||
'@typescript-eslint/types': 8.28.0
|
||||
'@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
|
||||
'@typescript-eslint/visitor-keys': 8.28.0
|
||||
debug: 4.4.0
|
||||
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
|
||||
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -2935,12 +2935,12 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.28.0
|
||||
'@typescript-eslint/visitor-keys': 8.28.0
|
||||
|
||||
'@typescript-eslint/type-utils@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)':
|
||||
'@typescript-eslint/type-utils@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
debug: 4.4.0
|
||||
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
|
||||
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
|
||||
ts-api-utils: 2.1.0(typescript@5.8.2)
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
|
@ -2962,13 +2962,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)':
|
||||
'@typescript-eslint/utils@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.5.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
|
||||
'@eslint-community/eslint-utils': 4.5.1(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
|
||||
'@typescript-eslint/scope-manager': 8.28.0
|
||||
'@typescript-eslint/types': 8.28.0
|
||||
'@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
|
||||
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
|
||||
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -3564,9 +3564,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.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))):
|
||||
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))):
|
||||
dependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
|
@ -3576,17 +3576,17 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
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.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
|
@ -3595,9 +3595,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.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(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
|
||||
|
@ -3609,13 +3609,13 @@ snapshots:
|
|||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
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
|
||||
|
@ -3623,7 +3623,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
|
||||
|
@ -3637,19 +3637,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.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)):
|
||||
eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(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.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
|
||||
eslint-scope@8.3.0:
|
||||
dependencies:
|
||||
|
@ -3660,9 +3660,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.5.1(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
|
||||
'@eslint-community/eslint-utils': 4.5.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
|
||||
|
@ -5028,12 +5028,12 @@ snapshots:
|
|||
|
||||
typed-query-selector@2.12.0: {}
|
||||
|
||||
typescript-eslint@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2):
|
||||
typescript-eslint@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2))(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.8.2)
|
||||
eslint: 9.20.1(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
|
||||
'@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2))(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
'@typescript-eslint/parser': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
'@typescript-eslint/utils': 8.28.0(eslint@9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215))(typescript@5.8.2)
|
||||
eslint: 9.20.1(patch_hash=4f22e92770bf528b2448fbec0984b9c0761dd588ed0e83dcc41edfc9af711215)
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
|
@ -62,7 +62,7 @@ async function ensureBinary() {
|
|||
|
||||
const res = await fetch(BASE_URL + filename, {
|
||||
headers: {
|
||||
"User-Agent": "Equicord (https://github.com/Equicord/Equicord)",
|
||||
"User-Agent": "Equicord (https://github.com/Rayanzay/ryncord)",
|
||||
"If-None-Match": etag
|
||||
}
|
||||
});
|
||||
|
|
|
@ -60,7 +60,7 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||
}, [user.id, user.username]);
|
||||
|
||||
const ContributedHyperLink = <Link href="https://github.com/Equicord/Equicord">contributed</Link>;
|
||||
const ContributedHyperLink = <Link href="https://github.com/Vendicated/Vencord">contributed</Link>;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -36,8 +36,8 @@ export const settings = definePluginSettings({
|
|||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "Anammox",
|
||||
description: "A microbial process that plays an important part in the nitrogen cycle",
|
||||
name: "RemoveCrap",
|
||||
description: "Remove Discord's bloatware from your client.",
|
||||
authors: [Devs.Kyuuhachi],
|
||||
settings,
|
||||
|
||||
|
|
6
src/equicordplugins/betterforwardmeta/.prettierrc.json
Normal file
6
src/equicordplugins/betterforwardmeta/.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
0
src/equicordplugins/betterforwardmeta/LICENSE
Normal file
0
src/equicordplugins/betterforwardmeta/LICENSE
Normal file
99
src/equicordplugins/betterforwardmeta/index.tsx
Normal file
99
src/equicordplugins/betterforwardmeta/index.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore, DateUtils, GuildStore, IconUtils, NavigationRouter, Popout, SelectedGuildStore, SnowflakeUtils, Text, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { ArrowSvg, checkForIconExistence, cl, ServerProfileComponent } from "./utils";
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterForwardMeta",
|
||||
description: "Access server profile under forwarded messages (if available) and always show time",
|
||||
authors: [Devs.nin0dev],
|
||||
ForwardFooter(message: any) {
|
||||
const { guild_id, channel_id, message_id } = message.message.messageReference;
|
||||
const guild = useStateFromStores([GuildStore], () => GuildStore.getGuild(guild_id));
|
||||
const channel = useStateFromStores([ChannelStore], () => ChannelStore.getChannel(channel_id));
|
||||
|
||||
return <div className={cl("footer")} >
|
||||
{
|
||||
guild_id && <>
|
||||
{
|
||||
guild_id !== SelectedGuildStore.getGuildId() && <Popout
|
||||
position="top"
|
||||
renderPopout={() => <ServerProfileComponent guildId={guild_id} />}
|
||||
>
|
||||
{popoutProps => <div className={cl("footer-element")} {...popoutProps}>
|
||||
{
|
||||
checkForIconExistence(guild) && <img src={guild.icon && IconUtils.getGuildIconURL({
|
||||
id: guild.id,
|
||||
icon: guild.icon,
|
||||
canAnimate: true,
|
||||
size: 32
|
||||
})} alt={`Server icon for ${guild.name}`} className={cl("guild-icon")} />
|
||||
}
|
||||
<Text variant="text-sm/medium" className={cl("footer-text")} style={{
|
||||
marginLeft: checkForIconExistence(guild) ? "20px" : "0"
|
||||
}}>{guild ? guild.name : "View server"} </Text>
|
||||
<ArrowSvg />
|
||||
</div>
|
||||
}
|
||||
</Popout>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
channel && <div className={cl("footer-element")} onClick={() => NavigationRouter.transitionTo(`/channels/${guild_id ?? "@me"}/${channel_id}/${message_id}`)} >
|
||||
<Text variant="text-sm/medium" className={cl("footer-text")}>{(() => {
|
||||
/*
|
||||
- Text channel
|
||||
- Voice channel
|
||||
- Announcement channel
|
||||
- Stage channel
|
||||
- Forum channel
|
||||
- Media channel
|
||||
*/
|
||||
if ([0, 2, 5, 13, 15, 16].includes(channel.type)) return `#${channel.name}`;
|
||||
// DMs
|
||||
if (channel.type === 1) return `@${(() => {
|
||||
const user = UserStore.getUser(channel.recipients[0]);
|
||||
// @ts-expect-error
|
||||
return user.globalName || user.username;
|
||||
})()}`;
|
||||
// GDMs
|
||||
if (channel.type === 3) return channel.name || (() => {
|
||||
const users = channel.recipients.map(r => UserStore.getUser(r));
|
||||
// @ts-expect-error
|
||||
return users.map(u => u.globalName || u.username).join(", ");
|
||||
})();
|
||||
// Threads
|
||||
if ([10, 11, 12].includes(channel.type)) return channel.name;
|
||||
})()}</Text>
|
||||
<ArrowSvg />
|
||||
</div>
|
||||
}
|
||||
<div className={cl("footer-element")} style={{
|
||||
pointerEvents: "none"
|
||||
}}>
|
||||
<Text variant="text-sm/medium" className={cl("footer-text")}>
|
||||
{DateUtils.calendarFormat(new Date(SnowflakeUtils.extractTimestamp(message_id)))}
|
||||
</Text>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
find: "originLabel,\" • \"",
|
||||
replacement: {
|
||||
match: /(let{message:\i,snapshot:\i,index:\i}=(\i))(.{0,400})return .+TEXT_LOW_CONTRAST}\)]}\)/,
|
||||
replace: "$1$3return $self.ForwardFooter($2)"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
47
src/equicordplugins/betterforwardmeta/style.css
Normal file
47
src/equicordplugins/betterforwardmeta/style.css
Normal file
|
@ -0,0 +1,47 @@
|
|||
.vc-serverprofileforward-footer {
|
||||
margin-top: 3px;
|
||||
|
||||
svg {
|
||||
width: 12px !important;
|
||||
}
|
||||
|
||||
.vc-serverprofileforward-footer-text {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: var(--text-low-contrast);
|
||||
}
|
||||
|
||||
.vc-serverprofileforward-guild-icon {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.vc-serverprofileforward-footer-element {
|
||||
&:hover {
|
||||
.vc-serverprofileforward-footer-text {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
svg path {
|
||||
fill: var(--interactive-hover) !important;
|
||||
}
|
||||
|
||||
background-color: var(--background-modifier-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
display: inline-flex;
|
||||
padding: 1px 0;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
20
src/equicordplugins/betterforwardmeta/utils.tsx
Normal file
20
src/equicordplugins/betterforwardmeta/utils.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Guild } from "discord-types/general";
|
||||
|
||||
export const ServerProfileComponent = findComponentByCodeLazy("{guildProfile:", "GUILD_PROFILE");
|
||||
export const cl = classNameFactory("vc-serverprofileforward-");
|
||||
|
||||
export const ArrowSvg = () => <svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="var(--text-low-contrast)" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z" className=""></path></svg>;
|
||||
|
||||
export const checkForIconExistence = (guild: Guild) => {
|
||||
if (!guild) return false;
|
||||
if (!guild.icon) return false;
|
||||
return true;
|
||||
};
|
|
@ -1,150 +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 { classNameFactory } from "@api/Styles";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, Forms, Switch, Text } from "@webpack/common";
|
||||
|
||||
// definitely not stolen from glide :P
|
||||
async function injectCSS() {
|
||||
var elementToRemove = document.getElementById("DemonstrationStyle");
|
||||
if (elementToRemove) {
|
||||
elementToRemove.remove();
|
||||
}
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.id = "DemonstrationStyle";
|
||||
const content = await fetch("https://minidiscordthemes.github.io/Demonstration/Demonstration.theme.css").then(e => e.text());
|
||||
styleElement.textContent = content;
|
||||
document.documentElement.appendChild(styleElement);
|
||||
}
|
||||
|
||||
const validKeycodes = [
|
||||
"Backspace", "Tab", "Enter", "ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "Pause", "CapsLock",
|
||||
"Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "PrintScreen", "Insert",
|
||||
"Delete", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "KeyA", "KeyB", "KeyC",
|
||||
"KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT",
|
||||
"KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ", "MetaLeft", "MetaRight", "ContextMenu", "Numpad0", "Numpad1", "Numpad2", "Numpad3",
|
||||
"Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "NumpadMultiply", "NumpadAdd", "NumpadSubtract", "NumpadDecimal",
|
||||
"NumpadDivide", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "NumLock", "ScrollLock"
|
||||
];
|
||||
|
||||
const settings = definePluginSettings({
|
||||
keyBind: {
|
||||
description: "The key to toggle the theme when pressed",
|
||||
type: OptionType.STRING,
|
||||
default: "F6",
|
||||
isValid: (value: string) => {
|
||||
if (validKeycodes.includes(value)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
soundVolume: {
|
||||
description: "How loud the toggle sound is (0 to disable)",
|
||||
type: OptionType.SLIDER,
|
||||
default: 0.5,
|
||||
markers: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
|
||||
},
|
||||
showConfirmationModal: {
|
||||
description: "Show modal to remind shortcut",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
}
|
||||
});
|
||||
|
||||
function ToggleModal() {
|
||||
const value = !settings.use(["showConfirmationModal"]).showConfirmationModal;
|
||||
return (
|
||||
<Switch
|
||||
note="You can re-enable this setting later"
|
||||
value={value}
|
||||
onChange={v => { settings.store.showConfirmationModal = !v; }}>
|
||||
Disable modal?
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function handleToggle() {
|
||||
const style = document.getElementById("DemonstrationStyle");
|
||||
if (style != null) {
|
||||
style.remove();
|
||||
playSound("https://github.com/Equicord/Equibored/raw/main/sounds/demonstration/wp5rpz.wav");
|
||||
}
|
||||
else {
|
||||
if (settings.store.showConfirmationModal) {
|
||||
const cl = classNameFactory("vc-demonstration-modal");
|
||||
|
||||
const key = openModal(props => (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader className={cl("header")}>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }} >Demonstration</Text>
|
||||
<ModalCloseButton onClick={() => closeModal(key)}></ModalCloseButton>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("content")}>
|
||||
<Forms.FormText>
|
||||
This will censor all text! To disable this, remember the shortcut:
|
||||
</Forms.FormText>
|
||||
<Text variant="heading-xl/bold" style={{ textAlign: "center", width: "100%", paddingTop: "20px", paddingBottom: "20px" }}>
|
||||
{settings.store.keyBind}
|
||||
</Text>
|
||||
<ToggleModal />
|
||||
</ModalContent>
|
||||
<Button
|
||||
onClick={() => {
|
||||
closeModal(key);
|
||||
injectCSS();
|
||||
playSound("https://github.com/Equicord/Equibored/raw/main/sounds/demonstration/ckz46t.wav");
|
||||
}}
|
||||
>Okay!</Button>
|
||||
</ModalRoot>
|
||||
));
|
||||
} else {
|
||||
injectCSS();
|
||||
playSound("https://github.com/Equicord/Equibored/raw/main/sounds/demonstration/ckz46t.wav");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.code !== settings.store.keyBind) { return; }
|
||||
handleToggle();
|
||||
}
|
||||
|
||||
async function playSound(url: string) {
|
||||
const audio = new Audio(url);
|
||||
audio.volume = settings.store.soundVolume;
|
||||
await audio.play().catch(error => {
|
||||
console.error("Error playing sound:", error);
|
||||
});
|
||||
audio.remove();
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Demonstration",
|
||||
description: "Plugin for taking theme screenshots - censors identifying images and text.",
|
||||
authors: [Devs.Samwich, EquicordDevs.Panniku],
|
||||
settings,
|
||||
toolboxActions: {
|
||||
"Toggle Demonstration": (() => handleToggle())
|
||||
},
|
||||
settingsAboutComponent: () => {
|
||||
return (
|
||||
<>
|
||||
<Text>To change your keycode, check out <a href="https://www.toptal.com/developers/keycode" target="_blank" rel="noreferrer noopener">this tool</a>!</Text>
|
||||
</>
|
||||
);
|
||||
},
|
||||
start() {
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
},
|
||||
stop() {
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
}
|
||||
});
|
179
src/equicordplugins/forceRoleColor/index.tsx
Normal file
179
src/equicordplugins/forceRoleColor/index.tsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Text } from "@webpack/common";
|
||||
import { Common } from "webpack";
|
||||
|
||||
type Author = {
|
||||
nick: string;
|
||||
colorString: string;
|
||||
colorStrings: {
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
tertiaryColor?: string;
|
||||
};
|
||||
colorRoleName: string;
|
||||
guildId?: string;
|
||||
};
|
||||
|
||||
const settings = definePluginSettings({
|
||||
hint: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: function () {
|
||||
return <Text>
|
||||
<b>Colors must be in hex (in the format of #XXXXXX) i.e. #ff0000, #123456, etc.</b><br /><br />
|
||||
Primary: Controls base color<br />
|
||||
Secondary & Tertiary: Set for gradient
|
||||
<br /><b>Switch channels for the color to update</b>
|
||||
<div style={{
|
||||
margin: "2em 0",
|
||||
padding: "1em",
|
||||
backgroundColor: "#e7828430",
|
||||
border: "1px solid #e78284",
|
||||
borderRadius: "5px",
|
||||
color: "var(--text-normal, white)"
|
||||
}} className="markup__75297">
|
||||
Gradient role colors require the experiment <code className="inline" style={{ whiteSpace: "nowrap" }}>2025-03_enhanced_role_colors</code> to be enabled!
|
||||
</div>
|
||||
</Text>;
|
||||
}
|
||||
},
|
||||
primaryColor: {
|
||||
type: OptionType.STRING,
|
||||
description: "",
|
||||
default: undefined,
|
||||
placeholder: "#000000"
|
||||
},
|
||||
secondaryColor: {
|
||||
type: OptionType.STRING,
|
||||
description: "",
|
||||
default: undefined,
|
||||
placeholder: "#000000"
|
||||
},
|
||||
tertiaryColor: {
|
||||
type: OptionType.STRING,
|
||||
description: "",
|
||||
default: undefined,
|
||||
placeholder: "#000000"
|
||||
},
|
||||
dmsOnly: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Applies your color only in DMs",
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "ForceRoleColor",
|
||||
description: "Forces a specific role color on yourself globally",
|
||||
authors: [Devs.surgedevs],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: ".tertiaryColor,roleStyle:\"username\",includeConvenienceGlow:!0",
|
||||
replacement: [
|
||||
// Override message author role color
|
||||
{
|
||||
match: /(?<=let{author:\i,message:)(\i)(.*?)(?<=colorStrings:\i,colorRoleName:\i}=)(\i)/,
|
||||
replace: "$1$2$self.getColorsForMessages($1,$3)"
|
||||
},
|
||||
// Always enable gradient roles
|
||||
{
|
||||
match: /\(0,\i\.\i\)\(null!=\i\?\i:\i,"BaseUsername"\)/,
|
||||
replace: "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: ".name,roleColors:",
|
||||
replacement: [
|
||||
// Override member list role color
|
||||
{
|
||||
match: /(?<=let{colorRoleName.*?colorString:(\i).*?roleColorStrings:(\i).*?user:(\i).*?}=\i;)/,
|
||||
replace: "let{colorString:_$1,roleColorStrings:_$2}=$self.getColorsForMemberList($3,$1,$2);$1=_$1;$2=_$2;"
|
||||
}
|
||||
]
|
||||
},
|
||||
// @TODO: find a better `find` here ??? not sure how stable this is lmao
|
||||
{
|
||||
find: ".showThreadPromptOnReply&&",
|
||||
replacement: [
|
||||
// Override reply role color, uses getColorsForMessage since the keys are the same
|
||||
{
|
||||
match: /(?<=message:(\i).*?colorString:(\i).*?,(\i)=\(0,\i\.\i\)\(\i,\i\)),/,
|
||||
replace: ";let{colorString:_$2,colorStrings:_$3}=$self.getColorsForMessages($1,{colorString:$2,colorStrings:$3});$2=_$2;$3=_$3;let "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "memberNameText}),(0,",
|
||||
replacement: [
|
||||
// Override color in guild member search
|
||||
{
|
||||
match: /(?<=let{member:(\i),user:(\i).*(\i)=\(0,.*?colorStrings\);)/,
|
||||
replace: "$1=$self.getColorsForMemberSearch($2,$1);$3=$1.colorStrings;"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
getColorsForMessages(message: any, old: Author): Author {
|
||||
if (
|
||||
message.author.id !== Common.UserStore.getCurrentUser().id
|
||||
|| (settings.store.dmsOnly && old.guildId)
|
||||
) {
|
||||
return old;
|
||||
}
|
||||
|
||||
return {
|
||||
...old,
|
||||
colorString: settings.store.primaryColor || old?.colorString,
|
||||
colorStrings: {
|
||||
primaryColor: settings.store.primaryColor || old?.colorStrings?.primaryColor,
|
||||
secondaryColor: settings.store.secondaryColor || old?.colorStrings?.secondaryColor,
|
||||
tertiaryColor: settings.store.tertiaryColor || old?.colorStrings?.tertiaryColor,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getColorsForMemberList(user: any, colorString: string, old: any) {
|
||||
if (user.id !== Common.UserStore.getCurrentUser().id || settings.store.dmsOnly) {
|
||||
return {
|
||||
colorString,
|
||||
roleColorStrings: old
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
colorString: settings.store.primaryColor || colorString,
|
||||
roleColorStrings: {
|
||||
primaryColor: settings.store.primaryColor || old?.primaryColor,
|
||||
secondaryColor: settings.store.secondaryColor || old?.secondaryColor,
|
||||
tertiaryColor: settings.store.tertiaryColor || old?.tertiaryColor,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getColorsForMemberSearch(user: any, old: any) {
|
||||
// can just call getColorsForMessages since keys are the same
|
||||
return this.getColorsForMessages({ author: user }, old);
|
||||
}
|
||||
});
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* 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 { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Menu, MessageActions, MessageStore, NavigationRouter, Toasts, UserStore } from "@webpack/common";
|
||||
|
||||
async function findLastMessageFromUser(channelId: string, userId: string) {
|
||||
try {
|
||||
const messageCollection = MessageStore.getMessages(channelId);
|
||||
let messages = messageCollection?.toArray() || [];
|
||||
let userMessage = messages.filter(m => m?.author?.id === userId).pop();
|
||||
if (userMessage) return userMessage.id;
|
||||
try {
|
||||
await MessageActions.fetchMessages({
|
||||
channelId: channelId,
|
||||
limit: 50
|
||||
});
|
||||
|
||||
const updatedCollection = MessageStore.getMessages(channelId);
|
||||
messages = updatedCollection?.toArray() || [];
|
||||
userMessage = messages.filter(m => m?.author?.id === userId).pop();
|
||||
|
||||
if (userMessage) return userMessage.id;
|
||||
} catch (fetchError) {
|
||||
console.error("Error fetching messages:", fetchError);
|
||||
}
|
||||
|
||||
Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Couldn't find any recent messages from this user.",
|
||||
id: Toasts.genId()
|
||||
});
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error finding last message:", error);
|
||||
Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to find messages. Check console for details.",
|
||||
id: Toasts.genId()
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function jumpToLastActive(channel: any, targetUserId?: string) {
|
||||
try {
|
||||
if (!channel) {
|
||||
Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Channel information not available.",
|
||||
id: Toasts.genId()
|
||||
});
|
||||
return;
|
||||
}
|
||||
const guildId = channel.guild_id !== null ? channel.guild_id : "@me";
|
||||
const channelId = channel.id;
|
||||
let userId: string;
|
||||
if (targetUserId) {
|
||||
|
||||
userId = targetUserId;
|
||||
} else {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
userId = currentUser.id;
|
||||
}
|
||||
const messageId = await findLastMessageFromUser(channelId, userId);
|
||||
if (messageId) {
|
||||
const url = `/channels/${guildId}/${channelId}/${messageId}`;
|
||||
NavigationRouter.transitionTo(url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in jumpToLastActive:", error);
|
||||
Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to jump to message. Check console for details.",
|
||||
id: Toasts.genId()
|
||||
});
|
||||
}
|
||||
}
|
||||
const ChannelContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }) => {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="LastActive"
|
||||
label={<span style={{ color: "#aa6746" }}>Your Last Message</span>}
|
||||
icon={LastActiveIcon}
|
||||
action={() => {
|
||||
jumpToLastActive(channel);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user, channel }) => {
|
||||
if (!channel || !user?.id) return;
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="LastActive"
|
||||
label={<span style={{ color: "#aa6746" }}>User's Last Message</span>}
|
||||
icon={UserLastActiveIcon}
|
||||
action={() => {
|
||||
jumpToLastActive(channel, user.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export function UserLastActiveIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 52 52"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#aa6746"
|
||||
>
|
||||
<g>
|
||||
<path d="M11.4,21.6L24.9,7.9c0.6-0.6,1.6-0.6,2.2,0l13.5,13.7c0.6,0.6,0.6,1.6,0,2.2L38.4,26
|
||||
c-0.6,0.6-1.6,0.6-2.2,0l-9.1-9.4c-0.6-0.6-1.6-0.6-2.2,0l-9.1,9.3c-0.6,0.6-1.6,0.6-2.2,0l-2.2-2.2C10.9,23.1,10.9,22.2,11.4,21.6
|
||||
z"/>
|
||||
<path d="M11.4,39.7L24.9,26c0.6-0.6,1.6-0.6,2.2,0l13.5,13.7c0.6,0.6,0.6,1.6,0,2.2l-2.2,2.2
|
||||
c-0.6,0.6-1.6,0.6-2.2,0l-9.1-9.4c-0.6-0.6-1.6-0.6-2.2,0L15.8,44c-0.6,0.6-1.6,0.6-2.2,0l-2.2-2.2C10.9,41.2,10.9,40.2,11.4,39.7z
|
||||
"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function LastActiveIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#aa6746"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path fillRule="evenodd" d="M12,2 C17.5228475,2 22,6.4771525 22,12 C22,17.5228475 17.5228475,22 12,22 C6.4771525,22 2,17.5228475 2,12 C2,6.4771525 6.4771525,2 12,2 Z M12,4 C7.581722,4 4,7.581722 4,12 C4,16.418278 7.581722,20 12,20 C16.418278,20 20,16.418278 20,12 C20,7.581722 16.418278,4 12,4 Z M12,6 C12.5128358,6 12.9355072,6.38604019 12.9932723,6.88337887 L13,7 L13,11.5857864 L14.7071068,13.2928932 C15.0976311,13.6834175 15.0976311,14.3165825 14.7071068,14.7071068 C14.3466228,15.0675907 13.7793918,15.0953203 13.3871006,14.7902954 L13.2928932,14.7071068 L11.2928932,12.7071068 C11.1366129,12.5508265 11.0374017,12.3481451 11.0086724,12.131444 L11,12 L11,7 C11,6.44771525 11.4477153,6 12,6 Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default definePlugin({
|
||||
name: "LastActive",
|
||||
description: "A plugin to jump to last active message from yourself or another user in a channel/server.",
|
||||
authors: [EquicordDevs.Crxa],
|
||||
contextMenus: {
|
||||
"channel-context": ChannelContextMenuPatch,
|
||||
"user-context": UserContextMenuPatch,
|
||||
"thread-context": ChannelContextMenuPatch
|
||||
}
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel, sendMessage } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
async function handleButtonClick() {
|
||||
// @ts-expect-error typing issue
|
||||
sendMessage(getCurrentChannel().id, { content: "meow" });
|
||||
}
|
||||
|
||||
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
if (!isMainChat) return null;
|
||||
return (
|
||||
<ChatBarButton tooltip="Meow" onClick={handleButtonClick}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 576 512"><path fill="currentColor" d="M320 192h17.1c22.1 38.3 63.5 64 110.9 64c11 0 21.8-1.4 32-4v228c0 17.7-14.3 32-32 32s-32-14.3-32-32V339.2L280 448h56c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-53 0-96-43-96-96V192.5c0-16.1-12-29.8-28-31.8l-7.9-1c-17.5-2.2-30-18.2-27.8-35.7S50.5 94 68 96.2l7.9 1c48 6 84.1 46.8 84.1 95.3v85.3c34.4-51.7 93.2-85.8 160-85.8m160 26.5c-10 3.5-20.8 5.5-32 5.5c-28.4 0-54-12.4-71.6-32c-3.7-4.1-7-8.5-9.9-13.2C357.3 164 352 146.6 352 128V10.7C352 4.8 356.7.1 362.6 0h.2c3.3 0 6.4 1.6 8.4 4.2v.1l12.8 17l27.2 36.3L416 64h64l4.8-6.4L512 21.3l12.8-17v-.1c2-2.6 5.1-4.2 8.4-4.2h.2c5.9.1 10.6 4.8 10.6 10.7V128c0 17.3-4.6 33.6-12.6 47.6c-11.3 19.8-29.6 35.2-51.4 42.9M432 128a16 16 0 1 0-32 0a16 16 0 1 0 32 0m48 16a16 16 0 1 0 0-32a16 16 0 1 0 0 32" /></svg>
|
||||
</ChatBarButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "Meow",
|
||||
description: "Adds a chatbar button to meow in chat",
|
||||
authors:
|
||||
[Devs.Samwich],
|
||||
start: () => addChatBarButton("Meow", ChatBarIcon),
|
||||
stop: () => removeChatBarButton("Meow")
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { Flogger, settings } from ".";
|
||||
import { addMessageIDB, db, DBMessageStatus, deleteMessagesBulkIDB, getOldestMessagesIDB } from "./db";
|
||||
import { LoggedMessage, LoggedMessageJSON } from "./types";
|
||||
import { cleanupMessage } from "./utils";
|
||||
import { cacheMessageImages } from "./utils/saveImage";
|
||||
|
||||
export const addMessage = async (message: LoggedMessage | LoggedMessageJSON, status: DBMessageStatus) => {
|
||||
if (settings.store.saveImages && status === DBMessageStatus.DELETED)
|
||||
await cacheMessageImages(message);
|
||||
const finalMessage = cleanupMessage(message);
|
||||
|
||||
await addMessageIDB(finalMessage, status);
|
||||
|
||||
if (settings.store.messageLimit > 0) {
|
||||
const currentMessageCount = await db.count("messages");
|
||||
if (currentMessageCount > settings.store.messageLimit) {
|
||||
const messagesToDelete = currentMessageCount - settings.store.messageLimit;
|
||||
if (messagesToDelete <= 0 || messagesToDelete >= settings.store.messageLimit) return;
|
||||
|
||||
const oldestMessages = await getOldestMessagesIDB(messagesToDelete);
|
||||
|
||||
Flogger.info(`Deleting ${messagesToDelete} oldest messages`);
|
||||
await deleteMessagesBulkIDB(oldestMessages.map(m => m.message_id));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { classNameFactory } from "@api/Styles";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { Button, Forms, Toasts } from "@webpack/common";
|
||||
|
||||
import { Native, settings } from "..";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "../utils/constants";
|
||||
|
||||
const cl = classNameFactory("folder-upload");
|
||||
|
||||
function createDirSelector(settingKey: "logsDir" | "imageCacheDir", successMessage: string) {
|
||||
return function DirSelector({ option }) {
|
||||
if (IS_WEB) return null;
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<SelectFolderInput
|
||||
settingsKey={settingKey}
|
||||
successMessage={successMessage}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const ImageCacheDir = createDirSelector("imageCacheDir", "Successfully updated Image Cache Dir");
|
||||
export const LogsDir = createDirSelector("logsDir", "Successfully updated Logs Dir");
|
||||
|
||||
interface Props {
|
||||
settingsKey: "imageCacheDir" | "logsDir",
|
||||
successMessage: string,
|
||||
}
|
||||
|
||||
export function SelectFolderInput({ settingsKey, successMessage }: Props) {
|
||||
const path = settings.store[settingsKey];
|
||||
|
||||
function getDirName(path: string) {
|
||||
const parts = path.split("\\").length > 1 ? path.split("\\") : path.split("/");
|
||||
|
||||
return parts.slice(parts.length - 2, parts.length).join("\\");
|
||||
}
|
||||
|
||||
async function onFolderSelect() {
|
||||
try {
|
||||
const res = await Native.chooseDir(settingsKey);
|
||||
settings.store[settingsKey] = res;
|
||||
|
||||
return Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.SUCCESS,
|
||||
message: successMessage
|
||||
});
|
||||
} catch (err) {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to update directory"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cl("-container")}>
|
||||
<div onClick={() => copyWithToast(path)} className={cl("-input")}>
|
||||
{path == null || path === DEFAULT_IMAGE_CACHE_DIR ? "Choose Folder" : getDirName(path)}
|
||||
</div>
|
||||
<Button
|
||||
className={cl("-button")}
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={onFolderSelect}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { findComponentByCodeLazy } from "@webpack";
|
||||
|
||||
import { openLogModal } from "./LogsModal";
|
||||
|
||||
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
|
||||
|
||||
export function OpenLogsIcon() {
|
||||
return (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 15 15"
|
||||
height={24}
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenLogsButton() {
|
||||
return (
|
||||
<HeaderBarIcon
|
||||
className="vc-log-toolbox-btn"
|
||||
onClick={() => openLogModal()}
|
||||
tooltip={"Open Logs"}
|
||||
icon={OpenLogsIcon}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,446 +0,0 @@
|
|||
/*
|
||||
* 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 { Flex } from "@components/Flex";
|
||||
import { InfoIcon } from "@components/Icons";
|
||||
import { openUserProfile } from "@utils/discord";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { closeAllModals, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { find, findByCode, findByCodeLazy } from "@webpack";
|
||||
import { Alerts, Button, ChannelStore, ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, React, TabBar, Text, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import { clearMessagesIDB, DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "../db";
|
||||
import { settings } from "../index";
|
||||
import { LoggedMessage, LoggedMessageJSON } from "../types";
|
||||
import { messageJsonToMessageClass } from "../utils";
|
||||
import { importLogs } from "../utils/settingsUtils";
|
||||
import { useMessages } from "./hooks";
|
||||
|
||||
export interface MessagePreviewProps {
|
||||
className: string;
|
||||
author: User;
|
||||
message: LoggedMessage;
|
||||
compact: boolean;
|
||||
isGroupStart: boolean;
|
||||
hideSimpleEmbedContent: boolean;
|
||||
|
||||
childrenAccessories: any;
|
||||
}
|
||||
|
||||
export interface ChildrenAccProops {
|
||||
channelMessageProps: {
|
||||
compact: boolean;
|
||||
channel: any;
|
||||
message: LoggedMessage;
|
||||
groupId: string;
|
||||
id: string;
|
||||
isLastItem: boolean;
|
||||
isHighlight: boolean;
|
||||
renderContentOnly: boolean;
|
||||
};
|
||||
hasSpoilerEmbeds: boolean;
|
||||
isInteracting: boolean;
|
||||
isAutomodBlockedMessage: boolean;
|
||||
showClydeAiEmbeds: boolean;
|
||||
}
|
||||
|
||||
const PrivateChannelRecord = findByCodeLazy(".is_message_request_timestamp,");
|
||||
const MessagePreview = LazyComponent<MessagePreviewProps>(() => find(m => m?.type?.toString().includes("previewLinkTarget:") && !m?.type?.toString().includes("HAS_THREAD")));
|
||||
const ChildrenAccessories = LazyComponent<ChildrenAccProops>(() => findByCode("channelMessageProps:{message:"));
|
||||
|
||||
const cl = classNameFactory("msg-logger-modal-");
|
||||
|
||||
export enum LogTabs {
|
||||
DELETED = "Deleted",
|
||||
EDITED = "Edited",
|
||||
GHOST_PING = "Ghost Pinged"
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modalProps: ModalProps;
|
||||
initalQuery?: string;
|
||||
}
|
||||
|
||||
export function LogsModal({ modalProps, initalQuery }: Props) {
|
||||
const [currentTab, setCurrentTab] = useState(LogTabs.DELETED);
|
||||
const [queryEh, setQuery] = useState(initalQuery ?? "");
|
||||
const [sortNewest, setSortNewest] = useState(settings.store.sortNewest);
|
||||
const [numDisplayedMessages, setNumDisplayedMessages] = useState(settings.store.messagesToDisplayAtOnceInLogs);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { messages, total, statusTotal, pending, reset } = useMessages(queryEh, currentTab, sortNewest, numDisplayedMessages);
|
||||
|
||||
return (
|
||||
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.LARGE}>
|
||||
<ModalHeader className={cl("header")}>
|
||||
<TextInput value={queryEh} onChange={e => setQuery(e)} style={{ width: "100%" }} placeholder="Filter Messages" />
|
||||
<TabBar
|
||||
type="top"
|
||||
look="brand"
|
||||
className={cl("tab-bar")}
|
||||
selectedItem={currentTab}
|
||||
onItemSelect={e => {
|
||||
setCurrentTab(e);
|
||||
setNumDisplayedMessages(settings.store.messagesToDisplayAtOnceInLogs);
|
||||
contentRef.current?.firstElementChild?.scrollTo(0, 0);
|
||||
// forceUpdate();
|
||||
}}
|
||||
>
|
||||
<TabBar.Item
|
||||
className={cl("tab-bar-item")}
|
||||
id={LogTabs.DELETED}
|
||||
>
|
||||
Deleted
|
||||
</TabBar.Item>
|
||||
<TabBar.Item
|
||||
className={cl("tab-bar-item")}
|
||||
id={LogTabs.EDITED}
|
||||
>
|
||||
Edited
|
||||
</TabBar.Item>
|
||||
<TabBar.Item
|
||||
className={cl("tab-bar-item")}
|
||||
id={LogTabs.GHOST_PING}
|
||||
>
|
||||
Ghost Pinged
|
||||
</TabBar.Item>
|
||||
</TabBar>
|
||||
</ModalHeader>
|
||||
<div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0" }} className={cl("content-container")} ref={contentRef}>
|
||||
{
|
||||
modalProps.transitionState === 1 &&
|
||||
<ModalContent
|
||||
className={cl("content")}
|
||||
>
|
||||
{messages != null && total === 0 && (
|
||||
<EmptyLogs
|
||||
hasQuery={queryEh.length !== 0}
|
||||
reset={reset}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!pending && messages != null && (
|
||||
<LogsContentMemo
|
||||
visibleMessages={messages}
|
||||
canLoadMore={messages.length < statusTotal && messages.length >= settings.store.messagesToDisplayAtOnceInLogs}
|
||||
tab={currentTab}
|
||||
sortNewest={sortNewest}
|
||||
reset={reset}
|
||||
handleLoadMore={() => setNumDisplayedMessages(e => e + settings.store.messagesToDisplayAtOnceInLogs)}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
}
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Clear Logs",
|
||||
body: "Are you sure you want to clear all the logs",
|
||||
confirmText: "Clear",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
await clearMessagesIDB();
|
||||
reset();
|
||||
}
|
||||
|
||||
})}
|
||||
>
|
||||
Clear All Logs
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginRight: "16px" }}
|
||||
color={Button.Colors.YELLOW}
|
||||
disabled={messages?.length === 0}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Clear Logs",
|
||||
body: `Are you sure you want to clear ${messages.length} logs`,
|
||||
confirmText: "Clear",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
await deleteMessagesBulkIDB(messages.map(e => e.message_id));
|
||||
reset();
|
||||
}
|
||||
})}
|
||||
>
|
||||
Clear Visible Logs
|
||||
</Button>
|
||||
<Button
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.PRIMARY}
|
||||
onClick={() => {
|
||||
setSortNewest(e => {
|
||||
const val = !e;
|
||||
settings.store.sortNewest = val;
|
||||
return val;
|
||||
});
|
||||
contentRef.current?.firstElementChild?.scrollTo(0, 0);
|
||||
}}
|
||||
>
|
||||
Sort {sortNewest ? "Oldest First" : "Newest First"}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
interface LogContentProps {
|
||||
sortNewest: boolean;
|
||||
tab: LogTabs;
|
||||
visibleMessages: DBMessageRecord[];
|
||||
canLoadMore: boolean;
|
||||
reset: () => void;
|
||||
handleLoadMore: () => void;
|
||||
}
|
||||
|
||||
function LogsContent({ visibleMessages, canLoadMore, sortNewest, tab, reset, handleLoadMore }: LogContentProps) {
|
||||
if (visibleMessages.length === 0)
|
||||
return <NoResults tab={tab} />;
|
||||
|
||||
return (
|
||||
<div className={cl("content-inner")}>
|
||||
{visibleMessages
|
||||
.map(({ message }, i) => (
|
||||
<LMessage
|
||||
key={message.id}
|
||||
log={{ message }}
|
||||
reset={reset}
|
||||
isGroupStart={isGroupStart(message, visibleMessages[i - 1]?.message, sortNewest)}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
canLoadMore &&
|
||||
<Button
|
||||
style={{ marginTop: "1rem", width: "100%" }}
|
||||
size={Button.Sizes.SMALL} onClick={() => handleLoadMore()}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LogsContentMemo = LazyComponent(() => React.memo(LogsContent));
|
||||
|
||||
|
||||
function NoResults({ tab }: { tab: LogTabs; }) {
|
||||
const generateSuggestedTabs = (tab: LogTabs) => {
|
||||
switch (tab) {
|
||||
case LogTabs.DELETED:
|
||||
return { nextTab: LogTabs.EDITED, lastTab: LogTabs.GHOST_PING };
|
||||
case LogTabs.EDITED:
|
||||
return { nextTab: LogTabs.GHOST_PING, lastTab: LogTabs.DELETED };
|
||||
case LogTabs.GHOST_PING:
|
||||
return { nextTab: LogTabs.DELETED, lastTab: LogTabs.EDITED };
|
||||
default:
|
||||
return { nextTab: "", lastTab: "" };
|
||||
}
|
||||
};
|
||||
|
||||
const { nextTab, lastTab } = generateSuggestedTabs(tab);
|
||||
|
||||
return (
|
||||
<div className={cl("empty-logs", "content-inner")} style={{ textAlign: "center" }}>
|
||||
<Text variant="text-lg/normal">
|
||||
No results in <b>{tab}</b>.
|
||||
</Text>
|
||||
<Text variant="text-lg/normal" style={{ marginTop: "0.2rem" }}>
|
||||
Maybe try <b>{nextTab}</b> or <b>{lastTab}</b>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyLogs({ hasQuery, reset: forceUpdate }: { hasQuery: boolean; reset: () => void; }) {
|
||||
return (
|
||||
<div className={cl("empty-logs", "content-inner")} style={{ textAlign: "center" }}>
|
||||
<Flex flexDirection="column" style={{ position: "relative" }}>
|
||||
|
||||
<Text variant="text-lg/normal">
|
||||
Empty eh
|
||||
</Text>
|
||||
|
||||
{!hasQuery && (
|
||||
<>
|
||||
<Tooltip text="ML Enhanced now stores logs in indexeddb. You need to import your old logs from the logs directory. Importing wont overwrite existing logs">
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div
|
||||
className={cl("info-icon")}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
<Button onClick={() => importLogs().then(() => forceUpdate())}>
|
||||
Import Logs
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
interface LMessageProps {
|
||||
log: { message: LoggedMessageJSON; };
|
||||
isGroupStart: boolean,
|
||||
reset: () => void;
|
||||
}
|
||||
function LMessage({ log, isGroupStart, reset, }: LMessageProps) {
|
||||
const message = useMemo(() => messageJsonToMessageClass(log), [log]);
|
||||
|
||||
// console.log(message);
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={e => {
|
||||
ContextMenuApi.openContextMenu(e, () =>
|
||||
<Menu.Menu
|
||||
navId="message-logger"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Message Logger"
|
||||
>
|
||||
|
||||
<Menu.MenuItem
|
||||
key="jump-to-message"
|
||||
id="jump-to-message"
|
||||
label="Jump To Message"
|
||||
action={() => {
|
||||
NavigationRouter.transitionTo(`/channels/${ChannelStore.getChannel(message.channel_id)?.guild_id ?? "@me"}/${message.channel_id}${message.id ? "/" + message.id : ""}`);
|
||||
closeAllModals();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key="open-user-profile"
|
||||
id="open-user-profile"
|
||||
label="Open user profile"
|
||||
action={() => {
|
||||
closeAllModals();
|
||||
openUserProfile(message.author.id);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem
|
||||
key="copy-content"
|
||||
id="copy-content"
|
||||
label="Copy Content"
|
||||
action={() => copyWithToast(message.content)}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem
|
||||
key="copy-user-id"
|
||||
id="copy-user-id"
|
||||
label="Copy User ID"
|
||||
action={() => copyWithToast(message.author.id)}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem
|
||||
key="copy-message-id"
|
||||
id="copy-message-id"
|
||||
label="Copy Message ID"
|
||||
action={() => copyWithToast(message.id)}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem
|
||||
key="copy-channel-id"
|
||||
id="copy-channel-id"
|
||||
label="Copy Channel ID"
|
||||
action={() => copyWithToast(message.channel_id)}
|
||||
/>
|
||||
|
||||
{
|
||||
log.message.guildId != null
|
||||
&& (
|
||||
<Menu.MenuItem
|
||||
key="copy-server-id"
|
||||
id="copy-server-id"
|
||||
label="Copy Server ID"
|
||||
action={() => copyWithToast(log.message.guildId!)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<Menu.MenuItem
|
||||
key="delete-log"
|
||||
id="delete-log"
|
||||
label="Delete Log"
|
||||
color="danger"
|
||||
action={() =>
|
||||
deleteMessageIDB(log.message.id).then(() => reset())
|
||||
}
|
||||
/>
|
||||
|
||||
</Menu.Menu>
|
||||
);
|
||||
}}>
|
||||
<MessagePreview
|
||||
className={`${cl("msg-preview")} ${message.deleted ? "messagelogger-deleted" : ""}`}
|
||||
author={message.author}
|
||||
message={message}
|
||||
compact={false}
|
||||
isGroupStart={isGroupStart}
|
||||
hideSimpleEmbedContent={false}
|
||||
|
||||
childrenAccessories={
|
||||
<ChildrenAccessories
|
||||
channelMessageProps={{
|
||||
channel: ChannelStore.getChannel(message.channel_id) || new PrivateChannelRecord({ id: "" }),
|
||||
message,
|
||||
compact: false,
|
||||
groupId: "1",
|
||||
id: message.id,
|
||||
isLastItem: false,
|
||||
isHighlight: false,
|
||||
renderContentOnly: false,
|
||||
}}
|
||||
hasSpoilerEmbeds={false}
|
||||
isInteracting={false}
|
||||
showClydeAiEmbeds={true}
|
||||
isAutomodBlockedMessage={false}
|
||||
/>
|
||||
}
|
||||
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const openLogModal = (initalQuery?: string) => openModal(modalProps => <LogsModal modalProps={modalProps} initalQuery={initalQuery} />);
|
||||
|
||||
function isGroupStart(
|
||||
currentMessage: LoggedMessageJSON | undefined,
|
||||
previousMessage: LoggedMessageJSON | undefined,
|
||||
sortNewest: boolean
|
||||
) {
|
||||
if (!currentMessage || !previousMessage) return true;
|
||||
|
||||
if (currentMessage.id === previousMessage.id) return true;
|
||||
|
||||
const [newestMessage, oldestMessage] = sortNewest
|
||||
? [previousMessage, currentMessage]
|
||||
: [currentMessage, previousMessage];
|
||||
|
||||
if (newestMessage.author.id !== oldestMessage.author.id) return true;
|
||||
|
||||
const timeDifferenceInMinutes = Math.abs(
|
||||
(new Date(newestMessage.timestamp)?.getTime() - new Date(oldestMessage.timestamp)?.getTime()) / (1000 * 60)
|
||||
);
|
||||
|
||||
return timeDifferenceInMinutes >= 5;
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { countMessagesByStatusIDB, countMessagesIDB, DBMessageRecord, DBMessageStatus, getDateStortedMessagesByStatusIDB } from "../db";
|
||||
import { doesMatch, tokenizeQuery } from "../utils/parseQuery";
|
||||
import { LogTabs } from "./LogsModal";
|
||||
|
||||
function useDebouncedValue<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
// this is so shit
|
||||
export function useMessages(query: string, currentTab: LogTabs, sortNewest: boolean, numDisplayedMessages: number) {
|
||||
// only for initial load
|
||||
const [pending, setPending] = useState(true);
|
||||
const [messages, setMessages] = useState<DBMessageRecord[]>([]);
|
||||
const [statusTotal, setStatusTotal] = useState<number>(0);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
|
||||
const debouncedQuery = useDebouncedValue(query, 300);
|
||||
|
||||
useEffect(() => {
|
||||
countMessagesIDB().then(x => setTotal(x));
|
||||
}, [pending]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadMessages = async () => {
|
||||
const status = getStatus(currentTab);
|
||||
|
||||
if (debouncedQuery === "") {
|
||||
const [messages, statusTotal] = await Promise.all([
|
||||
getDateStortedMessagesByStatusIDB(sortNewest, numDisplayedMessages, status),
|
||||
countMessagesByStatusIDB(status),
|
||||
]);
|
||||
|
||||
|
||||
if (isMounted) {
|
||||
setMessages(messages);
|
||||
setStatusTotal(statusTotal);
|
||||
}
|
||||
|
||||
setPending(false);
|
||||
} else {
|
||||
const allMessages = await getDateStortedMessagesByStatusIDB(sortNewest, Number.MAX_SAFE_INTEGER, status);
|
||||
const { queries, rest } = tokenizeQuery(debouncedQuery);
|
||||
|
||||
const filteredMessages = allMessages.filter(record => {
|
||||
for (const query of queries) {
|
||||
const matching = doesMatch(query.key, query.value, record.message);
|
||||
if (query.negate ? matching : !matching) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return rest.every(r =>
|
||||
record.message.content.toLowerCase().includes(r.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
if (isMounted) {
|
||||
setMessages(filteredMessages);
|
||||
setStatusTotal(Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadMessages();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
|
||||
}, [debouncedQuery, sortNewest, numDisplayedMessages, currentTab, pending]);
|
||||
|
||||
|
||||
return { messages, statusTotal, total, pending, reset: () => setPending(true) };
|
||||
}
|
||||
|
||||
|
||||
function getStatus(currentTab: LogTabs) {
|
||||
switch (currentTab) {
|
||||
case LogTabs.DELETED:
|
||||
return DBMessageStatus.DELETED;
|
||||
case LogTabs.EDITED:
|
||||
return DBMessageStatus.EDITED;
|
||||
default:
|
||||
return DBMessageStatus.GHOST_PINGED;
|
||||
}
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { DBSchema, IDBPDatabase, openDB } from "idb";
|
||||
|
||||
import { LoggedMessageJSON } from "./types";
|
||||
import { getMessageStatus } from "./utils";
|
||||
import { DB_NAME, DB_VERSION } from "./utils/constants";
|
||||
import { getAttachmentBlobUrl } from "./utils/saveImage";
|
||||
|
||||
export enum DBMessageStatus {
|
||||
DELETED = "DELETED",
|
||||
EDITED = "EDITED",
|
||||
GHOST_PINGED = "GHOST_PINGED",
|
||||
}
|
||||
|
||||
export interface DBMessageRecord {
|
||||
message_id: string;
|
||||
channel_id: string;
|
||||
status: DBMessageStatus;
|
||||
message: LoggedMessageJSON;
|
||||
}
|
||||
|
||||
export interface MLIDB extends DBSchema {
|
||||
messages: {
|
||||
key: string;
|
||||
value: DBMessageRecord;
|
||||
indexes: {
|
||||
by_channel_id: string;
|
||||
by_status: DBMessageStatus;
|
||||
by_timestamp: string;
|
||||
by_timestamp_and_message_id: [string, string];
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export let db: IDBPDatabase<MLIDB>;
|
||||
export const cachedMessages = new Map<string, LoggedMessageJSON>();
|
||||
|
||||
// this is probably not the best way to do this
|
||||
async function cacheRecords(records: DBMessageRecord[]) {
|
||||
for (const r of records) {
|
||||
cacheRecord(r);
|
||||
|
||||
for (const att of r.message.attachments) {
|
||||
const blobUrl = await getAttachmentBlobUrl(att);
|
||||
if (blobUrl) {
|
||||
att.url = blobUrl + "#";
|
||||
att.proxy_url = blobUrl + "#";
|
||||
}
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
async function cacheRecord(record?: DBMessageRecord | null) {
|
||||
if (!record) return record;
|
||||
|
||||
cachedMessages.set(record.message_id, record.message);
|
||||
return record;
|
||||
}
|
||||
|
||||
export async function initIDB() {
|
||||
db = await openDB<MLIDB>(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
const messageStore = db.createObjectStore("messages", { keyPath: "message_id" });
|
||||
messageStore.createIndex("by_channel_id", "channel_id");
|
||||
messageStore.createIndex("by_status", "status");
|
||||
messageStore.createIndex("by_timestamp", "message.timestamp");
|
||||
messageStore.createIndex("by_timestamp_and_message_id", ["channel_id", "message.timestamp"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
initIDB();
|
||||
|
||||
export async function hasMessageIDB(message_id: string) {
|
||||
return cachedMessages.has(message_id) || (await db.count("messages", message_id)) > 0;
|
||||
}
|
||||
|
||||
export async function countMessagesIDB() {
|
||||
return db.count("messages");
|
||||
}
|
||||
|
||||
export async function countMessagesByStatusIDB(status: DBMessageStatus) {
|
||||
return db.countFromIndex("messages", "by_status", status);
|
||||
}
|
||||
|
||||
export async function getAllMessagesIDB() {
|
||||
return cacheRecords(await db.getAll("messages"));
|
||||
}
|
||||
|
||||
export async function getMessagesForChannelIDB(channel_id: string) {
|
||||
return cacheRecords(await db.getAllFromIndex("messages", "by_channel_id", channel_id));
|
||||
}
|
||||
|
||||
export async function getMessageIDB(message_id: string) {
|
||||
return cacheRecord(await db.get("messages", message_id));
|
||||
}
|
||||
|
||||
export async function getMessagesByStatusIDB(status: DBMessageStatus) {
|
||||
return cacheRecords(await db.getAllFromIndex("messages", "by_status", status));
|
||||
}
|
||||
|
||||
export async function getOldestMessagesIDB(limit: number) {
|
||||
return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit));
|
||||
}
|
||||
|
||||
export async function getDateStortedMessagesByStatusIDB(newest: boolean, limit: number, status: DBMessageStatus) {
|
||||
const tx = db.transaction("messages", "readonly");
|
||||
const { store } = tx;
|
||||
const index = store.index("by_status");
|
||||
|
||||
const direction = newest ? "prev" : "next";
|
||||
const cursor = await index.openCursor(IDBKeyRange.only(status), direction);
|
||||
|
||||
if (!cursor) {
|
||||
console.log("No messages found");
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages: DBMessageRecord[] = [];
|
||||
for await (const c of cursor) {
|
||||
messages.push(c.value);
|
||||
if (messages.length >= limit) break;
|
||||
}
|
||||
|
||||
return cacheRecords(messages);
|
||||
}
|
||||
|
||||
export async function getMessagesByChannelAndAfterTimestampIDB(channel_id: string, start: string) {
|
||||
const tx = db.transaction("messages", "readonly");
|
||||
const { store } = tx;
|
||||
const index = store.index("by_timestamp_and_message_id");
|
||||
|
||||
const cursor = await index.openCursor(IDBKeyRange.bound([channel_id, start], [channel_id, "\uffff"]));
|
||||
|
||||
if (!cursor) {
|
||||
console.log("No messages found in range");
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages: DBMessageRecord[] = [];
|
||||
for await (const c of cursor) {
|
||||
messages.push(c.value);
|
||||
}
|
||||
|
||||
return cacheRecords(messages);
|
||||
}
|
||||
|
||||
export async function addMessageIDB(message: LoggedMessageJSON, status: DBMessageStatus) {
|
||||
await db.put("messages", {
|
||||
channel_id: message.channel_id,
|
||||
message_id: message.id,
|
||||
status,
|
||||
message,
|
||||
});
|
||||
|
||||
cachedMessages.set(message.id, message);
|
||||
}
|
||||
|
||||
export async function addMessagesBulkIDB(messages: LoggedMessageJSON[], status?: DBMessageStatus) {
|
||||
const tx = db.transaction("messages", "readwrite");
|
||||
const { store } = tx;
|
||||
|
||||
await Promise.all([
|
||||
...messages.map(message => store.add({
|
||||
channel_id: message.channel_id,
|
||||
message_id: message.id,
|
||||
status: status ?? getMessageStatus(message),
|
||||
message,
|
||||
})),
|
||||
tx.done
|
||||
]);
|
||||
|
||||
messages.forEach(message => cachedMessages.set(message.id, message));
|
||||
}
|
||||
|
||||
|
||||
export async function deleteMessageIDB(message_id: string) {
|
||||
await db.delete("messages", message_id);
|
||||
|
||||
cachedMessages.delete(message_id);
|
||||
}
|
||||
|
||||
export async function deleteMessagesBulkIDB(message_ids: string[]) {
|
||||
const tx = db.transaction("messages", "readwrite");
|
||||
const { store } = tx;
|
||||
|
||||
await Promise.all([...message_ids.map(id => store.delete(id)), tx.done]);
|
||||
message_ids.forEach(id => cachedMessages.delete(id));
|
||||
}
|
||||
|
||||
export async function clearMessagesIDB() {
|
||||
await db.clear("messages");
|
||||
cachedMessages.clear();
|
||||
}
|
|
@ -1,403 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const VERSION = "4.0.0";
|
||||
|
||||
export const Native = getNative();
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, MessageStore, React, UserStore } from "@webpack/common";
|
||||
|
||||
import { OpenLogsButton } from "./components/LogsButton";
|
||||
import { openLogModal } from "./components/LogsModal";
|
||||
import * as idb from "./db";
|
||||
import { addMessage } from "./LoggedMessageManager";
|
||||
import * as LoggedMessageManager from "./LoggedMessageManager";
|
||||
import { settings } from "./settings";
|
||||
import { FetchMessagesResponse, LoadMessagePayload, LoggedMessage, LoggedMessageJSON, MessageCreatePayload, MessageDeleteBulkPayload, MessageDeletePayload, MessageUpdatePayload } from "./types";
|
||||
import { cleanUpCachedMessage, cleanupUserObject, getNative, isGhostPinged, mapTimestamp, messageJsonToMessageClass, reAddDeletedMessages } from "./utils";
|
||||
import { removeContextMenuBindings, setupContextMenuPatches } from "./utils/contextMenu";
|
||||
import { shouldIgnore } from "./utils/index";
|
||||
import { LimitedMap } from "./utils/LimitedMap";
|
||||
import { doesMatch } from "./utils/parseQuery";
|
||||
import * as imageUtils from "./utils/saveImage";
|
||||
import * as ImageManager from "./utils/saveImage/ImageManager";
|
||||
export { settings };
|
||||
|
||||
export const Flogger = new Logger("MessageLoggerEnhanced", "#f26c6c");
|
||||
|
||||
export const cacheSentMessages = new LimitedMap<string, LoggedMessageJSON>();
|
||||
|
||||
const cacheThing = findByPropsLazy("commit", "getOrCreate");
|
||||
|
||||
let oldGetMessage: typeof MessageStore.getMessage;
|
||||
|
||||
const handledMessageIds = new Set();
|
||||
async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: boolean; }) {
|
||||
if (payload.mlDeleted) {
|
||||
if (settings.store.permanentlyRemoveLogByDefault)
|
||||
await idb.deleteMessageIDB(payload.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (handledMessageIds.has(payload.id)) {
|
||||
// Flogger.warn("skipping duplicate message", payload.id);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
handledMessageIds.add(payload.id);
|
||||
|
||||
let message: LoggedMessage | LoggedMessageJSON | null =
|
||||
oldGetMessage?.(payload.channelId, payload.id);
|
||||
if (message == null) {
|
||||
// most likely an edited message
|
||||
const cachedMessage = cacheSentMessages.get(`${payload.channelId},${payload.id}`);
|
||||
if (!cachedMessage) return; // Flogger.log("no message to save");
|
||||
|
||||
message = { ...cacheSentMessages.get(`${payload.channelId},${payload.id}`), deleted: true } as LoggedMessageJSON;
|
||||
}
|
||||
|
||||
const ghostPinged = isGhostPinged(message as any);
|
||||
|
||||
if (
|
||||
shouldIgnore({
|
||||
channelId: message?.channel_id ?? payload.channelId,
|
||||
guildId: payload.guildId ?? (message as any).guildId ?? (message as any).guild_id,
|
||||
authorId: message?.author?.id,
|
||||
bot: message?.bot || message?.author?.bot,
|
||||
flags: message?.flags,
|
||||
ghostPinged,
|
||||
isCachedByUs: (message as LoggedMessageJSON).ourCache,
|
||||
webhookId: message?.webhookId
|
||||
})
|
||||
) {
|
||||
// Flogger.log("IGNORING", message, payload);
|
||||
return FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: payload.channelId,
|
||||
id: payload.id,
|
||||
mlDeleted: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (message == null || message.channel_id == null || !message.deleted) return;
|
||||
// Flogger.log("ADDING MESSAGE (DELETED)", message);
|
||||
if (payload.isBulk)
|
||||
return message;
|
||||
|
||||
await addMessage(message, ghostPinged ? idb.DBMessageStatus.GHOST_PINGED : idb.DBMessageStatus.DELETED);
|
||||
}
|
||||
finally {
|
||||
handledMessageIds.delete(payload.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function messageDeleteBulkHandler({ channelId, guildId, ids }: MessageDeleteBulkPayload) {
|
||||
// is this bad? idk man
|
||||
const messages = [] as LoggedMessageJSON[];
|
||||
for (const id of ids) {
|
||||
const msg = await messageDeleteHandler({ type: "MESSAGE_DELETE", channelId, guildId, id, isBulk: true });
|
||||
if (msg) messages.push(msg as LoggedMessageJSON);
|
||||
}
|
||||
|
||||
await idb.addMessagesBulkIDB(messages);
|
||||
}
|
||||
|
||||
async function messageUpdateHandler(payload: MessageUpdatePayload) {
|
||||
const cachedMessage = cacheSentMessages.get(`${payload.message.channel_id},${payload.message.id}`);
|
||||
if (
|
||||
shouldIgnore({
|
||||
channelId: payload.message?.channel_id,
|
||||
guildId: payload.guildId ?? (payload as any).guild_id,
|
||||
authorId: payload.message?.author?.id,
|
||||
bot: (payload.message?.author as any)?.bot,
|
||||
flags: payload.message?.flags,
|
||||
ghostPinged: isGhostPinged(payload.message as any),
|
||||
isCachedByUs: cachedMessage?.ourCache ?? false
|
||||
})
|
||||
) {
|
||||
const cache = cacheThing.getOrCreate(payload.message.channel_id);
|
||||
const message = cache.get(payload.message.id);
|
||||
if (message) {
|
||||
message.editHistory = [];
|
||||
cacheThing.commit(cache);
|
||||
}
|
||||
return;// Flogger.log("this message has been ignored", payload);
|
||||
}
|
||||
|
||||
let message = oldGetMessage?.(payload.message.channel_id, payload.message.id) as LoggedMessage | LoggedMessageJSON | null;
|
||||
|
||||
if (message == null) {
|
||||
// MESSAGE_UPDATE gets dispatched when emebeds change too and content becomes null
|
||||
if (cachedMessage != null && payload.message.content != null && cachedMessage.content !== payload.message.content) {
|
||||
message = {
|
||||
...cachedMessage,
|
||||
content: payload.message.content,
|
||||
editHistory: [
|
||||
...(cachedMessage.editHistory ?? []),
|
||||
{
|
||||
content: cachedMessage.content,
|
||||
timestamp: (new Date()).toISOString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, message);
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null || message.channel_id == null || message.editHistory == null || message.editHistory.length === 0) return;
|
||||
|
||||
// Flogger.log("ADDING MESSAGE (EDITED)", message, payload);
|
||||
await addMessage(message, idb.DBMessageStatus.EDITED);
|
||||
}
|
||||
|
||||
function messageCreateHandler(payload: MessageCreatePayload) {
|
||||
// we do this here because cache is limited and to save memory
|
||||
if (!settings.store.cacheMessagesFromServers && payload.guildId != null) {
|
||||
const ids = [payload.channelId, payload.message?.author?.id, payload.guildId];
|
||||
const isWhitelisted =
|
||||
settings.store.whitelistedIds
|
||||
.split(",")
|
||||
.some(e => ids.includes(e));
|
||||
if (!isWhitelisted) {
|
||||
return; // dont cache messages from servers when cacheMessagesFromServers is disabled and not whitelisted.
|
||||
}
|
||||
}
|
||||
|
||||
cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, cleanUpCachedMessage(payload.message));
|
||||
// Flogger.log(`cached\nkey:${payload.message.channel_id},${payload.message.id}\nvalue:`, payload.message);
|
||||
}
|
||||
|
||||
async function processMessageFetch(response: FetchMessagesResponse) {
|
||||
try {
|
||||
if (!response.ok || response.body.length === 0) {
|
||||
Flogger.error("Failed to fetch messages", response);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstMessage = response.body[response.body.length - 1];
|
||||
// console.time("fetching messages from idb");
|
||||
const messages = await idb.getMessagesByChannelAndAfterTimestampIDB(firstMessage.channel_id, firstMessage.timestamp);
|
||||
// console.timeEnd("fetching messages from idb");
|
||||
|
||||
if (!messages.length) return;
|
||||
|
||||
const deletedMessages = messages.filter(m => m.status === idb.DBMessageStatus.DELETED);
|
||||
|
||||
for (const recivedMessage of response.body) {
|
||||
const record = messages.find(m => m.message_id === recivedMessage.id);
|
||||
|
||||
if (record == null) continue;
|
||||
|
||||
if (record.message.editHistory && record.message.editHistory.length > 0) {
|
||||
recivedMessage.editHistory = record.message.editHistory;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUser = (id: string) => UserStore.getUser(id) || response.body.find(e => e.author.id === id);
|
||||
|
||||
for (let i = 0, len = messages.length; i < len; i++) {
|
||||
const record = messages[i];
|
||||
if (!record) continue;
|
||||
|
||||
const { message } = record;
|
||||
|
||||
for (let j = 0, len2 = message.mentions.length; j < len2; j++) {
|
||||
const user = message.mentions[j];
|
||||
const cachedUser = fetchUser((user as any).id || user);
|
||||
if (cachedUser) (message.mentions[j] as any) = cleanupUserObject(cachedUser);
|
||||
}
|
||||
|
||||
const author = fetchUser(message.author.id);
|
||||
if (!author) continue;
|
||||
(message.author as any) = cleanupUserObject(author);
|
||||
}
|
||||
|
||||
response.body.extra = deletedMessages.map(m => m.message);
|
||||
|
||||
} catch (e) {
|
||||
Flogger.error("Failed to fetch messages", e);
|
||||
}
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageLoggerEnhanced",
|
||||
authors: [Devs.Aria],
|
||||
description: "G'day",
|
||||
dependencies: ["MessageLogger"],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "_tryFetchMessagesCached",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=\.get\({url.+?then\()(\i)=>\(/,
|
||||
replace: "async $1=>(await $self.processMessageFetch($1),"
|
||||
},
|
||||
{
|
||||
match: /(?<=type:"LOAD_MESSAGES_SUCCESS",.{1,100})messages:(\i)/,
|
||||
replace: "get messages() {return $self.coolReAddDeletedMessages($1, this);}"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "THREAD_STARTER_MESSAGE?null==",
|
||||
replacement: {
|
||||
match: /deleted:\i\.deleted, editHistory:\i\.editHistory,/,
|
||||
replace: "deleted:$self.getDeleted(...arguments), editHistory:$self.getEdited(...arguments),"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "toolbar:function",
|
||||
predicate: () => settings.store.ShowLogsButton,
|
||||
replacement: {
|
||||
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/,
|
||||
replace: "$1$self.addIconToToolBar(arguments[0]);$2"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
find: "childrenMessageContent:null",
|
||||
replacement: {
|
||||
match: /(cozyMessage.{1,50},)childrenHeader:/,
|
||||
replace: "$1childrenAccessories:arguments[0].childrenAccessories || null,childrenHeader:"
|
||||
}
|
||||
},
|
||||
|
||||
// https://regex101.com/r/S3IVGm/1
|
||||
// fix vidoes failing because there are no thumbnails
|
||||
{
|
||||
find: ".handleImageLoad)",
|
||||
replacement: {
|
||||
match: /(componentDidMount\(\){)(.{1,150}===(.+?)\.LOADING)/,
|
||||
replace:
|
||||
"$1if(this.props?.src?.startsWith('blob:') && this.props?.item?.type === 'VIDEO')" +
|
||||
"return this.setState({readyState: $3.READY});$2"
|
||||
}
|
||||
},
|
||||
|
||||
// dont fetch messages for channels in modal
|
||||
{
|
||||
find: "Using PollReferenceMessageContext without",
|
||||
replacement: {
|
||||
match: /(?:\i\.)?\i\.(?:default\.)?focusMessage\(/,
|
||||
replace: "!(arguments[0]?.message?.deleted || arguments[0]?.message?.editHistory?.length > 0) && $&"
|
||||
}
|
||||
},
|
||||
|
||||
// only check for expired attachments if the message is not deleted
|
||||
{
|
||||
find: "\"/ephemeral-attachments/\"",
|
||||
replacement: {
|
||||
match: /\i\.attachments\.some\(\i\)\|\|\i\.embeds\.some/,
|
||||
replace: "!arguments[0].deleted && $&"
|
||||
}
|
||||
}
|
||||
],
|
||||
settings,
|
||||
|
||||
toolboxActions: {
|
||||
"Message Logger"() {
|
||||
openLogModal();
|
||||
}
|
||||
},
|
||||
|
||||
addIconToToolBar(e: { toolbar: React.ReactNode[] | React.ReactNode; }) {
|
||||
if (Array.isArray(e.toolbar))
|
||||
return e.toolbar.push(
|
||||
<ErrorBoundary noop={true}>
|
||||
<OpenLogsButton />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
e.toolbar = [
|
||||
<ErrorBoundary noop={true} key={"MessageLoggerEnhanced"} >
|
||||
<OpenLogsButton />
|
||||
</ErrorBoundary>,
|
||||
e.toolbar,
|
||||
];
|
||||
},
|
||||
|
||||
processMessageFetch,
|
||||
openLogModal,
|
||||
doesMatch,
|
||||
reAddDeletedMessages,
|
||||
LoggedMessageManager,
|
||||
ImageManager,
|
||||
imageUtils,
|
||||
idb,
|
||||
|
||||
coolReAddDeletedMessages: (messages: LoggedMessageJSON[] & { extra: LoggedMessageJSON[]; }, payload: LoadMessagePayload) => {
|
||||
try {
|
||||
if (messages.extra)
|
||||
reAddDeletedMessages(messages, messages.extra, !payload.hasMoreAfter && !payload.isBefore, !payload.hasMoreBefore && !payload.isAfter);
|
||||
}
|
||||
catch (e) {
|
||||
Flogger.error("Failed to re-add deleted messages", e);
|
||||
}
|
||||
finally {
|
||||
return messages;
|
||||
}
|
||||
},
|
||||
|
||||
isDeletedMessage: (id: string) => cacheSentMessages.get(id)?.deleted ?? false,
|
||||
|
||||
getDeleted(m1, m2) {
|
||||
const deleted = m2?.deleted;
|
||||
if (deleted == null && m1?.deleted != null) return m1.deleted;
|
||||
return deleted;
|
||||
},
|
||||
|
||||
getEdited(m1, m2) {
|
||||
const editHistory = m2?.editHistory;
|
||||
if (editHistory == null && m1?.editHistory != null && m1.editHistory.length > 0)
|
||||
return m1.editHistory.map(mapTimestamp);
|
||||
return editHistory;
|
||||
},
|
||||
|
||||
flux: {
|
||||
"MESSAGE_DELETE": messageDeleteHandler as any,
|
||||
"MESSAGE_DELETE_BULK": messageDeleteBulkHandler,
|
||||
"MESSAGE_UPDATE": messageUpdateHandler,
|
||||
"MESSAGE_CREATE": messageCreateHandler
|
||||
},
|
||||
|
||||
async start() {
|
||||
this.oldGetMessage = oldGetMessage = MessageStore.getMessage;
|
||||
|
||||
// we have to do this because the original message logger fetches the message from the store now
|
||||
MessageStore.getMessage = (channelId: string, messageId: string) => {
|
||||
const MLMessage = idb.cachedMessages.get(messageId);
|
||||
if (MLMessage) return messageJsonToMessageClass({ message: MLMessage });
|
||||
|
||||
return this.oldGetMessage(channelId, messageId);
|
||||
};
|
||||
|
||||
Native.init();
|
||||
|
||||
const { imageCacheDir, logsDir } = await Native.getSettings();
|
||||
settings.store.imageCacheDir = imageCacheDir;
|
||||
settings.store.logsDir = logsDir;
|
||||
|
||||
setupContextMenuPatches();
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuBindings();
|
||||
MessageStore.getMessage = this.oldGetMessage;
|
||||
}
|
||||
});
|
|
@ -1,200 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { DATA_DIR } from "@main/utils/constants";
|
||||
import { dialog, IpcMainInvokeEvent, shell } from "electron";
|
||||
|
||||
import { getSettings, saveSettings } from "./settings";
|
||||
export * from "./updater";
|
||||
|
||||
import { LoggedAttachment } from "../types";
|
||||
import { LOGS_DATA_FILENAME } from "../utils/constants";
|
||||
import { ensureDirectoryExists, getAttachmentIdFromFilename, sleep } from "./utils";
|
||||
|
||||
export { getSettings };
|
||||
|
||||
// so we can filter the native helpers by this key
|
||||
export function messageLoggerEnhancedUniqueIdThingyIdkMan() { }
|
||||
|
||||
// Map<attachmetId, path>()
|
||||
const nativeSavedImages = new Map<string, string>();
|
||||
export const getNativeSavedImages = () => nativeSavedImages;
|
||||
|
||||
let logsDir: string;
|
||||
let imageCacheDir: string;
|
||||
|
||||
const getImageCacheDir = async () => imageCacheDir ?? await getDefaultNativeImageDir();
|
||||
const getLogsDir = async () => logsDir ?? await getDefaultNativeDataDir();
|
||||
|
||||
|
||||
|
||||
export async function initDirs() {
|
||||
const { logsDir: ld, imageCacheDir: icd } = await getSettings();
|
||||
|
||||
logsDir = ld || await getDefaultNativeDataDir();
|
||||
imageCacheDir = icd || await getDefaultNativeImageDir();
|
||||
}
|
||||
initDirs();
|
||||
|
||||
export async function init(_event: IpcMainInvokeEvent) {
|
||||
const imageDir = await getImageCacheDir();
|
||||
|
||||
await ensureDirectoryExists(imageDir);
|
||||
const files = await readdir(imageDir);
|
||||
for (const filename of files) {
|
||||
const attachmentId = getAttachmentIdFromFilename(filename);
|
||||
nativeSavedImages.set(attachmentId, path.join(imageDir, filename));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getImageNative(_event: IpcMainInvokeEvent, attachmentId: string): Promise<Uint8Array | Buffer | null> {
|
||||
const imagePath = nativeSavedImages.get(attachmentId);
|
||||
if (!imagePath) return null;
|
||||
|
||||
try {
|
||||
return await readFile(imagePath);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeImageNative(_event: IpcMainInvokeEvent, filename: string, content: Uint8Array) {
|
||||
if (!filename || !content) return;
|
||||
const imageDir = await getImageCacheDir();
|
||||
|
||||
// returns the file name
|
||||
// ../../someMalicousPath.png -> someMalicousPath
|
||||
const attachmentId = getAttachmentIdFromFilename(filename);
|
||||
|
||||
const existingImage = nativeSavedImages.get(attachmentId);
|
||||
if (existingImage) return;
|
||||
|
||||
const imagePath = path.join(imageDir, filename);
|
||||
await ensureDirectoryExists(imageDir);
|
||||
await writeFile(imagePath, content);
|
||||
|
||||
nativeSavedImages.set(attachmentId, imagePath);
|
||||
}
|
||||
|
||||
export async function deleteFileNative(_event: IpcMainInvokeEvent, attachmentId: string) {
|
||||
const imagePath = nativeSavedImages.get(attachmentId);
|
||||
if (!imagePath) return;
|
||||
|
||||
await unlink(imagePath);
|
||||
}
|
||||
|
||||
|
||||
export async function writeLogs(_event: IpcMainInvokeEvent, contents: string) {
|
||||
const logsDir = await getLogsDir();
|
||||
|
||||
writeFile(path.join(logsDir, LOGS_DATA_FILENAME), contents);
|
||||
}
|
||||
|
||||
|
||||
export async function getDefaultNativeImageDir(): Promise<string> {
|
||||
return path.join(await getDefaultNativeDataDir(), "savedImages");
|
||||
}
|
||||
|
||||
export async function getDefaultNativeDataDir(): Promise<string> {
|
||||
return path.join(DATA_DIR, "MessageLoggerData");
|
||||
}
|
||||
|
||||
export async function chooseDir(event: IpcMainInvokeEvent, logKey: "logsDir" | "imageCacheDir") {
|
||||
const settings = await getSettings();
|
||||
const defaultPath = settings[logKey] || await getDefaultNativeDataDir();
|
||||
|
||||
const res = await dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: defaultPath });
|
||||
const dir = res.filePaths[0];
|
||||
|
||||
if (!dir) throw Error("Invalid Directory");
|
||||
|
||||
settings[logKey] = dir;
|
||||
|
||||
await saveSettings(settings);
|
||||
|
||||
switch (logKey) {
|
||||
case "logsDir": logsDir = dir; break;
|
||||
case "imageCacheDir": imageCacheDir = dir; break;
|
||||
}
|
||||
|
||||
if (logKey === "imageCacheDir")
|
||||
await init(event);
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
export async function showItemInFolder(_event: IpcMainInvokeEvent, filePath: string) {
|
||||
shell.showItemInFolder(filePath);
|
||||
}
|
||||
|
||||
export async function chooseFile(_event: IpcMainInvokeEvent, title: string, filters: Electron.FileFilter[], defaultPath?: string) {
|
||||
const res = await dialog.showOpenDialog({ title, filters, properties: ["openFile"], defaultPath });
|
||||
const [path] = res.filePaths;
|
||||
|
||||
if (!path) throw Error("Invalid file");
|
||||
|
||||
return await readFile(path, "utf-8");
|
||||
}
|
||||
|
||||
// doing it in native because you can only fetch images from the renderer
|
||||
// other types of files will cause cors issues
|
||||
export async function downloadAttachment(_event: IpcMainInvokeEvent, attachemnt: LoggedAttachment, attempts = 0, useOldUrl = false): Promise<{ error: string | null; path: string | null; }> {
|
||||
try {
|
||||
if (!attachemnt?.url || !attachemnt.oldUrl || !attachemnt?.id || !attachemnt?.fileExtension)
|
||||
return { error: "Invalid Attachment", path: null };
|
||||
|
||||
if (attachemnt.id.match(/[\\/.]/)) {
|
||||
return { error: "Invalid Attachment ID", path: null };
|
||||
}
|
||||
|
||||
const existingImage = nativeSavedImages.get(attachemnt.id);
|
||||
if (existingImage)
|
||||
return {
|
||||
error: null,
|
||||
path: existingImage
|
||||
};
|
||||
|
||||
const res = await fetch(useOldUrl ? attachemnt.oldUrl : attachemnt.url);
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (res.status === 404 || res.status === 403 || res.status === 415)
|
||||
useOldUrl = true;
|
||||
|
||||
attempts++;
|
||||
if (attempts > 3) {
|
||||
return {
|
||||
error: `Failed to get attachment ${attachemnt.id} for caching. too many attempts, error code ${res.status}`,
|
||||
path: null,
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
return downloadAttachment(_event, attachemnt, attempts, useOldUrl);
|
||||
}
|
||||
|
||||
const ab = await res.arrayBuffer();
|
||||
const imageCacheDir = await getImageCacheDir();
|
||||
await ensureDirectoryExists(imageCacheDir);
|
||||
|
||||
const finalPath = path.join(imageCacheDir, `${attachemnt.id}${attachemnt.fileExtension}`);
|
||||
await writeFile(finalPath, Buffer.from(ab));
|
||||
|
||||
nativeSavedImages.set(attachemnt.id, finalPath);
|
||||
|
||||
return {
|
||||
error: null,
|
||||
path: finalPath
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return { error: error.message, path: null };
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
import { getDefaultNativeDataDir, getDefaultNativeImageDir } from ".";
|
||||
import { ensureDirectoryExists } from "./utils";
|
||||
|
||||
interface MLSettings {
|
||||
logsDir: string;
|
||||
imageCacheDir: string;
|
||||
}
|
||||
export async function getSettings(): Promise<MLSettings> {
|
||||
try {
|
||||
const settings = await fs.readFile(await getSettingsFilePath(), "utf8");
|
||||
return JSON.parse(settings);
|
||||
} catch (err) {
|
||||
// probably doesnt exist
|
||||
// time to create it
|
||||
const settings = {
|
||||
logsDir: await getDefaultNativeDataDir(),
|
||||
imageCacheDir: await getDefaultNativeImageDir(),
|
||||
};
|
||||
try {
|
||||
await saveSettings(settings);
|
||||
} catch (err) { }
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
// dont expose this to renderer future me
|
||||
export async function saveSettings(settings: MLSettings) {
|
||||
if (!settings) return;
|
||||
await fs.writeFile(await getSettingsFilePath(), JSON.stringify(settings, null, 4), "utf8");
|
||||
}
|
||||
|
||||
async function getSettingsFilePath() {
|
||||
// mlSettings.json will always in that folder
|
||||
const MlDataDir = await getDefaultNativeDataDir();
|
||||
await ensureDirectoryExists(MlDataDir);
|
||||
const mlSettingsDir = path.join(MlDataDir, "mlSettings.json");
|
||||
|
||||
return mlSettingsDir;
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { execFile as cpExecFile, ExecFileOptions } from "node:child_process";
|
||||
|
||||
import { readdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import type { GitResult } from "../types";
|
||||
import { memoize } from "../utils/memoize";
|
||||
|
||||
const execFile = promisify(cpExecFile);
|
||||
|
||||
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
||||
|
||||
|
||||
const VENCORD_USER_PLUGIN_DIR = join(__dirname, "..", "src", "userplugins");
|
||||
const getCwd = memoize(async () => {
|
||||
const dirs = await readdir(VENCORD_USER_PLUGIN_DIR, { withFileTypes: true });
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!dir.isDirectory()) continue;
|
||||
|
||||
const pluginDir = join(VENCORD_USER_PLUGIN_DIR, dir.name);
|
||||
const files = await readdir(pluginDir);
|
||||
|
||||
if (files.includes("LoggedMessageManager.ts")) return join(VENCORD_USER_PLUGIN_DIR, dir.name);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
async function git(...args: string[]): Promise<GitResult> {
|
||||
const opts: ExecFileOptions = { cwd: await getCwd(), shell: true };
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (isFlatpak) {
|
||||
result = await execFile("flatpak-spawn", ["--host", "git", ...args], opts);
|
||||
} else {
|
||||
result = await execFile("git", args, opts);
|
||||
}
|
||||
|
||||
return { value: result.stdout.trim(), stderr: result.stderr, ok: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
ok: false,
|
||||
cmd: error.cmd as string,
|
||||
message: error.stderr as string,
|
||||
error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function update() {
|
||||
return await git("pull");
|
||||
}
|
||||
|
||||
export async function getCommitHash() {
|
||||
return await git("rev-parse", "HEAD");
|
||||
}
|
||||
|
||||
export interface GitInfo {
|
||||
repo: string;
|
||||
gitHash: string;
|
||||
}
|
||||
|
||||
export async function getRepoInfo(): Promise<GitResult> {
|
||||
const res = await git("remote", "get-url", "origin");
|
||||
if (!res.ok) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const gitHash = await getCommitHash();
|
||||
if (!gitHash.ok) {
|
||||
return gitHash;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
repo: res.value
|
||||
.replace(/git@(.+):/, "https://$1/")
|
||||
.replace(/\.git$/, ""),
|
||||
gitHash: gitHash.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
hash: string;
|
||||
longHash: string;
|
||||
message: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
export async function getNewCommits(): Promise<GitResult> {
|
||||
const branch = await git("branch", "--show-current");
|
||||
if (!branch.ok) {
|
||||
return branch;
|
||||
}
|
||||
|
||||
const logFormat = "%H;%an;%s";
|
||||
const branchRange = `HEAD..origin/${branch.value}`;
|
||||
|
||||
try {
|
||||
await git("fetch");
|
||||
|
||||
const logOutput = await git("log", `--format="${logFormat}"`, branchRange);
|
||||
|
||||
if (!logOutput.ok) {
|
||||
return logOutput;
|
||||
}
|
||||
|
||||
if (logOutput.value.trim() === "") {
|
||||
return { ok: true, value: [] };
|
||||
}
|
||||
|
||||
const commitLines = logOutput.value.trim().split("\n");
|
||||
const commits: Commit[] = commitLines.map(line => {
|
||||
const [hash, author, ...rest] = line.split(";");
|
||||
return { longHash: hash, hash: hash.slice(0, 7), author, message: rest.join(";") } satisfies Commit;
|
||||
});
|
||||
|
||||
return { ok: true, value: commits };
|
||||
} catch (error: any) {
|
||||
return { ok: false, cmd: error.cmd, message: error.message, error };
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { access, mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export async function exists(filename: string) {
|
||||
try {
|
||||
await access(filename);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDirectoryExists(cacheDir: string) {
|
||||
if (!await exists(cacheDir))
|
||||
await mkdir(cacheDir);
|
||||
}
|
||||
|
||||
export function getAttachmentIdFromFilename(filename: string) {
|
||||
return path.parse(filename).name;
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@ -1,242 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { Alerts, Button } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
import { Native } from ".";
|
||||
import { ImageCacheDir, LogsDir } from "./components/FolderSelectInput";
|
||||
import { openLogModal } from "./components/LogsModal";
|
||||
import { clearMessagesIDB } from "./db";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "./utils/constants";
|
||||
import { exportLogs, importLogs } from "./utils/settingsUtils";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
saveMessages: {
|
||||
default: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Wether to save the deleted and edited messages.",
|
||||
},
|
||||
|
||||
saveImages: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Save deleted attachments.",
|
||||
default: false
|
||||
},
|
||||
|
||||
sortNewest: {
|
||||
default: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Sort logs by newest.",
|
||||
},
|
||||
|
||||
cacheMessagesFromServers: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Usually message logger only logs from whitelisted ids and dms, enabling this would mean it would log messages from all servers as well. Note that this may cause the cache to exceed its limit, resulting in some messages being missed. If you are in a lot of servers, this may significantly increase the chances of messages being logged, which can result in a large message record and the inclusion of irrelevant messages.",
|
||||
},
|
||||
|
||||
ignoreBots: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to ignore messages by bots",
|
||||
default: false,
|
||||
onChange() {
|
||||
// we will be handling the ignoreBots now (enabled or not) so the original messageLogger shouldnt
|
||||
Settings.plugins.MessageLogger.ignoreBots = false;
|
||||
}
|
||||
},
|
||||
|
||||
ignoreWebhooks: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to ignore messages by webhooks",
|
||||
default: false,
|
||||
},
|
||||
|
||||
ignoreSelf: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to ignore messages by yourself",
|
||||
default: false,
|
||||
onChange() {
|
||||
Settings.plugins.MessageLogger.ignoreSelf = false;
|
||||
}
|
||||
},
|
||||
|
||||
ignoreMutedGuilds: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Messages in muted guilds will not be logged. Whitelisted users/channels in muted guilds will still be logged."
|
||||
},
|
||||
|
||||
ignoreMutedCategories: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Messages in channels belonging to muted categories will not be logged. Whitelisted users/channels in muted guilds will still be logged."
|
||||
},
|
||||
|
||||
ignoreMutedChannels: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Messages in muted channels will not be logged. Whitelisted users/channels in muted guilds will still be logged."
|
||||
},
|
||||
|
||||
alwaysLogDirectMessages: {
|
||||
default: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Always log DMs",
|
||||
},
|
||||
|
||||
alwaysLogCurrentChannel: {
|
||||
default: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Always log current selected channel. Blacklisted channels/users will still be ignored.",
|
||||
},
|
||||
|
||||
permanentlyRemoveLogByDefault: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Vencord's base MessageLogger remove log button wiil delete logs permanently",
|
||||
},
|
||||
|
||||
hideMessageFromMessageLoggers: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "When enabled, a context menu button will be added to messages to allow you to delete messages without them being logged by other loggers. Might not be safe, use at your own risk."
|
||||
},
|
||||
|
||||
ShowLogsButton: {
|
||||
default: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Toggle to whenever show the toolbox or not",
|
||||
restartNeeded: true,
|
||||
},
|
||||
|
||||
messagesToDisplayAtOnceInLogs: {
|
||||
default: 100,
|
||||
type: OptionType.NUMBER,
|
||||
description: "Number of messages to display at once in logs & number of messages to load when loading more messages in logs.",
|
||||
},
|
||||
|
||||
hideMessageFromMessageLoggersDeletedMessage: {
|
||||
default: "redacted eh",
|
||||
type: OptionType.STRING,
|
||||
description: "The message content to replace the message with when using the hide message from message loggers feature.",
|
||||
},
|
||||
|
||||
messageLimit: {
|
||||
default: 200,
|
||||
type: OptionType.NUMBER,
|
||||
description: "Maximum number of messages to save. Older messages are deleted when the limit is reached. 0 means there is no limit"
|
||||
},
|
||||
|
||||
attachmentSizeLimitInMegabytes: {
|
||||
default: 12,
|
||||
type: OptionType.NUMBER,
|
||||
description: "Maximum size of an attachment in megabytes to save. Attachments larger than this size will not be saved."
|
||||
},
|
||||
|
||||
attachmentFileExtensions: {
|
||||
default: "png,jpg,jpeg,gif,webp,mp4,webm,mp3,ogg,wav",
|
||||
type: OptionType.STRING,
|
||||
description: "Comma separated list of file extensions to save. Attachments with file extensions not in this list will not be saved. Leave empty to save all attachments."
|
||||
},
|
||||
|
||||
cacheLimit: {
|
||||
default: 1000,
|
||||
type: OptionType.NUMBER,
|
||||
description: "Maximum number of messages to store in the cache. Older messages are deleted when the limit is reached. This helps reduce memory usage and improve performance. 0 means there is no limit",
|
||||
},
|
||||
|
||||
whitelistedIds: {
|
||||
default: "",
|
||||
type: OptionType.STRING,
|
||||
description: "Whitelisted server, channel, or user IDs."
|
||||
},
|
||||
|
||||
blacklistedIds: {
|
||||
default: "",
|
||||
type: OptionType.STRING,
|
||||
description: "Blacklisted server, channel, or user IDs."
|
||||
},
|
||||
|
||||
imageCacheDir: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Select saved images directory",
|
||||
component: ErrorBoundary.wrap(ImageCacheDir) as any
|
||||
},
|
||||
|
||||
logsDir: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Select logs directory",
|
||||
component: ErrorBoundary.wrap(LogsDir) as any
|
||||
},
|
||||
|
||||
importLogs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Import Logs From File",
|
||||
component: () =>
|
||||
<Button onClick={importLogs}>
|
||||
Import Logs
|
||||
</Button>
|
||||
},
|
||||
|
||||
exportLogs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Export Logs From IndexedDB",
|
||||
component: () =>
|
||||
<Button onClick={exportLogs}>
|
||||
Export Logs
|
||||
</Button>
|
||||
},
|
||||
|
||||
openLogs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Open Logs",
|
||||
component: () =>
|
||||
<Button onClick={() => openLogModal()}>
|
||||
Open Logs
|
||||
</Button>
|
||||
},
|
||||
openImageCacheFolder: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Opens the image cache directory",
|
||||
component: () =>
|
||||
<Button
|
||||
disabled={
|
||||
IS_WEB
|
||||
|| settings.store.imageCacheDir == null
|
||||
|| settings.store.imageCacheDir === DEFAULT_IMAGE_CACHE_DIR
|
||||
}
|
||||
onClick={() => Native.showItemInFolder(settings.store.imageCacheDir)}
|
||||
>
|
||||
Open Image Cache Folder
|
||||
</Button>
|
||||
},
|
||||
|
||||
clearLogs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Clear Logs",
|
||||
component: () =>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Clear Logs",
|
||||
body: "Are you sure you want to clear all logs?",
|
||||
confirmColor: Button.Colors.RED,
|
||||
confirmText: "Clear",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: () => {
|
||||
clearMessagesIDB();
|
||||
},
|
||||
})}
|
||||
>
|
||||
Clear Logs
|
||||
</Button>
|
||||
},
|
||||
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
.msg-logger-modal-root {
|
||||
max-height: 80vh;
|
||||
min-height: 80vh;
|
||||
min-width: 85vw;
|
||||
max-width: 85vw;
|
||||
}
|
||||
|
||||
.msg-logger-modal-empty-logs {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.msg-logger-modal-info-icon {
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
right: -24px;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.msg-logger-modal-header {
|
||||
flex-direction: column;
|
||||
|
||||
/* width: 100%; */
|
||||
}
|
||||
|
||||
.msg-logger-modal-content-container {
|
||||
/* max-height: 80vh; */
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
transition: opacity 100ms ease-in;
|
||||
}
|
||||
|
||||
.msg-logger-modal-content {
|
||||
padding-bottom: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.msg-logger-modal-content-inner {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.msg-logger-modal-header > div:has(input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.msg-logger-modal-tab-bar-item {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.msg-logger-modal-tab-bar {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.vc-log-toolbox-btn svg {
|
||||
color: var(--interactive-normal);
|
||||
}
|
||||
|
||||
:is(.vc-log-toolbox-btn:hover, .vc-log-toolbox-btn[class*="selected"]) svg {
|
||||
color: var(--interactive-active);
|
||||
}
|
||||
|
||||
.folder-upload-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.folder-upload-input {
|
||||
cursor: pointer;
|
||||
padding: 10px 0 10px 10px;
|
||||
color: var(--header-secondary);
|
||||
}
|
||||
|
||||
.folder-upload-button {
|
||||
margin: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { Message, MessageAttachment, MessageJSON } from "discord-types/general";
|
||||
|
||||
export interface LoggedAttachment extends MessageAttachment {
|
||||
fileExtension?: string | null;
|
||||
path?: string | null;
|
||||
blobUrl?: string;
|
||||
nativefileSystem?: boolean;
|
||||
oldUrl?: string;
|
||||
oldProxyUrl?: string;
|
||||
}
|
||||
|
||||
export type RefrencedMessage = LoggedMessageJSON & { message_id: string; };
|
||||
export interface LoggedMessageJSON extends Omit<LoggedMessage, "timestamp"> {
|
||||
mention_everyone?: string;
|
||||
guildId?: string;
|
||||
guild_id?: string;
|
||||
ghostPinged?: boolean;
|
||||
timestamp: string;
|
||||
ourCache?: boolean;
|
||||
referenced_message: RefrencedMessage;
|
||||
message_reference: RefrencedMessage;
|
||||
}
|
||||
|
||||
export interface LoggedMessage extends Message {
|
||||
attachments: LoggedAttachment[];
|
||||
deleted?: boolean;
|
||||
deletedTimestamp?: string;
|
||||
editHistory?: {
|
||||
timestamp: string;
|
||||
content: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface MessageDeletePayload {
|
||||
type: string;
|
||||
guildId: string;
|
||||
id: string;
|
||||
channelId: string;
|
||||
mlDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageDeleteBulkPayload {
|
||||
type: string;
|
||||
guildId: string;
|
||||
ids: string[];
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface MessageUpdatePayload {
|
||||
type: string;
|
||||
guildId: string;
|
||||
message: MessageJSON;
|
||||
}
|
||||
|
||||
export interface MessageCreatePayload {
|
||||
type: string;
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
message: MessageJSON;
|
||||
optimistic: boolean;
|
||||
isPushNotification: boolean;
|
||||
}
|
||||
|
||||
export interface LoadMessagePayload {
|
||||
type: string;
|
||||
channelId: string;
|
||||
messages: LoggedMessageJSON[];
|
||||
isBefore: boolean;
|
||||
isAfter: boolean;
|
||||
hasMoreBefore: boolean;
|
||||
hasMoreAfter: boolean;
|
||||
limit: number;
|
||||
isStale: boolean;
|
||||
}
|
||||
|
||||
export interface FetchMessagesResponse {
|
||||
ok: boolean;
|
||||
headers: Headers;
|
||||
body: LoggedMessageJSON[] & {
|
||||
extra?: LoggedMessageJSON[];
|
||||
};
|
||||
text: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface PatchAttachmentItem {
|
||||
uniqueId: string;
|
||||
originalItem: LoggedAttachment;
|
||||
type: string;
|
||||
downloadUrl: string;
|
||||
height: number;
|
||||
width: number;
|
||||
spoiler: boolean;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface AttachmentData {
|
||||
messageId: string;
|
||||
attachmentId: string;
|
||||
}
|
||||
|
||||
export type SavedImages = Record<string, AttachmentData>;
|
||||
|
||||
export type LoggedMessageIds = {
|
||||
// [channel_id: string]: message_id
|
||||
deletedMessages: Record<string, string[]>;
|
||||
editedMessages: Record<string, string[]>;
|
||||
};
|
||||
|
||||
export type MessageRecord = { message: LoggedMessageJSON; };
|
||||
|
||||
export type LoggedMessages = LoggedMessageIds & { [message_id: string]: { message?: LoggedMessageJSON; }; };
|
||||
|
||||
export type GitValue = {
|
||||
value: any;
|
||||
stderr?: string;
|
||||
ok: true;
|
||||
};
|
||||
|
||||
export type GitError = {
|
||||
ok: false;
|
||||
cmd: string;
|
||||
message: string;
|
||||
error: any;
|
||||
};
|
||||
|
||||
export type GitResult = GitValue | GitError;
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { settings } from "../index";
|
||||
|
||||
export class LimitedMap<K, V> {
|
||||
public map: Map<K, V> = new Map();
|
||||
constructor() { }
|
||||
|
||||
set(key: K, value: V) {
|
||||
if (settings.store.cacheLimit > 0 && this.map.size >= settings.store.cacheLimit) {
|
||||
const firstKey = this.map.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.map.delete(firstKey);
|
||||
}
|
||||
}
|
||||
this.map.set(key, value);
|
||||
}
|
||||
|
||||
get(key: K) {
|
||||
return this.map.get(key);
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { MessageStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import { LoggedMessageJSON, RefrencedMessage } from "../types";
|
||||
import { getGuildIdByChannel, isGhostPinged } from "./index";
|
||||
|
||||
export function cleanupMessage(message: any, removeDetails: boolean = true): LoggedMessageJSON {
|
||||
const ret: LoggedMessageJSON = typeof message.toJS === "function" ? JSON.parse(JSON.stringify(message.toJS())) : { ...message };
|
||||
if (removeDetails) {
|
||||
ret.author.phone = undefined;
|
||||
ret.author.email = undefined;
|
||||
}
|
||||
|
||||
ret.ghostPinged = ret.mentioned ?? isGhostPinged(message);
|
||||
ret.guildId = ret.guild_id ?? getGuildIdByChannel(ret.channel_id);
|
||||
ret.embeds = (ret.embeds ?? []).map(cleanupEmbed);
|
||||
ret.deleted = ret.deleted ?? false;
|
||||
ret.deletedTimestamp = ret.deleted ? (new Date()).toISOString() : undefined;
|
||||
ret.editHistory = ret.editHistory ?? [];
|
||||
if (ret.type === 19) {
|
||||
ret.message_reference = message.message_reference || message.messageReference;
|
||||
if (ret.message_reference) {
|
||||
if (message.referenced_message) {
|
||||
ret.referenced_message = cleanupMessage(message.referenced_message) as RefrencedMessage;
|
||||
} else if (MessageStore.getMessage(ret.message_reference.channel_id, ret.message_reference.message_id)) {
|
||||
ret.referenced_message = cleanupMessage(MessageStore.getMessage(ret.message_reference.channel_id, ret.message_reference.message_id)) as RefrencedMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function cleanUpCachedMessage(message: any) {
|
||||
const ret = cleanupMessage(message, false);
|
||||
ret.ourCache = true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// stolen from mlv2
|
||||
export function cleanupEmbed(embed) {
|
||||
/* backported code from MLV2 rewrite */
|
||||
if (!embed.id) return embed; /* already cleaned */
|
||||
const retEmbed: any = {};
|
||||
if (typeof embed.rawTitle === "string") retEmbed.title = embed.rawTitle;
|
||||
if (typeof embed.rawDescription === "string") retEmbed.description = embed.rawDescription;
|
||||
if (typeof embed.referenceId !== "undefined") retEmbed.reference_id = embed.referenceId;
|
||||
// if (typeof embed.color === "string") retEmbed.color = ZeresPluginLibrary.ColorConverter.hex2int(embed.color);
|
||||
if (typeof embed.type !== "undefined") retEmbed.type = embed.type;
|
||||
if (typeof embed.url !== "undefined") retEmbed.url = embed.url;
|
||||
if (typeof embed.provider === "object") retEmbed.provider = { name: embed.provider.name, url: embed.provider.url };
|
||||
if (typeof embed.footer === "object") retEmbed.footer = { text: embed.footer.text, icon_url: embed.footer.iconURL, proxy_icon_url: embed.footer.iconProxyURL };
|
||||
if (typeof embed.author === "object") retEmbed.author = { name: embed.author.name, url: embed.author.url, icon_url: embed.author.iconURL, proxy_icon_url: embed.author.iconProxyURL };
|
||||
if (typeof embed.timestamp === "object" && embed.timestamp._isAMomentObject) retEmbed.timestamp = embed.timestamp.milliseconds();
|
||||
if (typeof embed.thumbnail === "object") {
|
||||
if (typeof embed.thumbnail.proxyURL === "string" || (typeof embed.thumbnail.url === "string" && !embed.thumbnail.url.endsWith("?format=jpeg"))) {
|
||||
retEmbed.thumbnail = {
|
||||
url: embed.thumbnail.url,
|
||||
proxy_url: typeof embed.thumbnail.proxyURL === "string" ? embed.thumbnail.proxyURL.split("?format")[0] : undefined,
|
||||
width: embed.thumbnail.width,
|
||||
height: embed.thumbnail.height
|
||||
};
|
||||
}
|
||||
}
|
||||
if (typeof embed.image === "object") {
|
||||
retEmbed.image = {
|
||||
url: embed.image.url,
|
||||
proxy_url: embed.image.proxyURL,
|
||||
width: embed.image.width,
|
||||
height: embed.image.height
|
||||
};
|
||||
}
|
||||
if (typeof embed.video === "object") {
|
||||
retEmbed.video = {
|
||||
url: embed.video.url,
|
||||
proxy_url: embed.video.proxyURL,
|
||||
width: embed.video.width,
|
||||
height: embed.video.height
|
||||
};
|
||||
}
|
||||
if (Array.isArray(embed.fields) && embed.fields.length) {
|
||||
retEmbed.fields = embed.fields.map(e => ({ name: e.rawName, value: e.rawValue, inline: e.inline }));
|
||||
}
|
||||
return retEmbed;
|
||||
}
|
||||
|
||||
// stolen from mlv2
|
||||
export function cleanupUserObject(user: User) {
|
||||
/* backported from MLV2 rewrite */
|
||||
return {
|
||||
discriminator: user.discriminator,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
id: user.id,
|
||||
bot: user.bot,
|
||||
public_flags: typeof user.publicFlags !== "undefined" ? user.publicFlags : (user as any).public_flags
|
||||
};
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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/>.
|
||||
*/
|
||||
|
||||
export const DEFAULT_IMAGE_CACHE_DIR = "savedImages";
|
||||
|
||||
export const DB_NAME = "MessageLoggerIDB";
|
||||
export const DB_VERSION = 1;
|
||||
export const LOGS_DATA_FILENAME = "message-logger-logs.json";
|
|
@ -1,171 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { FluxDispatcher, Menu, MessageActions, React, Toasts, UserStore } from "@webpack/common";
|
||||
|
||||
import { openLogModal } from "../components/LogsModal";
|
||||
import { deleteMessageIDB } from "../db";
|
||||
import { settings } from "../index";
|
||||
import { addToXAndRemoveFromOpposite, ListType, removeFromX } from ".";
|
||||
|
||||
const idFunctions = {
|
||||
Server: props => props?.guild?.id,
|
||||
User: props => props?.message?.author?.id || props?.user?.id,
|
||||
Channel: props => props.message?.channel_id || props.channel?.id
|
||||
} as const;
|
||||
|
||||
type idKeys = keyof typeof idFunctions;
|
||||
|
||||
function renderListOption(listType: ListType, IdType: idKeys, props: any) {
|
||||
const id = idFunctions[IdType](props);
|
||||
if (!id) return null;
|
||||
|
||||
const isBlocked = settings.store[listType].includes(id);
|
||||
const oppositeListType = listType === "blacklistedIds" ? "whitelistedIds" : "blacklistedIds";
|
||||
const isOppositeBlocked = settings.store[oppositeListType].includes(id);
|
||||
const list = listType === "blacklistedIds" ? "Blacklist" : "Whitelist";
|
||||
|
||||
const addToList = () => addToXAndRemoveFromOpposite(listType, id);
|
||||
const removeFromList = () => removeFromX(listType, id);
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={`${listType}-${IdType}-${id}`}
|
||||
label={
|
||||
isOppositeBlocked
|
||||
? `Move ${IdType} to ${list}`
|
||||
: isBlocked ? `Remove ${IdType} From ${list}` : `${list} ${IdType}`
|
||||
}
|
||||
action={isBlocked ? removeFromList : addToList}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOpenLogs(idType: idKeys, props: any) {
|
||||
const id = idFunctions[idType](props);
|
||||
if (!id) return null;
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={`open-logs-for-${idType.toLowerCase()}`}
|
||||
label={`Open Logs For ${idType}`}
|
||||
action={() => openLogModal(`${idType.toLowerCase()}:${id}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const contextMenuPath: NavContextMenuPatchCallback = (children, props) => {
|
||||
if (!props) return;
|
||||
|
||||
if (!children.some(child => child?.props?.id === "message-logger")) {
|
||||
children.push(
|
||||
<Menu.MenuSeparator />,
|
||||
<Menu.MenuItem
|
||||
id="message-logger"
|
||||
label="Message Logger"
|
||||
>
|
||||
|
||||
<Menu.MenuItem
|
||||
id="open-logs"
|
||||
label="Open Logs"
|
||||
action={() => openLogModal()}
|
||||
/>
|
||||
|
||||
{Object.keys(idFunctions).map(IdType => renderOpenLogs(IdType as idKeys, props))}
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
{Object.keys(idFunctions).map(IdType => (
|
||||
<React.Fragment key={IdType}>
|
||||
{renderListOption("blacklistedIds", IdType as idKeys, props)}
|
||||
{renderListOption("whitelistedIds", IdType as idKeys, props)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{
|
||||
props.navId === "message"
|
||||
&& (props.message?.deleted || props.message?.editHistory?.length > 0)
|
||||
&& (
|
||||
<>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
id="remove-message"
|
||||
label={props.message?.deleted ? "Remove Message (Permanent)" : "Remove Message History (Permanent)"}
|
||||
color="danger"
|
||||
action={() =>
|
||||
deleteMessageIDB(props.message.id)
|
||||
.then(() => {
|
||||
if (props.message.deleted) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: props.message.channel_id,
|
||||
id: props.message.id,
|
||||
mlDeleted: true
|
||||
});
|
||||
} else {
|
||||
props.message.editHistory = [];
|
||||
}
|
||||
}).catch(() => Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to remove message",
|
||||
id: Toasts.genId()
|
||||
}))
|
||||
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
settings.store.hideMessageFromMessageLoggers
|
||||
&& props.navId === "message"
|
||||
&& props.message?.author?.id === UserStore.getCurrentUser().id
|
||||
&& props.message?.deleted === false
|
||||
&& (
|
||||
<>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
id="hide-from-message-loggers"
|
||||
label="Delete Message (Hide From Message Loggers)"
|
||||
color="danger"
|
||||
|
||||
action={async () => {
|
||||
await MessageActions.deleteMessage(props.message.channel_id, props.message.id);
|
||||
MessageActions._sendMessage(props.message.channel_id, {
|
||||
"content": settings.store.hideMessageFromMessageLoggersDeletedMessage,
|
||||
"tts": false,
|
||||
"invalidEmojis": [],
|
||||
"validNonShortcutEmojis": []
|
||||
}, { nonce: props.message.id });
|
||||
}}
|
||||
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupContextMenuPatches = () => {
|
||||
addContextMenuPatch("message", contextMenuPath);
|
||||
addContextMenuPatch("channel-context", contextMenuPath);
|
||||
addContextMenuPatch("user-context", contextMenuPath);
|
||||
addContextMenuPatch("guild-context", contextMenuPath);
|
||||
addContextMenuPatch("gdm-context", contextMenuPath);
|
||||
};
|
||||
|
||||
export const removeContextMenuBindings = () => {
|
||||
removeContextMenuPatch("message", contextMenuPath);
|
||||
removeContextMenuPatch("channel-context", contextMenuPath);
|
||||
removeContextMenuPatch("user-context", contextMenuPath);
|
||||
removeContextMenuPatch("guild-context", contextMenuPath);
|
||||
removeContextMenuPatch("gdm-context", contextMenuPath);
|
||||
};
|
|
@ -1,185 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
|
||||
import { settings } from "../index";
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
import { findLastIndex, getGuildIdByChannel } from "./misc";
|
||||
|
||||
export * from "./cleanUp";
|
||||
export * from "./misc";
|
||||
|
||||
|
||||
// stolen from mlv2
|
||||
// https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js#L2367
|
||||
interface Id { id: string, time: number; message?: LoggedMessageJSON; }
|
||||
export const DISCORD_EPOCH = 14200704e5;
|
||||
export function reAddDeletedMessages(messages: LoggedMessageJSON[], deletedMessages: LoggedMessageJSON[], channelStart: boolean, channelEnd: boolean) {
|
||||
if (!messages.length || !deletedMessages?.length) return;
|
||||
const IDs: Id[] = [];
|
||||
const savedIDs: Id[] = [];
|
||||
|
||||
for (let i = 0, len = messages.length; i < len; i++) {
|
||||
const { id } = messages[i];
|
||||
IDs.push({ id: id, time: (parseInt(id) / 4194304) + DISCORD_EPOCH });
|
||||
}
|
||||
for (let i = 0, len = deletedMessages.length; i < len; i++) {
|
||||
const record = deletedMessages[i];
|
||||
if (!record) continue;
|
||||
savedIDs.push({ id: record.id, time: (parseInt(record.id) / 4194304) + DISCORD_EPOCH, message: record });
|
||||
}
|
||||
|
||||
savedIDs.sort((a, b) => a.time - b.time);
|
||||
if (!savedIDs.length) return;
|
||||
const { time: lowestTime } = IDs[IDs.length - 1];
|
||||
const [{ time: highestTime }] = IDs;
|
||||
const lowestIDX = channelEnd ? 0 : savedIDs.findIndex(e => e.time > lowestTime);
|
||||
if (lowestIDX === -1) return;
|
||||
const highestIDX = channelStart ? savedIDs.length - 1 : findLastIndex(savedIDs, e => e.time < highestTime);
|
||||
if (highestIDX === -1) return;
|
||||
const reAddIDs = savedIDs.slice(lowestIDX, highestIDX + 1);
|
||||
reAddIDs.push(...IDs);
|
||||
reAddIDs.sort((a, b) => b.time - a.time);
|
||||
for (let i = 0, len = reAddIDs.length; i < len; i++) {
|
||||
const { id, message } = reAddIDs[i];
|
||||
if (messages.findIndex(e => e.id === id) !== -1) continue;
|
||||
if (!message) continue;
|
||||
messages.splice(i, 0, message);
|
||||
}
|
||||
}
|
||||
|
||||
interface ShouldIgnoreArguments {
|
||||
channelId?: string,
|
||||
authorId?: string,
|
||||
guildId?: string;
|
||||
flags?: number,
|
||||
bot?: boolean;
|
||||
ghostPinged?: boolean;
|
||||
isCachedByUs?: boolean;
|
||||
webhookId?: string;
|
||||
}
|
||||
|
||||
const EPHEMERAL = 64;
|
||||
|
||||
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
|
||||
|
||||
/**
|
||||
* the function `shouldIgnore` evaluates whether a message should be ignored or kept, following a priority hierarchy: User > Channel > Server.
|
||||
* In this hierarchy, whitelisting takes priority; if any element (User, Channel, or Server) is whitelisted, the message is kept.
|
||||
* However, if a higher-priority element, like a User, is blacklisted, it will override the whitelisting status of a lower-priority element, such as a Server, causing the message to be ignored.
|
||||
* @param {ShouldIgnoreArguments} args - An object containing the message details.
|
||||
* @returns {boolean} - True if the message should be ignored, false if it should be kept.
|
||||
*/
|
||||
export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs, webhookId }: ShouldIgnoreArguments): boolean {
|
||||
const isEphemeral = ((flags ?? 0) & EPHEMERAL) === EPHEMERAL;
|
||||
if (isEphemeral) return true; // ignore
|
||||
|
||||
if (channelId && guildId == null)
|
||||
guildId = getGuildIdByChannel(channelId);
|
||||
|
||||
const myId = UserStore.getCurrentUser().id;
|
||||
const { ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
||||
const { ignoreBots, ignoreSelf, ignoreWebhooks } = settings.store;
|
||||
|
||||
if (ignoreSelf && authorId === myId)
|
||||
return true; // ignore
|
||||
if (settings.store.alwaysLogDirectMessages && ChannelStore.getChannel(channelId ?? "-1")?.isDM?.())
|
||||
return false; // keep
|
||||
|
||||
const shouldLogCurrentChannel = settings.store.alwaysLogCurrentChannel && SelectedChannelStore.getChannelId() === channelId;
|
||||
|
||||
const ids = [authorId, channelId, guildId];
|
||||
|
||||
const whitelistedIds = settings.store.whitelistedIds.split(",");
|
||||
|
||||
const isWhitelisted = settings.store.whitelistedIds.split(",").some(e => ids.includes(e));
|
||||
const isAuthorWhitelisted = whitelistedIds.includes(authorId!);
|
||||
const isChannelWhitelisted = whitelistedIds.includes(channelId!);
|
||||
const isGuildWhitelisted = whitelistedIds.includes(guildId!);
|
||||
|
||||
const blacklistedIds = [
|
||||
...settings.store.blacklistedIds.split(","),
|
||||
...(ignoreUsers ?? []).split(","),
|
||||
...(ignoreChannels ?? []).split(","),
|
||||
...(ignoreGuilds ?? []).split(",")
|
||||
];
|
||||
|
||||
const isBlacklisted = blacklistedIds.some(e => ids.includes(e));
|
||||
const isAuthorBlacklisted = blacklistedIds.includes(authorId);
|
||||
const isChannelBlacklisted = blacklistedIds.includes(channelId);
|
||||
|
||||
|
||||
const shouldIgnoreMutedGuilds = settings.store.ignoreMutedGuilds;
|
||||
const shouldIgnoreMutedCategories = settings.store.ignoreMutedCategories;
|
||||
const shouldIgnoreMutedChannels = settings.store.ignoreMutedChannels;
|
||||
|
||||
if ((ignoreBots && bot) && !isAuthorWhitelisted) return true; // ignore
|
||||
|
||||
if ((ignoreWebhooks && webhookId) && !isAuthorWhitelisted) return true;
|
||||
|
||||
if (ghostPinged) return false; // keep
|
||||
|
||||
// author has highest priority
|
||||
if (isAuthorWhitelisted) return false; // keep
|
||||
if (isAuthorBlacklisted) return true; // ignore
|
||||
|
||||
if (isChannelWhitelisted) return false; // keep
|
||||
if (isChannelBlacklisted) return true; // ignore
|
||||
|
||||
if (shouldLogCurrentChannel) return false; // keep
|
||||
|
||||
if (isWhitelisted) return false; // keep
|
||||
|
||||
if (isCachedByUs && (!settings.store.cacheMessagesFromServers && guildId != null && !isGuildWhitelisted)) return true; // ignore
|
||||
|
||||
if (isBlacklisted && (!isAuthorWhitelisted || !isChannelWhitelisted)) return true; // ignore
|
||||
|
||||
if (guildId != null && shouldIgnoreMutedGuilds && UserGuildSettingsStore.isMuted(guildId)) return true; // ignore
|
||||
if (channelId != null && shouldIgnoreMutedCategories && UserGuildSettingsStore.isCategoryMuted(guildId, channelId)) return true; // ignore
|
||||
if (channelId != null && shouldIgnoreMutedChannels && UserGuildSettingsStore.isChannelMuted(guildId, channelId)) return true; // ignore
|
||||
|
||||
return false; // keep;
|
||||
}
|
||||
|
||||
export type ListType = "blacklistedIds" | "whitelistedIds";
|
||||
|
||||
export function addToXAndRemoveFromOpposite(list: ListType, id: string) {
|
||||
const oppositeListType = list === "blacklistedIds" ? "whitelistedIds" : "blacklistedIds";
|
||||
removeFromX(oppositeListType, id);
|
||||
|
||||
addToX(list, id);
|
||||
}
|
||||
|
||||
export function addToX(list: ListType, id: string) {
|
||||
const items = settings.store[list] ? settings.store[list].split(",") : [];
|
||||
items.push(id);
|
||||
|
||||
settings.store[list] = items.join(",");
|
||||
}
|
||||
|
||||
export function removeFromX(list: ListType, id: string) {
|
||||
const items = settings.store[list] ? settings.store[list].split(",") : [];
|
||||
const index = items.indexOf(id);
|
||||
if (index !== -1) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
settings.store[list] = items.join(",");
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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/>.
|
||||
*/
|
||||
|
||||
type MemoizedFunction<T extends (...args: any[]) => any> = {
|
||||
(...args: Parameters<T>): ReturnType<T>;
|
||||
clear(): void;
|
||||
};
|
||||
|
||||
export function memoize<T extends (...args: any[]) => any>(func: T): MemoizedFunction<T> {
|
||||
const cache = new Map<string, ReturnType<T>>();
|
||||
|
||||
const memoizedFunc = (...args: Parameters<T>): ReturnType<T> => {
|
||||
const key = JSON.stringify(args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key)!;
|
||||
}
|
||||
|
||||
const result = func(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
memoizedFunc.clear = () => cache.clear();
|
||||
|
||||
return memoizedFunc;
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { PluginNative } from "@utils/types";
|
||||
import { findByCodeLazy, findLazy } from "@webpack";
|
||||
import { ChannelStore, moment, UserStore } from "@webpack/common";
|
||||
|
||||
import { DBMessageStatus } from "../db";
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "./constants";
|
||||
import { DISCORD_EPOCH } from "./index";
|
||||
import { memoize } from "./memoize";
|
||||
|
||||
const MessageClass: any = findLazy(m => m?.prototype?.isEdited);
|
||||
const AuthorClass = findLazy(m => m?.prototype?.getAvatarURL);
|
||||
const sanitizeEmbed = findByCodeLazy('"embed_"),');
|
||||
|
||||
export function getGuildIdByChannel(channel_id: string) {
|
||||
return ChannelStore.getChannel(channel_id)?.guild_id;
|
||||
}
|
||||
|
||||
export const isGhostPinged = (message?: LoggedMessageJSON) => {
|
||||
return message?.ghostPinged || message?.deleted && hasPingged(message);
|
||||
|
||||
};
|
||||
|
||||
export const hasPingged = (message?: LoggedMessageJSON | { mention_everyone: boolean, mentions: any[]; }) => {
|
||||
return message && !!(
|
||||
message.mention_everyone ||
|
||||
message.mentions?.find(m => (typeof m === "string" ? m : m.id) === UserStore.getCurrentUser().id)
|
||||
);
|
||||
};
|
||||
|
||||
export const getMessageStatus = (message: LoggedMessageJSON) => {
|
||||
if (isGhostPinged(message)) return DBMessageStatus.GHOST_PINGED;
|
||||
if (message.deleted) return DBMessageStatus.DELETED;
|
||||
if (message.editHistory?.length) return DBMessageStatus.EDITED;
|
||||
|
||||
throw new Error("Unknown message status");
|
||||
};
|
||||
|
||||
export const discordIdToDate = (id: string) => new Date((parseInt(id) / 4194304) + DISCORD_EPOCH);
|
||||
|
||||
export const sortMessagesByDate = (timestampA: string, timestampB: string) => {
|
||||
// very expensive
|
||||
// const timestampA = discordIdToDate(a).getTime();
|
||||
// const timestampB = discordIdToDate(b).getTime();
|
||||
// return timestampB - timestampA;
|
||||
|
||||
// newest first
|
||||
if (timestampA < timestampB) {
|
||||
return 1;
|
||||
} else if (timestampA > timestampB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// stolen from mlv2
|
||||
export function findLastIndex<T>(array: T[], predicate: (e: T, t: number, n: T[]) => boolean) {
|
||||
let l = array.length;
|
||||
while (l--) {
|
||||
if (predicate(array[l], l, array))
|
||||
return l;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const getTimestamp = (timestamp: any): Date => {
|
||||
return new Date(timestamp);
|
||||
};
|
||||
|
||||
export const mapTimestamp = (m: any) => {
|
||||
if (m.timestamp) m.timestamp = getTimestamp(m.timestamp);
|
||||
if (m.editedTimestamp) m.editedTimestamp = getTimestamp(m.editedTimestamp);
|
||||
if (m.embeds) m.embeds = m.embeds.map(e => sanitizeEmbed(m.channel_id, m.id, e));
|
||||
return m;
|
||||
};
|
||||
|
||||
|
||||
export const messageJsonToMessageClass = memoize((log: { message: LoggedMessageJSON; }) => {
|
||||
// console.time("message populate");
|
||||
if (!log?.message) return null;
|
||||
|
||||
const message: any = new MessageClass(log.message);
|
||||
message.timestamp = getTimestamp(message.timestamp);
|
||||
|
||||
const editHistory = message.editHistory?.map(mapTimestamp);
|
||||
if (editHistory && editHistory.length > 0) {
|
||||
message.editHistory = editHistory;
|
||||
}
|
||||
if (message.editedTimestamp)
|
||||
message.editedTimestamp = getTimestamp(message.editedTimestamp);
|
||||
|
||||
if (message.firstEditTimestamp)
|
||||
message.firstEditTimestamp = getTimestamp(message.firstEditTimestamp);
|
||||
|
||||
message.author = UserStore.getUser(message.author.id) ?? new AuthorClass(message.author);
|
||||
message.author.nick = message.author.globalName ?? message.author.username;
|
||||
|
||||
message.embeds = message.embeds.map(e => sanitizeEmbed(message.channel_id, message.id, e));
|
||||
|
||||
if (message.poll)
|
||||
message.poll.expiry = moment(message.poll.expiry);
|
||||
|
||||
if (message.messageSnapshots)
|
||||
message.messageSnapshots.map(m => mapTimestamp(m.message));
|
||||
|
||||
// console.timeEnd("message populate");
|
||||
return message;
|
||||
});
|
||||
|
||||
|
||||
export function parseJSON(json?: string | null) {
|
||||
try {
|
||||
return JSON.parse(json!);
|
||||
} finally {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function doesBlobUrlExist(url: string) {
|
||||
const res = await fetch(url);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
export function getNative(): PluginNative<typeof import("../native")> {
|
||||
if (IS_WEB) {
|
||||
const Native = {
|
||||
writeLogs: async () => { },
|
||||
getDefaultNativeImageDir: async () => DEFAULT_IMAGE_CACHE_DIR,
|
||||
getDefaultNativeDataDir: async () => "",
|
||||
deleteFileNative: async () => { },
|
||||
chooseDir: async (x: string) => "",
|
||||
getSettings: async () => ({ imageCacheDir: DEFAULT_IMAGE_CACHE_DIR, logsDir: "" }),
|
||||
init: async () => { },
|
||||
initDirs: async () => { },
|
||||
getImageNative: async (x: string) => new Uint8Array(0),
|
||||
getNativeSavedImages: async () => new Map(),
|
||||
messageLoggerEnhancedUniqueIdThingyIdkMan: async () => { },
|
||||
showItemInFolder: async () => { },
|
||||
writeImageNative: async () => { },
|
||||
getCommitHash: async () => ({ ok: true, value: "" }),
|
||||
getRepoInfo: async () => ({ ok: true, value: { repo: "", gitHash: "" } }),
|
||||
getNewCommits: async () => ({ ok: true, value: [] }),
|
||||
update: async () => ({ ok: true, value: "" }),
|
||||
chooseFile: async () => "",
|
||||
downloadAttachment: async () => ({ error: "web", path: null }),
|
||||
} satisfies PluginNative<typeof import("../native")>;
|
||||
|
||||
return Native;
|
||||
|
||||
}
|
||||
|
||||
return Object.values(VencordNative.pluginHelpers)
|
||||
.find(m => m.messageLoggerEnhancedUniqueIdThingyIdkMan) as PluginNative<typeof import("../native")>;
|
||||
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { ChannelStore, GuildStore } from "@webpack/common";
|
||||
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
import { getGuildIdByChannel } from "./index";
|
||||
import { memoize } from "./memoize";
|
||||
|
||||
|
||||
const validIdSearchTypes = ["server", "guild", "channel", "in", "user", "from", "message", "has", "before", "after", "around", "near", "during"] as const;
|
||||
type ValidIdSearchTypesUnion = typeof validIdSearchTypes[number];
|
||||
|
||||
interface QueryResult {
|
||||
key: ValidIdSearchTypesUnion;
|
||||
value: string;
|
||||
negate: boolean;
|
||||
}
|
||||
|
||||
export const parseQuery = memoize((query: string = ""): QueryResult | string => {
|
||||
let trimmedQuery = query.trim();
|
||||
if (!trimmedQuery) {
|
||||
return query;
|
||||
}
|
||||
|
||||
let negate = false;
|
||||
if (trimmedQuery.startsWith("!")) {
|
||||
negate = true;
|
||||
trimmedQuery = trimmedQuery.substring(trimmedQuery.length, 1);
|
||||
}
|
||||
|
||||
const [filter, rest] = trimmedQuery.split(" ", 2);
|
||||
if (!filter) {
|
||||
return query;
|
||||
}
|
||||
|
||||
const [type, id] = filter.split(":") as [ValidIdSearchTypesUnion, string];
|
||||
if (!type || !id || !validIdSearchTypes.includes(type)) {
|
||||
return query;
|
||||
}
|
||||
|
||||
return {
|
||||
key: type,
|
||||
value: id,
|
||||
negate,
|
||||
};
|
||||
});
|
||||
|
||||
export const tokenizeQuery = (query: string) => {
|
||||
const parts = query.split(" ").map(parseQuery);
|
||||
const queries = parts.filter(p => typeof p !== "string") as QueryResult[];
|
||||
const rest = parts.filter(p => typeof p === "string") as string[];
|
||||
|
||||
return { queries, rest };
|
||||
};
|
||||
|
||||
const linkRegex = /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||
|
||||
export const doesMatch = (type: typeof validIdSearchTypes[number], value: string, message: LoggedMessageJSON) => {
|
||||
switch (type) {
|
||||
case "in":
|
||||
case "channel":
|
||||
const channel = ChannelStore.getChannel(message.channel_id);
|
||||
if (!channel)
|
||||
return message.channel_id === value;
|
||||
const { name, id } = channel;
|
||||
return id === value
|
||||
|| name.toLowerCase().includes(value.toLowerCase());
|
||||
case "message":
|
||||
return message.id === value;
|
||||
case "from":
|
||||
case "user":
|
||||
return message.author.id === value
|
||||
|| message.author?.username?.toLowerCase().includes(value.toLowerCase())
|
||||
|| (message.author as any)?.globalName?.toLowerCase()?.includes(value.toLowerCase());
|
||||
case "guild":
|
||||
case "server": {
|
||||
const guildId = message.guildId ?? getGuildIdByChannel(message.channel_id);
|
||||
if (!guildId) return false;
|
||||
|
||||
const guild = GuildStore.getGuild(guildId);
|
||||
if (!guild)
|
||||
return guildId === value;
|
||||
|
||||
return guild.id === value
|
||||
|| guild.name.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
case "before":
|
||||
return new Date(message.timestamp) < new Date(value);
|
||||
case "after":
|
||||
return new Date(message.timestamp) > new Date(value);
|
||||
case "around":
|
||||
case "near":
|
||||
case "during":
|
||||
return Math.abs(new Date(message.timestamp)?.getTime() - new Date(value)?.getTime()) < 1000 * 60 * 60 * 24;
|
||||
case "has": {
|
||||
switch (value) {
|
||||
case "attachment":
|
||||
return message.attachments.length > 0;
|
||||
case "image":
|
||||
return message.attachments.some(a => a?.content_type?.startsWith("image")) ||
|
||||
message.embeds.some(e => e.image || e.thumbnail);
|
||||
case "video":
|
||||
return message.attachments.some(a => a?.content_type?.startsWith("video")) ||
|
||||
message.embeds.some(e => e.video);
|
||||
case "embed":
|
||||
return message.embeds.length > 0;
|
||||
case "link":
|
||||
return message.content.match(linkRegex);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 {
|
||||
createStore,
|
||||
del,
|
||||
get,
|
||||
keys,
|
||||
set,
|
||||
} from "@api/DataStore";
|
||||
import { sleep } from "@utils/misc";
|
||||
|
||||
import { Flogger, Native } from "../..";
|
||||
import { LoggedAttachment } from "../../types";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "../constants";
|
||||
|
||||
const ImageStore = createStore("MessageLoggerImageData", "MessageLoggerImageStore");
|
||||
|
||||
interface IDBSavedImage { attachmentId: string, path: string; }
|
||||
const idbSavedImages = new Map<string, IDBSavedImage>();
|
||||
(async () => {
|
||||
try {
|
||||
|
||||
const paths = await keys(ImageStore);
|
||||
paths.forEach(path => {
|
||||
const str = path.toString();
|
||||
if (!str.startsWith(DEFAULT_IMAGE_CACHE_DIR)) return;
|
||||
|
||||
idbSavedImages.set(str.split("/")?.[1]?.split(".")?.[0], { attachmentId: str.split("/")?.[1]?.split(".")?.[0], path: str });
|
||||
});
|
||||
} catch (err) {
|
||||
Flogger.error("Failed to get idb images", err);
|
||||
}
|
||||
})();
|
||||
|
||||
export async function getImage(attachmentId: string, fileExt?: string | null): Promise<any> {
|
||||
// for people who have access to native api but some images are still in idb
|
||||
// also for people who dont have native api
|
||||
const idbPath = idbSavedImages.get(attachmentId)?.path;
|
||||
if (idbPath)
|
||||
return get(idbPath, ImageStore);
|
||||
|
||||
if (IS_WEB) return null;
|
||||
|
||||
return await Native.getImageNative(attachmentId);
|
||||
}
|
||||
|
||||
export async function downloadAttachment(attachemnt: LoggedAttachment): Promise<string | undefined> {
|
||||
if (IS_WEB) {
|
||||
return await downloadAttachmentWeb(attachemnt);
|
||||
}
|
||||
|
||||
const { path, error } = await Native.downloadAttachment(attachemnt);
|
||||
|
||||
if (error || !path) {
|
||||
Flogger.error("Failed to download attachment", error, path);
|
||||
return;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export async function deleteImage(attachmentId: string): Promise<void> {
|
||||
const idbPath = idbSavedImages.get(attachmentId)?.path;
|
||||
if (idbPath)
|
||||
return await del(idbPath, ImageStore);
|
||||
|
||||
|
||||
if (IS_WEB) return;
|
||||
|
||||
await Native.deleteFileNative(attachmentId);
|
||||
}
|
||||
|
||||
|
||||
async function downloadAttachmentWeb(attachemnt: LoggedAttachment, attempts = 0) {
|
||||
if (!attachemnt?.url || !attachemnt?.id || !attachemnt?.fileExtension) {
|
||||
Flogger.error("Invalid attachment", attachemnt);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(attachemnt.url);
|
||||
if (res.status !== 200) {
|
||||
if (res.status === 404 || res.status === 403) return;
|
||||
attempts++;
|
||||
if (attempts > 3) {
|
||||
Flogger.warn(`Failed to get attachment ${attachemnt.id} for caching, error code ${res.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
return downloadAttachmentWeb(attachemnt, attempts);
|
||||
}
|
||||
const ab = await res.arrayBuffer();
|
||||
const path = `${DEFAULT_IMAGE_CACHE_DIR}/${attachemnt.id}${attachemnt.fileExtension}`;
|
||||
|
||||
// await writeImage(imageCacheDir, `${attachmentId}${fileExtension}`, new Uint8Array(ab));
|
||||
|
||||
await set(path, new Uint8Array(ab), ImageStore);
|
||||
idbSavedImages.set(attachemnt.id, { attachmentId: attachemnt.id, path });
|
||||
|
||||
return path;
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { MessageAttachment } from "discord-types/general";
|
||||
|
||||
import { Flogger, settings } from "../..";
|
||||
import { LoggedAttachment, LoggedMessage, LoggedMessageJSON } from "../../types";
|
||||
import { memoize } from "../memoize";
|
||||
import { deleteImage, downloadAttachment, getImage, } from "./ImageManager";
|
||||
|
||||
export function getFileExtension(str: string) {
|
||||
const matches = str.match(/(\.[a-zA-Z0-9]+)(?:\?.*)?$/);
|
||||
if (!matches) return null;
|
||||
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
export function isAttachmentGoodToCache(attachment: MessageAttachment, fileExtension: string) {
|
||||
if (attachment.size > settings.store.attachmentSizeLimitInMegabytes * 1024 * 1024) {
|
||||
Flogger.log("Attachment too large to cache", attachment.filename);
|
||||
return false;
|
||||
}
|
||||
const attachmentFileExtensionsStr = settings.store.attachmentFileExtensions.trim();
|
||||
|
||||
if (attachmentFileExtensionsStr === "")
|
||||
return true;
|
||||
|
||||
const allowedFileExtensions = attachmentFileExtensionsStr.split(",");
|
||||
|
||||
if (fileExtension.startsWith(".")) {
|
||||
fileExtension = fileExtension.slice(1);
|
||||
}
|
||||
|
||||
if (!fileExtension || !allowedFileExtensions.includes(fileExtension)) {
|
||||
Flogger.log("Attachment not in allowed file extensions", attachment.filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function cacheMessageImages(message: LoggedMessage | LoggedMessageJSON) {
|
||||
try {
|
||||
for (const attachment of message.attachments) {
|
||||
const fileExtension = getFileExtension(attachment.filename ?? attachment.url) ?? attachment?.content_type?.split("/")?.[1] ?? ".png";
|
||||
|
||||
if (!isAttachmentGoodToCache(attachment, fileExtension)) {
|
||||
Flogger.log("skipping", attachment.filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
attachment.oldUrl = attachment.url;
|
||||
attachment.oldProxyUrl = attachment.proxy_url;
|
||||
|
||||
// only normal urls work if theres a charset in the content type /shrug
|
||||
if (attachment?.content_type?.includes(";")) {
|
||||
attachment.proxy_url = attachment.url;
|
||||
} else {
|
||||
// apparently proxy urls last longer
|
||||
attachment.url = attachment.proxy_url;
|
||||
attachment.proxy_url = attachment.url;
|
||||
}
|
||||
|
||||
attachment.fileExtension = fileExtension;
|
||||
|
||||
const path = await downloadAttachment(attachment);
|
||||
|
||||
if (!path) {
|
||||
Flogger.error("Failed to cache attachment", attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
attachment.path = path;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
Flogger.error("Error caching message images:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMessageImages(message: LoggedMessage | LoggedMessageJSON) {
|
||||
for (let i = 0; i < message.attachments.length; i++) {
|
||||
const attachment = message.attachments[i];
|
||||
await deleteImage(attachment.id);
|
||||
}
|
||||
}
|
||||
|
||||
export const getAttachmentBlobUrl = memoize(async (attachment: LoggedAttachment) => {
|
||||
const imageData = await getImage(attachment.id, attachment.fileExtension);
|
||||
if (!imageData) return null;
|
||||
|
||||
const blob = new Blob([imageData]);
|
||||
const resUrl = URL.createObjectURL(blob);
|
||||
|
||||
return resUrl;
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 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 { chooseFile as chooseFileWeb } from "@utils/web";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { Native } from "..";
|
||||
import { addMessagesBulkIDB, DBMessageRecord, getAllMessagesIDB } from "../db";
|
||||
import { LoggedMessage, LoggedMessageJSON } from "../types";
|
||||
|
||||
async function getLogContents(): Promise<string> {
|
||||
if (IS_WEB) {
|
||||
const file = await chooseFileWeb(".json");
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) return reject("No file selected");
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await Native.getSettings();
|
||||
return Native.chooseFile("Logs", [{ extensions: ["json"], name: "logs" }], settings.logsDir);
|
||||
}
|
||||
|
||||
export async function importLogs() {
|
||||
try {
|
||||
const content = await getLogContents();
|
||||
const data = JSON.parse(content) as { messages: DBMessageRecord[]; };
|
||||
|
||||
let messages: LoggedMessageJSON[] = [];
|
||||
|
||||
if ((data as any).deletedMessages || (data as any).editedMessages) {
|
||||
messages = Object.values((data as unknown as LoggedMessage)).filter(m => m.message).map(m => m.message) as LoggedMessageJSON[];
|
||||
} else
|
||||
messages = data.messages.map(m => m.message);
|
||||
|
||||
if (!Array.isArray(messages)) {
|
||||
throw new Error("Invalid log file format");
|
||||
}
|
||||
|
||||
if (!messages.length) {
|
||||
throw new Error("No messages found in log file");
|
||||
}
|
||||
|
||||
if (!messages.every(m => m.id && m.channel_id && m.timestamp)) {
|
||||
throw new Error("Invalid message format");
|
||||
}
|
||||
|
||||
await addMessagesBulkIDB(messages);
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Successfully imported logs",
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Error importing logs. Check the console for more information",
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function exportLogs() {
|
||||
const filename = "message-logger-logs-idb.json";
|
||||
|
||||
const messages = await getAllMessagesIDB();
|
||||
const data = JSON.stringify({ messages }, null, 2);
|
||||
|
||||
if (IS_WEB || IS_VESKTOP || IS_EQUIBOP || !DiscordNative) {
|
||||
const file = new File([data], filename, { type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = filename;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setImmediate(() => {
|
||||
URL.revokeObjectURL(a.href);
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
} else {
|
||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||
}
|
||||
}
|
||||
|
74
src/equicordplugins/moreUserTags/consts.ts
Normal file
74
src/equicordplugins/moreUserTags/consts.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findByCodeLazy, findLazy } from "@webpack";
|
||||
import { GuildStore } from "@webpack/common";
|
||||
import { RC } from "@webpack/types";
|
||||
import { Channel, Guild, Message, User } from "discord-types/general";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import type { ITag } from "./types";
|
||||
|
||||
export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
|
||||
export const tags = [
|
||||
{
|
||||
name: "WEBHOOK",
|
||||
displayName: "Webhook",
|
||||
description: "Messages sent by webhooks",
|
||||
condition: isWebhook
|
||||
}, {
|
||||
name: "OWNER",
|
||||
displayName: "Owner",
|
||||
description: "Owns the server",
|
||||
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
|
||||
}, {
|
||||
name: "ADMINISTRATOR",
|
||||
displayName: "Admin",
|
||||
description: "Has the administrator permission",
|
||||
permissions: ["ADMINISTRATOR"]
|
||||
}, {
|
||||
name: "MODERATOR_STAFF",
|
||||
displayName: "Staff",
|
||||
description: "Can manage the server, channels or roles",
|
||||
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
|
||||
}, {
|
||||
name: "MODERATOR",
|
||||
displayName: "Mod",
|
||||
description: "Can manage messages or kick/ban people",
|
||||
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
|
||||
}, {
|
||||
name: "VOICE_MODERATOR",
|
||||
displayName: "VC Mod",
|
||||
description: "Can manage voice chats",
|
||||
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||
}, {
|
||||
name: "CHAT_MODERATOR",
|
||||
displayName: "Chat Mod",
|
||||
description: "Can timeout people",
|
||||
permissions: ["MODERATE_MEMBERS"]
|
||||
}, {
|
||||
name: "SPECIAL_USER",
|
||||
displayName: "Special",
|
||||
description: "Special custom tag for specific users",
|
||||
condition: (_message, user) => {
|
||||
const { specialUsers } = settings.store;
|
||||
const userIds = specialUsers.split(",").map(id => id.trim());
|
||||
return userIds.includes(user.id);
|
||||
},
|
||||
verified: true
|
||||
}
|
||||
] as const satisfies ITag[];
|
||||
|
||||
export const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number | null, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };
|
||||
|
||||
// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
|
||||
export const computePermissions: (options: {
|
||||
user?: { id: string; } | string | null;
|
||||
context?: Guild | Channel | null;
|
||||
overwrites?: Channel["permissionOverwrites"] | null;
|
||||
checkElevated?: boolean /* = true */;
|
||||
excludeGuildPermissions?: boolean /* = false */;
|
||||
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");
|
185
src/equicordplugins/moreUserTags/index.tsx
Normal file
185
src/equicordplugins/moreUserTags/index.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { getCurrentChannel, getIntlMessage } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore, GuildStore, PermissionsBits, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { Channel, Message, User } from "discord-types/general";
|
||||
|
||||
import { computePermissions, Tag, tags } from "./consts";
|
||||
import { settings } from "./settings";
|
||||
import { TagSettings } from "./types";
|
||||
|
||||
const cl = classNameFactory("vc-mut-");
|
||||
|
||||
const genTagTypes = () => {
|
||||
let i = 100;
|
||||
const obj = {};
|
||||
|
||||
for (const { name } of tags) {
|
||||
obj[name] = ++i;
|
||||
obj[i] = name;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "MoreUserTags",
|
||||
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
||||
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen],
|
||||
dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"],
|
||||
settings,
|
||||
patches: [
|
||||
// Make discord actually use our tags
|
||||
{
|
||||
find: ".STAFF_ONLY_DM:",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})default:(\i)=/,
|
||||
replace: "default:$2=$self.getTagText($self.localTags[$1]);",
|
||||
},
|
||||
{
|
||||
match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})\.BOT:(?=default:)/,
|
||||
replace: "$&return null;",
|
||||
predicate: () => settings.store.dontShowBotTag
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
start() {
|
||||
const tagSettings = settings.store.tagSettings || {} as TagSettings;
|
||||
for (const tag of Object.values(tags)) {
|
||||
tagSettings[tag.name] ??= {
|
||||
showInChat: true,
|
||||
showInNotChat: true,
|
||||
text: tag.displayName
|
||||
};
|
||||
}
|
||||
|
||||
settings.store.tagSettings = tagSettings;
|
||||
},
|
||||
localTags: genTagTypes(),
|
||||
getChannelId() {
|
||||
return SelectedChannelStore.getChannelId();
|
||||
},
|
||||
renderNicknameIcon(props) {
|
||||
const tagId = this.getTag({
|
||||
user: UserStore.getUser(props.userId),
|
||||
channel: ChannelStore.getChannel(this.getChannelId()),
|
||||
channelId: this.getChannelId(),
|
||||
isChat: false
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
type={tagId}
|
||||
verified={false}>
|
||||
</Tag>;
|
||||
|
||||
},
|
||||
renderMessageDecoration(props) {
|
||||
const tagId = this.getTag({
|
||||
message: props.message,
|
||||
user: UserStore.getUser(props.message.author.id),
|
||||
channelId: props.message.channel_id,
|
||||
isChat: false
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
useRemSizes={true}
|
||||
className={cl("message-tag", props.message.author.isVerifiedBot() && "message-verified")}
|
||||
type={tagId}
|
||||
verified={false}>
|
||||
</Tag>;
|
||||
},
|
||||
renderMemberListDecorator(props) {
|
||||
const tagId = this.getTag({
|
||||
user: props.user,
|
||||
channel: getCurrentChannel(),
|
||||
channelId: this.getChannelId(),
|
||||
isChat: false
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
type={tagId}
|
||||
verified={false}>
|
||||
</Tag>;
|
||||
},
|
||||
|
||||
getTagText(tagName: string) {
|
||||
if (!tagName) return getIntlMessage("APP_TAG");
|
||||
const tag = tags.find(({ name }) => tagName === name);
|
||||
if (!tag) return tagName || getIntlMessage("APP_TAG");
|
||||
|
||||
return settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
|
||||
},
|
||||
|
||||
getTag({
|
||||
message, user, channelId, isChat, channel
|
||||
}: {
|
||||
message?: Message,
|
||||
user?: User & { isClyde(): boolean; },
|
||||
channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; },
|
||||
channelId?: string;
|
||||
isChat?: boolean;
|
||||
}): number | null {
|
||||
const settings = this.settings.store;
|
||||
|
||||
if (!user) return null;
|
||||
if (isChat && user.id === "1") return null;
|
||||
if (user.isClyde()) return null;
|
||||
if (user.bot && settings.dontShowForBots) return null;
|
||||
|
||||
channel ??= ChannelStore.getChannel(channelId!) as any;
|
||||
if (!channel) return null;
|
||||
|
||||
const perms = this.getPermissions(user, channel);
|
||||
|
||||
for (const tag of tags) {
|
||||
if (isChat && !settings.tagSettings[tag.name].showInChat)
|
||||
continue;
|
||||
if (!isChat && !settings.tagSettings[tag.name].showInNotChat)
|
||||
continue;
|
||||
|
||||
// If the owner tag is disabled, and the user is the owner of the guild,
|
||||
// avoid adding other tags because the owner will always match the condition for them
|
||||
if (
|
||||
(tag.name !== "OWNER" &&
|
||||
GuildStore.getGuild(channel?.guild_id)?.ownerId ===
|
||||
user.id &&
|
||||
isChat &&
|
||||
!settings.tagSettings.OWNER.showInChat) ||
|
||||
(!isChat &&
|
||||
!settings.tagSettings.OWNER.showInNotChat)
|
||||
)
|
||||
continue;
|
||||
|
||||
if ("permissions" in tag ?
|
||||
tag.permissions.some(perm => perms.includes(perm)) :
|
||||
tag.condition(message!, user, channel)) {
|
||||
|
||||
return this.localTags[tag.name];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
getPermissions(user: User, channel: Channel): string[] {
|
||||
const guild = GuildStore.getGuild(channel?.guild_id);
|
||||
if (!guild) return [];
|
||||
|
||||
const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
|
||||
return Object.entries(PermissionsBits)
|
||||
.map(([perm, permInt]) =>
|
||||
permissions & permInt ? perm : ""
|
||||
)
|
||||
.filter(Boolean);
|
||||
},
|
||||
});
|
21
src/equicordplugins/moreUserTags/settings.ts
Normal file
21
src/equicordplugins/moreUserTags/settings.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { OptionType } from "@utils/types";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
specialUsers: {
|
||||
type: OptionType.STRING,
|
||||
description: "List of special user IDs (comma separated)",
|
||||
default: "878151241769820173",
|
||||
},
|
||||
specialTag: {
|
||||
type: OptionType.STRING,
|
||||
description: "Text to display for special users",
|
||||
default: "Special",
|
||||
}
|
||||
});
|
114
src/equicordplugins/moreUserTags/settings.tsx
Normal file
114
src/equicordplugins/moreUserTags/settings.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 { Margins } from "@utils/margins";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { Card, Flex, Forms, Switch, TextInput, Tooltip } from "@webpack/common";
|
||||
|
||||
import { Tag, tags } from "./consts";
|
||||
import { TagSettings } from "./types";
|
||||
|
||||
function SettingsComponent() {
|
||||
const tagSettings = settings.store.tagSettings as TagSettings;
|
||||
const { localTags } = Vencord.Plugins.plugins.MoreUserTags as any;
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
{tags.map(t => (
|
||||
<Card
|
||||
key={t.name}
|
||||
style={{
|
||||
padding: "1em 1em 0",
|
||||
width: "calc(33.333% - 11px)",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<Forms.FormTitle style={{ width: "fit-content" }}>
|
||||
<Tooltip text={t.description}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{t.displayName} Tag
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Forms.FormTitle>
|
||||
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<Forms.FormText style={{ fontSize: "13px" }}>
|
||||
Example:
|
||||
</Forms.FormText>
|
||||
<Tag type={localTags[t.name]} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
type="text"
|
||||
value={tagSettings[t.name]?.text ?? t.displayName}
|
||||
placeholder={`Text on tag (default: ${t.displayName})`}
|
||||
onChange={v => tagSettings[t.name].text = v}
|
||||
className={Margins.bottom16}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
value={tagSettings[t.name]?.showInChat ?? true}
|
||||
onChange={v => tagSettings[t.name].showInChat = v}
|
||||
hideBorder
|
||||
>
|
||||
Show in messages
|
||||
</Switch>
|
||||
|
||||
<Switch
|
||||
value={tagSettings[t.name]?.showInNotChat ?? true}
|
||||
onChange={v => tagSettings[t.name].showInNotChat = v}
|
||||
hideBorder
|
||||
>
|
||||
Show in member list and profiles
|
||||
</Switch>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
dontShowForBots: {
|
||||
description: "Don't show extra tags for bots (excluding webhooks)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
dontShowBotTag: {
|
||||
description: "Only show extra tags for bots / Hide [APP] text",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
tagSettings: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: SettingsComponent,
|
||||
description: "fill me"
|
||||
},
|
||||
specialUsers: {
|
||||
type: OptionType.STRING,
|
||||
description: "List of special user IDs (comma separated)",
|
||||
default: "878151241769820173",
|
||||
},
|
||||
specialTag: {
|
||||
type: OptionType.STRING,
|
||||
description: "Text to display for special users",
|
||||
default: "Special",
|
||||
}
|
||||
});
|
27
src/equicordplugins/moreUserTags/styles.css
Normal file
27
src/equicordplugins/moreUserTags/styles.css
Normal file
|
@ -0,0 +1,27 @@
|
|||
.vc-message-decorations-wrapper .vc-mut-message-tag {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.vc-mut-message-tag {
|
||||
/* Remove default margin from tags in messages */
|
||||
margin-top: unset !important;
|
||||
|
||||
/* Align with Discord default tags in messages */
|
||||
/* stylelint-disable-next-line length-zero-no-unit */
|
||||
bottom: 0px;
|
||||
top: -2px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.vc-mut-message-verified {
|
||||
height: 1rem !important;
|
||||
}
|
||||
|
||||
span[class*="botTagCozy"][data-moreTags-darkFg="true"]>svg>path {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
span[class*="botTagCozy"][data-moreTags-darkFg="false"]>svg>path {
|
||||
fill: #fff;
|
||||
}
|
33
src/equicordplugins/moreUserTags/types.ts
Normal file
33
src/equicordplugins/moreUserTags/types.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Permissions } from "@webpack/types";
|
||||
import type { Channel, Message, User } from "discord-types/general";
|
||||
|
||||
import { tags } from "./consts";
|
||||
|
||||
export type ITag = {
|
||||
// name used for identifying, must be alphanumeric + underscores
|
||||
name: string;
|
||||
// name shown on the tag itself, can be anything probably; automatically uppercase'd
|
||||
displayName: string;
|
||||
description: string;
|
||||
verified?: boolean;
|
||||
} & ({
|
||||
permissions: Permissions[];
|
||||
} | {
|
||||
condition?(message: Message | null, user: User, channel: Channel): boolean;
|
||||
});
|
||||
|
||||
export interface TagSetting {
|
||||
text: string;
|
||||
showInChat: boolean;
|
||||
showInNotChat: boolean;
|
||||
}
|
||||
|
||||
export type TagSettings = {
|
||||
[k in typeof tags[number]["name"]]: TagSetting;
|
||||
};
|
|
@ -30,7 +30,11 @@ const messagePatch: NavContextMenuPatchCallback = (children, { message }) => {
|
|||
label="Quote"
|
||||
icon={QuoteIcon}
|
||||
action={async () => {
|
||||
openModal(props => <QuoteModal {...props} />);
|
||||
if (settings.store.autoSendQuote) {
|
||||
await SendInChat(() => { });
|
||||
} else {
|
||||
openModal(props => <QuoteModal {...props} />);
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
|
||||
|
@ -65,6 +69,11 @@ const settings = definePluginSettings({
|
|||
{ label: "Username", value: userIDOptions.userName },
|
||||
{ label: "User ID", value: userIDOptions.userId }
|
||||
]
|
||||
},
|
||||
autoSendQuote: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically send the quote when clicking the quote button",
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -187,7 +196,7 @@ function registerStyleChange(style) {
|
|||
}
|
||||
|
||||
function QuoteModal(props: ModalProps) {
|
||||
const [gray, setGray] = useState(true);
|
||||
const [gray, setGray] = useState(false); // Default to false (disabled)
|
||||
useEffect(() => {
|
||||
grayscale = gray;
|
||||
GeneratePreview();
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
* 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", {});
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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 />;
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* 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 };
|
||||
}
|
|
@ -1,61 +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 { 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();
|
||||
},
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* 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
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* 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").filter(line => line.trim() !== "");
|
||||
|
||||
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
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { proxyLazyWebpack } from "@webpack";
|
||||
import { Flux, FluxDispatcher } from "@webpack/common";
|
||||
import { Track } from "plugins/spotifyControls/SpotifyStore";
|
||||
|
||||
import { getLyrics, lyricFetchers, updateLyrics } from "../api";
|
||||
import settings from "../settings";
|
||||
import { romanizeLyrics, translateLyrics } from "./translator";
|
||||
import { LyricsData, Provider } from "./types";
|
||||
|
||||
interface PlayerStateMin {
|
||||
track: Track | null;
|
||||
device?: Device;
|
||||
isPlaying: boolean,
|
||||
position: number,
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
function showNotif(title: string, body: string) {
|
||||
if (settings.store.ShowFailedToasts) {
|
||||
showNotification({
|
||||
color: "#ee2902",
|
||||
title,
|
||||
body,
|
||||
noPersist: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// steal from spotifycontrols
|
||||
export const SpotifyLrcStore = proxyLazyWebpack(() => {
|
||||
class SpotifyLrcStore extends Flux.Store {
|
||||
public mPosition = 0;
|
||||
private start = 0;
|
||||
|
||||
public track: Track | null = null;
|
||||
public device: Device | null = null;
|
||||
public isPlaying = false;
|
||||
public lyricsInfo: LyricsData | null = null;
|
||||
public fetchingsTracks: string[] = [];
|
||||
|
||||
|
||||
public get position(): number {
|
||||
let pos = this.mPosition;
|
||||
if (this.isPlaying) {
|
||||
pos += Date.now() - this.start;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
public set position(p: number) {
|
||||
this.mPosition = p;
|
||||
this.start = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
const store = new SpotifyLrcStore(FluxDispatcher, {
|
||||
async SPOTIFY_PLAYER_STATE(e: PlayerStateMin) {
|
||||
if (store.fetchingsTracks.includes(e.track?.id ?? "")) return;
|
||||
|
||||
store.fetchingsTracks.push(e.track?.id ?? "");
|
||||
store.track = e.track;
|
||||
store.isPlaying = e.isPlaying ?? false;
|
||||
store.position = e.position ?? 0;
|
||||
store.device = e.device ?? null;
|
||||
store.lyricsInfo = await getLyrics(e.track);
|
||||
const { LyricsConversion } = settings.store;
|
||||
if (LyricsConversion !== Provider.None) {
|
||||
// @ts-ignore
|
||||
FluxDispatcher.dispatch({ type: "SPOTIFY_LYRICS_PROVIDER_CHANGE", provider: LyricsConversion });
|
||||
}
|
||||
|
||||
store.fetchingsTracks = store.fetchingsTracks.filter(id => id !== e.track?.id);
|
||||
store.emitChange();
|
||||
},
|
||||
|
||||
SPOTIFY_SET_DEVICES({ devices }: { devices: Device[]; }) {
|
||||
store.device = devices.find(d => d.is_active) ?? devices[0] ?? null;
|
||||
store.emitChange();
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
async SPOTIFY_LYRICS_PROVIDER_CHANGE(e: { provider: Provider; }) {
|
||||
const currentInfo = await getLyrics(store.track);
|
||||
const { provider } = e;
|
||||
if (currentInfo?.useLyric === provider) return;
|
||||
|
||||
if (currentInfo?.lyricsVersions[provider]) {
|
||||
store.lyricsInfo = { ...currentInfo, useLyric: provider };
|
||||
|
||||
await updateLyrics(store.track!.id, currentInfo.lyricsVersions[provider]!, provider);
|
||||
store.emitChange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === Provider.Translated || provider === Provider.Romanized) {
|
||||
if (!currentInfo?.lyricsVersions[Provider.Spotify] && !currentInfo?.lyricsVersions[Provider.Lrclib]) {
|
||||
showNotif("No lyrics", `No lyrics to ${provider === Provider.Translated ? "translate" : "romanize"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetcher = provider === Provider.Translated ? translateLyrics : romanizeLyrics;
|
||||
|
||||
const fetchResult = await fetcher(currentInfo.lyricsVersions[currentInfo.useLyric]);
|
||||
|
||||
if (!fetchResult) {
|
||||
showNotif("Lyrics fetch failed", `Failed to fetch ${provider === Provider.Translated ? "translation" : "romanization"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
store.lyricsInfo = {
|
||||
...currentInfo,
|
||||
useLyric: provider,
|
||||
lyricsVersions: {
|
||||
...currentInfo.lyricsVersions,
|
||||
[provider]: fetchResult
|
||||
}
|
||||
};
|
||||
|
||||
await updateLyrics(store.track!.id, fetchResult, provider);
|
||||
|
||||
store.emitChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const newLyricsInfo = await lyricFetchers[e.provider](store.track!);
|
||||
if (!newLyricsInfo) {
|
||||
showNotif("Lyrics fetch failed", `Failed to fetch ${e.provider} lyrics`);
|
||||
return;
|
||||
}
|
||||
|
||||
store.lyricsInfo = newLyricsInfo;
|
||||
|
||||
updateLyrics(store.track!.id, newLyricsInfo.lyricsVersions[e.provider], e.provider);
|
||||
|
||||
store.emitChange();
|
||||
}
|
||||
});
|
||||
return store;
|
||||
});
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import settings from "../../settings";
|
||||
import { LyricsData, Provider, SyncedLyric } from "../types";
|
||||
|
||||
// stolen from src\plugins\translate\utils.ts
|
||||
|
||||
interface GoogleData {
|
||||
src: string;
|
||||
sentences: {
|
||||
// 🏳️⚧️
|
||||
trans: string;
|
||||
orig: string;
|
||||
src_translit?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
async function googleTranslate(text: string, targetLang: string, romanize: boolean): Promise<GoogleData | null> {
|
||||
const url = "https://translate.googleapis.com/translate_a/single?" + new URLSearchParams({
|
||||
// see https://stackoverflow.com/a/29537590 for more params
|
||||
// holy shidd nvidia
|
||||
client: "gtx",
|
||||
// source language
|
||||
sl: "auto",
|
||||
// target language
|
||||
tl: targetLang,
|
||||
// what to return, t = translation probably
|
||||
dt: romanize ? "rm" : "t",
|
||||
// Send json object response instead of weird array
|
||||
dj: "1",
|
||||
source: "input",
|
||||
// query, duh
|
||||
q: text
|
||||
});
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok)
|
||||
return null;
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function processLyrics(
|
||||
lyrics: LyricsData["lyricsVersions"][Provider],
|
||||
targetLang: string,
|
||||
romanize: boolean
|
||||
): Promise<SyncedLyric[] | null> {
|
||||
if (!lyrics) return null;
|
||||
|
||||
const nonDuplicatedLyrics = lyrics.filter((lyric, index, self) =>
|
||||
self.findIndex(l => l.text === lyric.text) === index
|
||||
);
|
||||
|
||||
const processedLyricsResp = await Promise.all(
|
||||
nonDuplicatedLyrics.map(async lyric => {
|
||||
if (!lyric.text) return [lyric.text, null];
|
||||
|
||||
const translation = await googleTranslate(lyric.text, targetLang, romanize);
|
||||
|
||||
if (!translation || !translation.sentences || translation.sentences.length === 0) return [lyric.text, null];
|
||||
|
||||
return [lyric.text, romanize ? translation.sentences[0].src_translit : translation.sentences[0].trans];
|
||||
})
|
||||
);
|
||||
|
||||
if (processedLyricsResp[0][1] === null) return null;
|
||||
|
||||
return lyrics.map(lyric => ({
|
||||
...lyric,
|
||||
text: processedLyricsResp.find(mapping => mapping[0] === lyric.text)?.[1] ?? lyric.text
|
||||
}));
|
||||
}
|
||||
|
||||
export async function translateLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise<SyncedLyric[] | null> {
|
||||
const language = settings.store.TranslateTo;
|
||||
// Why not make only one request to translate?
|
||||
// because occasionally it will add a new line
|
||||
// and i dont have a good way to handle that
|
||||
return processLyrics(lyrics, language, false);
|
||||
}
|
||||
|
||||
export async function romanizeLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise<SyncedLyric[] | null> {
|
||||
// Why not make only one request to romanize?
|
||||
// it will romanize it as one string, and how would i know where to split it?
|
||||
return processLyrics(lyrics, "", true);
|
||||
}
|
|
@ -1,546 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// hate having this twice but oh well
|
||||
export default [
|
||||
{
|
||||
value: "auto",
|
||||
label: "Detect language"
|
||||
},
|
||||
{
|
||||
value: "af",
|
||||
label: "Afrikaans"
|
||||
},
|
||||
{
|
||||
value: "sq",
|
||||
label: "Albanian"
|
||||
},
|
||||
{
|
||||
value: "am",
|
||||
label: "Amharic"
|
||||
},
|
||||
{
|
||||
value: "ar",
|
||||
label: "Arabic"
|
||||
},
|
||||
{
|
||||
value: "hy",
|
||||
label: "Armenian"
|
||||
},
|
||||
{
|
||||
value: "as",
|
||||
label: "Assamese"
|
||||
},
|
||||
{
|
||||
value: "ay",
|
||||
label: "Aymara"
|
||||
},
|
||||
{
|
||||
value: "az",
|
||||
label: "Azerbaijani"
|
||||
},
|
||||
{
|
||||
value: "bm",
|
||||
label: "Bambara"
|
||||
},
|
||||
{
|
||||
value: "eu",
|
||||
label: "Basque"
|
||||
},
|
||||
{
|
||||
value: "be",
|
||||
label: "Belarusian"
|
||||
},
|
||||
{
|
||||
value: "bn",
|
||||
label: "Bengali"
|
||||
},
|
||||
{
|
||||
value: "bho",
|
||||
label: "Bhojpuri"
|
||||
},
|
||||
{
|
||||
value: "bs",
|
||||
label: "Bosnian"
|
||||
},
|
||||
{
|
||||
value: "bg",
|
||||
label: "Bulgarian"
|
||||
},
|
||||
{
|
||||
value: "ca",
|
||||
label: "Catalan"
|
||||
},
|
||||
{
|
||||
value: "ceb",
|
||||
label: "Cebuano"
|
||||
},
|
||||
{
|
||||
value: "ny",
|
||||
label: "Chichewa"
|
||||
},
|
||||
{
|
||||
value: "zh-CN",
|
||||
label: "Chinese (Simplified)"
|
||||
},
|
||||
{
|
||||
value: "zh-TW",
|
||||
label: "Chinese (Traditional)"
|
||||
},
|
||||
{
|
||||
value: "co",
|
||||
label: "Corsican"
|
||||
},
|
||||
{
|
||||
value: "hr",
|
||||
label: "Croatian"
|
||||
},
|
||||
{
|
||||
value: "cs",
|
||||
label: "Czech"
|
||||
},
|
||||
{
|
||||
value: "da",
|
||||
label: "Danish"
|
||||
},
|
||||
{
|
||||
value: "dv",
|
||||
label: "Dhivehi"
|
||||
},
|
||||
{
|
||||
value: "doi",
|
||||
label: "Dogri"
|
||||
},
|
||||
{
|
||||
value: "nl",
|
||||
label: "Dutch"
|
||||
},
|
||||
{
|
||||
value: "en",
|
||||
label: "English",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
value: "eo",
|
||||
label: "Esperanto"
|
||||
},
|
||||
{
|
||||
value: "et",
|
||||
label: "Estonian"
|
||||
},
|
||||
{
|
||||
value: "ee",
|
||||
label: "Ewe"
|
||||
},
|
||||
{
|
||||
value: "tl",
|
||||
label: "Filipino"
|
||||
},
|
||||
{
|
||||
value: "fi",
|
||||
label: "Finnish"
|
||||
},
|
||||
{
|
||||
value: "fr",
|
||||
label: "French"
|
||||
},
|
||||
{
|
||||
value: "fy",
|
||||
label: "Frisian"
|
||||
},
|
||||
{
|
||||
value: "gl",
|
||||
label: "Galician"
|
||||
},
|
||||
{
|
||||
value: "ka",
|
||||
label: "Georgian"
|
||||
},
|
||||
{
|
||||
value: "de",
|
||||
label: "German"
|
||||
},
|
||||
{
|
||||
value: "el",
|
||||
label: "Greek"
|
||||
},
|
||||
{
|
||||
value: "gn",
|
||||
label: "Guarani"
|
||||
},
|
||||
{
|
||||
value: "gu",
|
||||
label: "Gujarati"
|
||||
},
|
||||
{
|
||||
value: "ht",
|
||||
label: "Haitian Creole"
|
||||
},
|
||||
{
|
||||
value: "ha",
|
||||
label: "Hausa"
|
||||
},
|
||||
{
|
||||
value: "haw",
|
||||
label: "Hawaiian"
|
||||
},
|
||||
{
|
||||
value: "iw",
|
||||
label: "Hebrew"
|
||||
},
|
||||
{
|
||||
value: "hi",
|
||||
label: "Hindi"
|
||||
},
|
||||
{
|
||||
value: "hmn",
|
||||
label: "Hmong"
|
||||
},
|
||||
{
|
||||
value: "hu",
|
||||
label: "Hungarian"
|
||||
},
|
||||
{
|
||||
value: "is",
|
||||
label: "Icelandic"
|
||||
},
|
||||
{
|
||||
value: "ig",
|
||||
label: "Igbo"
|
||||
},
|
||||
{
|
||||
value: "ilo",
|
||||
label: "Ilocano"
|
||||
},
|
||||
{
|
||||
value: "id",
|
||||
label: "Indonesian"
|
||||
},
|
||||
{
|
||||
value: "ga",
|
||||
label: "Irish"
|
||||
},
|
||||
{
|
||||
value: "it",
|
||||
label: "Italian"
|
||||
},
|
||||
{
|
||||
value: "ja",
|
||||
label: "Japanese"
|
||||
},
|
||||
{
|
||||
value: "jw",
|
||||
label: "Javanese"
|
||||
},
|
||||
{
|
||||
value: "kn",
|
||||
label: "Kannada"
|
||||
},
|
||||
{
|
||||
value: "kk",
|
||||
label: "Kazakh"
|
||||
},
|
||||
{
|
||||
value: "km",
|
||||
label: "Khmer"
|
||||
},
|
||||
{
|
||||
value: "rw",
|
||||
label: "Kinyarwanda"
|
||||
},
|
||||
{
|
||||
value: "gom",
|
||||
label: "Konkani"
|
||||
},
|
||||
{
|
||||
value: "ko",
|
||||
label: "Korean"
|
||||
},
|
||||
{
|
||||
value: "kri",
|
||||
label: "Krio"
|
||||
},
|
||||
{
|
||||
value: "ku",
|
||||
label: "Kurdish (Kurmanji)"
|
||||
},
|
||||
{
|
||||
value: "ckb",
|
||||
label: "Kurdish (Sorani)"
|
||||
},
|
||||
{
|
||||
value: "ky",
|
||||
label: "Kyrgyz"
|
||||
},
|
||||
{
|
||||
value: "lo",
|
||||
label: "Lao"
|
||||
},
|
||||
{
|
||||
value: "la",
|
||||
label: "Latin"
|
||||
},
|
||||
{
|
||||
value: "lv",
|
||||
label: "Latvian"
|
||||
},
|
||||
{
|
||||
value: "ln",
|
||||
label: "Lingala"
|
||||
},
|
||||
{
|
||||
value: "lt",
|
||||
label: "Lithuanian"
|
||||
},
|
||||
{
|
||||
value: "lg",
|
||||
label: "Luganda"
|
||||
},
|
||||
{
|
||||
value: "lb",
|
||||
label: "Luxembourgish"
|
||||
},
|
||||
{
|
||||
value: "mk",
|
||||
label: "Macedonian"
|
||||
},
|
||||
{
|
||||
value: "mai",
|
||||
label: "Maithili"
|
||||
},
|
||||
{
|
||||
value: "mg",
|
||||
label: "Malagasy"
|
||||
},
|
||||
{
|
||||
value: "ms",
|
||||
label: "Malay"
|
||||
},
|
||||
{
|
||||
value: "ml",
|
||||
label: "Malayalam"
|
||||
},
|
||||
{
|
||||
value: "mt",
|
||||
label: "Maltese"
|
||||
},
|
||||
{
|
||||
value: "mi",
|
||||
label: "Maori"
|
||||
},
|
||||
{
|
||||
value: "mr",
|
||||
label: "Marathi"
|
||||
},
|
||||
{
|
||||
value: "mni-Mtei",
|
||||
label: "Meiteilon (Manipuri)"
|
||||
},
|
||||
{
|
||||
value: "lus",
|
||||
label: "Mizo"
|
||||
},
|
||||
{
|
||||
value: "mn",
|
||||
label: "Mongolian"
|
||||
},
|
||||
{
|
||||
value: "my",
|
||||
label: "Myanmar (Burmese)"
|
||||
},
|
||||
{
|
||||
value: "ne",
|
||||
label: "Nepali"
|
||||
},
|
||||
{
|
||||
value: "no",
|
||||
label: "Norwegian"
|
||||
},
|
||||
{
|
||||
value: "or",
|
||||
label: "Odia (Oriya)"
|
||||
},
|
||||
{
|
||||
value: "om",
|
||||
label: "Oromo"
|
||||
},
|
||||
{
|
||||
value: "ps",
|
||||
label: "Pashto"
|
||||
},
|
||||
{
|
||||
value: "fa",
|
||||
label: "Persian"
|
||||
},
|
||||
{
|
||||
value: "pl",
|
||||
label: "Polish"
|
||||
},
|
||||
{
|
||||
value: "pt",
|
||||
label: "Portuguese"
|
||||
},
|
||||
{
|
||||
value: "pa",
|
||||
label: "Punjabi"
|
||||
},
|
||||
{
|
||||
value: "qu",
|
||||
label: "Quechua"
|
||||
},
|
||||
{
|
||||
value: "ro",
|
||||
label: "Romanian"
|
||||
},
|
||||
{
|
||||
value: "ru",
|
||||
label: "Russian"
|
||||
},
|
||||
{
|
||||
value: "sm",
|
||||
label: "Samoan"
|
||||
},
|
||||
{
|
||||
value: "sa",
|
||||
label: "Sanskrit"
|
||||
},
|
||||
{
|
||||
value: "gd",
|
||||
label: "Scots Gaelic"
|
||||
},
|
||||
{
|
||||
value: "nso",
|
||||
label: "Sepedi"
|
||||
},
|
||||
{
|
||||
value: "sr",
|
||||
label: "Serbian"
|
||||
},
|
||||
{
|
||||
value: "st",
|
||||
label: "Sesotho"
|
||||
},
|
||||
{
|
||||
value: "sn",
|
||||
label: "Shona"
|
||||
},
|
||||
{
|
||||
value: "sd",
|
||||
label: "Sindhi"
|
||||
},
|
||||
{
|
||||
value: "si",
|
||||
label: "Sinhala"
|
||||
},
|
||||
{
|
||||
value: "sk",
|
||||
label: "Slovak"
|
||||
},
|
||||
{
|
||||
value: "sl",
|
||||
label: "Slovenian"
|
||||
},
|
||||
{
|
||||
value: "so",
|
||||
label: "Somali"
|
||||
},
|
||||
{
|
||||
value: "es",
|
||||
label: "Spanish"
|
||||
},
|
||||
{
|
||||
value: "su",
|
||||
label: "Sundanese"
|
||||
},
|
||||
{
|
||||
value: "sw",
|
||||
label: "Swahili"
|
||||
},
|
||||
{
|
||||
value: "sv",
|
||||
label: "Swedish"
|
||||
},
|
||||
{
|
||||
value: "tg",
|
||||
label: "Tajik"
|
||||
},
|
||||
{
|
||||
value: "ta",
|
||||
label: "Tamil"
|
||||
},
|
||||
{
|
||||
value: "tt",
|
||||
label: "Tatar"
|
||||
},
|
||||
{
|
||||
value: "te",
|
||||
label: "Telugu"
|
||||
},
|
||||
{
|
||||
value: "th",
|
||||
label: "Thai"
|
||||
},
|
||||
{
|
||||
value: "ti",
|
||||
label: "Tigrinya"
|
||||
},
|
||||
{
|
||||
value: "ts",
|
||||
label: "Tsonga"
|
||||
},
|
||||
{
|
||||
value: "tr",
|
||||
label: "Turkish"
|
||||
},
|
||||
{
|
||||
value: "tk",
|
||||
label: "Turkmen"
|
||||
},
|
||||
{
|
||||
value: "ak",
|
||||
label: "Twi"
|
||||
},
|
||||
{
|
||||
value: "uk",
|
||||
label: "Ukrainian"
|
||||
},
|
||||
{
|
||||
value: "ur",
|
||||
label: "Urdu"
|
||||
},
|
||||
{
|
||||
value: "ug",
|
||||
label: "Uyghur"
|
||||
},
|
||||
{
|
||||
value: "uz",
|
||||
label: "Uzbek"
|
||||
},
|
||||
{
|
||||
value: "vi",
|
||||
label: "Vietnamese"
|
||||
},
|
||||
{
|
||||
value: "cy",
|
||||
label: "Welsh"
|
||||
},
|
||||
{
|
||||
value: "xh",
|
||||
label: "Xhosa"
|
||||
},
|
||||
{
|
||||
value: "yi",
|
||||
label: "Yiddish"
|
||||
},
|
||||
{
|
||||
value: "yo",
|
||||
label: "Yoruba"
|
||||
},
|
||||
{
|
||||
value: "zu",
|
||||
label: "Zulu"
|
||||
}
|
||||
];
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export interface SyncedLyric {
|
||||
time: number;
|
||||
text: string | null;
|
||||
}
|
||||
|
||||
export enum Provider {
|
||||
Lrclib = "LRCLIB",
|
||||
Spotify = "Spotify",
|
||||
Translated = "Translated",
|
||||
Romanized = "Romanized",
|
||||
None = "None",
|
||||
}
|
||||
|
||||
export interface LyricsData {
|
||||
lyricsVersions: Partial<Record<Provider, SyncedLyric[] | null>>;
|
||||
useLyric: Provider;
|
||||
}
|
|
@ -1,146 +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 { makeRange, SettingSliderComponent } from "@components/PluginSettings/components";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { Button, showToast, Text, Toasts, useMemo } from "@webpack/common";
|
||||
|
||||
import { clearLyricsCache, getLyricsCount, removeTranslations } from "./api";
|
||||
import { Lyrics } from "./components/lyrics";
|
||||
import { useLyrics } from "./components/util";
|
||||
import languages from "./providers/translator/languages";
|
||||
import { Provider } from "./providers/types";
|
||||
|
||||
const sliderOptions = {
|
||||
markers: makeRange(-2500, 2500, 250),
|
||||
stickToMarkers: true,
|
||||
};
|
||||
|
||||
function Details() {
|
||||
const { lyricsInfo } = useLyrics();
|
||||
|
||||
const [count, error, loading] = useAwaiter(
|
||||
useMemo(() => getLyricsCount, []),
|
||||
{
|
||||
onError: () => console.error("Failed to get lyrics count"),
|
||||
fallbackValue: null,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>Current lyrics provider: {lyricsInfo?.useLyric || "None"}</Text>
|
||||
{loading ? <Text>Loading lyrics count...</Text> : error ? <Text>Failed to get lyrics count</Text> : <Text>Lyrics count: {count}</Text>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
ShowMusicNoteOnNoLyrics: {
|
||||
description: "Show a music note icon when no lyrics are found",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
LyricsPosition: {
|
||||
description: "Position of the lyrics",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ value: "above", label: "Above SpotifyControls" },
|
||||
{ value: "below", label: "Below SpotifyControls", default: true },
|
||||
],
|
||||
},
|
||||
LyricsProvider: {
|
||||
description: "Where lyrics are fetched from",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ value: Provider.Spotify, label: "Spotify (Musixmatch)", default: true },
|
||||
{ value: Provider.Lrclib, label: "LRCLIB" },
|
||||
],
|
||||
},
|
||||
FallbackProvider: {
|
||||
description: "When a lyrics provider fails, try other providers",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
TranslateTo: {
|
||||
description: "Translate lyrics to - Changing this will clear existing translations",
|
||||
type: OptionType.SELECT,
|
||||
options: languages,
|
||||
onChange: async () => {
|
||||
await removeTranslations();
|
||||
showToast("Translations cleared", Toasts.Type.SUCCESS);
|
||||
}
|
||||
},
|
||||
LyricsConversion: {
|
||||
description: "Automatically translate or romanize lyrics",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ value: Provider.None, label: "None", default: true },
|
||||
{ value: Provider.Translated, label: "Translate" },
|
||||
{ value: Provider.Romanized, label: "Romanize" },
|
||||
]
|
||||
},
|
||||
ShowFailedToasts: {
|
||||
description: "Hide toasts when lyrics fail to fetch",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
LyricDelay: {
|
||||
description: "",
|
||||
type: OptionType.SLIDER,
|
||||
default: 0,
|
||||
hidden: true,
|
||||
...sliderOptions
|
||||
},
|
||||
Display: {
|
||||
description: "",
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<>
|
||||
<SettingSliderComponent
|
||||
option={{ ...sliderOptions } as any}
|
||||
onChange={v => {
|
||||
settings.store.LyricDelay = v;
|
||||
}}
|
||||
pluginSettings={Vencord.Settings.plugins.SpotifyLyrics}
|
||||
id={"LyricDelay"}
|
||||
onError={() => { }}
|
||||
/>
|
||||
<Lyrics />
|
||||
</>
|
||||
)
|
||||
},
|
||||
Details: {
|
||||
description: "",
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => <Details />,
|
||||
},
|
||||
PurgeLyricsCache: {
|
||||
description: "Purge the lyrics cache",
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
clearLyricsCache();
|
||||
showToast("Lyrics cache purged", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
>
|
||||
Purge Cache
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
TestingCache: {
|
||||
description: "Save songs to a testing cache instead",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
hidden: true,
|
||||
}
|
||||
});
|
||||
|
||||
export default settings;
|
|
@ -1,78 +0,0 @@
|
|||
.vc-spotify-lyrics {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-accent);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
max-height: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-music-note {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
animation: side-to-side 2s ease-in-out infinite;
|
||||
color: var(--text-muted);
|
||||
height: 18px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-next,
|
||||
.vc-spotify-lyrics-prev {
|
||||
opacity: 0.6;
|
||||
margin-bottom: 1%;
|
||||
}
|
||||
|
||||
@keyframes side-to-side {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-album-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 5%;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-modal-line-current,
|
||||
.vc-spotify-lyrics-modal-line {
|
||||
margin: 4px 0;
|
||||
padding: 4px 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-modal-line-current {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-modal-note {
|
||||
height: 1lh;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-modal-timestamp {
|
||||
color: var(--text-muted);
|
||||
margin-right: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-modal-timestamp:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.vc-spotify-lyrics-modal-no-lyrics {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel, sendMessage } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
async function handleButtonClick() {
|
||||
// @ts-expect-error typing issue
|
||||
sendMessage(getCurrentChannel().id, { content: "woof" });
|
||||
}
|
||||
|
||||
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
if (!isMainChat) return null;
|
||||
return (
|
||||
<ChatBarButton tooltip="Woof" onClick={handleButtonClick}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 576 512"><path fill="currentColor" d="m309.6 158.5l23.1-138.7C334.6 8.4 344.5 0 356.1 0c7.5 0 14.5 3.5 19 9.5L392 32h52.1c12.7 0 24.9 5.1 33.9 14.1L496 64h56c13.3 0 24 10.7 24 24v24c0 44.2-35.8 80-80 80h-69.3l-5.1 30.5zM416 256.1V480c0 17.7-14.3 32-32 32h-32c-17.7 0-32-14.3-32-32V364.8c-24 12.3-51.2 19.2-80 19.2s-56-6.9-80-19.2V480c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V249.8c-28.8-10.9-51.4-35.3-59.2-66.5L1 167.8c-4.3-17.1 6.1-34.5 23.3-38.8s34.5 6.1 38.8 23.3l3.9 15.5C70.5 182 83.3 192 98 192h205.8zM464 80a16 16 0 1 0-32 0a16 16 0 1 0 32 0" /></svg>
|
||||
</ChatBarButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "Woof",
|
||||
description: "Adds a chatbar button to woof in chat",
|
||||
authors: [Devs.Samwich],
|
||||
start: () => addChatBarButton("Woof", ChatBarIcon),
|
||||
stop: () => removeChatBarButton("Woof")
|
||||
});
|
|
@ -71,7 +71,7 @@ export async function installExt(id: string) {
|
|||
|
||||
const buf = await get(url, {
|
||||
headers: {
|
||||
"User-Agent": `Electron ${process.versions.electron} ~ Equicord (https://github.com/Equicord/Equicord)`
|
||||
"User-Agent": `Electron ${process.versions.electron} ~ Equicord (https://github.com/Rayanzay/ryncord)`
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ async function generateDebugInfoMessage() {
|
|||
|
||||
const info = {
|
||||
Equicord:
|
||||
`v${VERSION} • [${gitHash}](<https://github.com/Equicord/Equicord/commit/${gitHash}>)` +
|
||||
`v${VERSION} • [${gitHash}](<https://github.com/Rayanzay/ryncord/commit/${gitHash}>)` +
|
||||
`${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
||||
Platform: typeof DiscordNative !== "undefined" ?
|
||||
|
@ -265,8 +265,8 @@ export default definePlugin({
|
|||
<Forms.FormText>You are using a custom build of Equicord, which we do not provide support for!</Forms.FormText>
|
||||
|
||||
<Forms.FormText className={Margins.top8}>
|
||||
We only provide support for <Link href="https://github.com/Equicord/Equicord">official builds</Link>.
|
||||
Either <Link href="https://github.com/Equicord/Equilotl">switch to an official build</Link> or figure your issue out yourself.
|
||||
We only provide support for <Link href="https://github.com/Rayanzay/ryncord">official builds</Link>.
|
||||
Either <Link href="https://github.com/Rayanzay/rynstaller">switch to an official build</Link> or figure your issue out yourself.
|
||||
</Forms.FormText>
|
||||
|
||||
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue