Merge branch 'dev' into translation
This commit is contained in:
commit
aec3c153a5
239 changed files with 7448 additions and 5885 deletions
33
.github/workflows/autofix.yml
vendored
Normal file
33
.github/workflows/autofix.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: autofix.ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
autofix:
|
||||
name: autofix
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run lint fix
|
||||
run: pnpm lint:fix
|
||||
|
||||
- name: Run stylelint fix
|
||||
run: pnpm lint-styles:fix
|
||||
|
||||
- uses: autofix-ci/action@v1
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -73,7 +73,7 @@ jobs:
|
|||
gh release upload latest --clobber dist/release/*
|
||||
|
||||
- name: Upload Plugin JSONs to Equibored repo
|
||||
if: github.repository == 'Equicord/Equicord' && !vars.ACT == 'false'
|
||||
if: github.repository == 'Equicord/Equicord' && vars.ACT == 'false'
|
||||
run: |
|
||||
git config --global user.name "GitHub-Actions"
|
||||
git config --global user.email actions@github.com
|
||||
|
|
|
|||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
|
@ -83,7 +83,7 @@ jobs:
|
|||
".git/*"
|
||||
|
||||
- name: Publish extension
|
||||
if: steps.check.outputs.release == 'true' && !vars.ACT == 'false'
|
||||
if: steps.check.outputs.release == 'true' && vars.ACT == 'false'
|
||||
run: pnpx @equicord/publish-browser-extension@latest --chrome-zip
|
||||
dist/extension-chrome.zip --firefox-zip
|
||||
dist/extension-firefox.zip --firefox-sources-zip
|
||||
|
|
|
|||
345
.rules
345
.rules
|
|
@ -1,213 +1,238 @@
|
|||
# Equicord Development Guidelines
|
||||
# Equicord Development Rules
|
||||
|
||||
## Critical Rules
|
||||
## Philosophy
|
||||
|
||||
- **NEVER** edit existing license headers. New files use 2026.
|
||||
- Push back when the user idea is wrong or not correct.
|
||||
- **NEVER** git checkout files without explicit permission.
|
||||
- Always verify components/functions arent deprecated before using.
|
||||
- **NEVER** use `any` for Discord objects. Import proper types from `@vencord/discord-types`.
|
||||
- Fix with smallest possible change. No refactoring unless explicitly asked.
|
||||
- Solve the problem at hand only. No patterns for patterns sake.
|
||||
- **NEVER** overengineer. No premature abstractions, no helpers for one-time operations, no building for hypothetical futures. Three similar lines is better than a premature abstraction.
|
||||
- Write modern, robust, professional code. Use current language features, proper error handling, and clean structure.
|
||||
- Write natural human text in errors, descriptions, messages. No dashes or robotic formatting. Write like "Module not found" not "Module - not found". Keep it simple and conversational.
|
||||
|
||||
## Code Rules
|
||||
|
||||
- **DELETE** dead code, dont comment it. Git preserves history.
|
||||
- No hardcoded values, unused imports, magic numbers.
|
||||
- Import `classNameFactory` from `@utils/css` (NOT `@api/Styles` which is deprecated). **ALWAYS** use `cl()` for class names, never hardcode strings like `"vc-plugin-name-class"`.
|
||||
- When combining multiple class names use `classes()` from `@utils/misc`, not template strings like `` `${a} ${b}` ``.
|
||||
- Use utilities from `@utils/`, `@api/`, `@components/`.
|
||||
- Use `Logger` from `@utils/Logger`, not `console.log`.
|
||||
- Descriptions: capital first letter, end with period.
|
||||
- Less code with same functionality is **ALWAYS** better. If it can be done in fewer lines without losing clarity, do it.
|
||||
- Keep logic flat. Avoid deep nesting, prefer early returns, guard clauses, and simple control flow.
|
||||
- **NEVER** add comments unless explicitly asked.
|
||||
- Keep existing comments, they contain important context.
|
||||
- Use `Map`/`Set`, `.some`/`.find`, no spread in loops, `Promise.all`.
|
||||
- KISS over clever, flat over nested, explicit over implicit.
|
||||
- Solve the problem at hand. No refactors unless asked.
|
||||
- Smallest diff that fixes it. Three similar lines beats a premature helper.
|
||||
- Less code with the same behavior wins.
|
||||
- Never add comments unless asked. Keep existing comments, they carry context.
|
||||
- Delete dead code. Don't comment it out.
|
||||
- Natural human text in errors, descriptions, toasts. No dashes, no robotic phrasing. Descriptions start capitalized and end with a period.
|
||||
|
||||
## TypeScript
|
||||
|
||||
- **Prefer:** optional chaining (`?.`), nullish coalescing (`??`), `const`, arrow functions
|
||||
- **Use:** destructuring, template literals, object shorthand, array methods
|
||||
- **Style:** early returns, trust inference, inline single-use variables
|
||||
- `strict: true`, `noImplicitAny: false`, `noUncheckedIndexedAccess: false`. Annotate params explicitly. Array index access is not auto `undefined`.
|
||||
- Never `any`. Use `unknown` and narrow, or type webpack results via the `find*` generic.
|
||||
- Never `as unknown as`. Write a type guard or use `satisfies`.
|
||||
- Never `@ts-ignore`. Use `@ts-expect-error` with a one line reason only when unavoidable.
|
||||
- Never `!` non null assertion. Narrow with `if` or early return.
|
||||
- Never plain `enum`. Use `const enum` for internal closed sets. Use `as const` object plus literal union for cross boundary values.
|
||||
- Never `namespace`. Modules only.
|
||||
- Never relative `../../` when a `@alias` exists.
|
||||
- Don't optional chain through values guaranteed to exist (`settings.store`, plugin proxies, find proxies). Optional chaining is for actually optional values.
|
||||
- `import type` for type only imports.
|
||||
|
||||
## React
|
||||
## Plugin Policy
|
||||
|
||||
- Wrap complex components with `ErrorBoundary.wrap(Component, { noop: true })`.
|
||||
- Return `null` for conditional rendering, not `undefined`.
|
||||
- **Forbidden:** `React.cloneElement`, `React.isValidElement`, `React.memo()`, `React.lazy`, `React.Children`
|
||||
- Always return cleanup functions in `useEffect`.
|
||||
What does NOT get merged:
|
||||
|
||||
## Forbidden
|
||||
- Selfbot, automation of user actions, client state spoofing, anything abusing the API.
|
||||
- Plugins requiring user supplied API keys or untrusted third party endpoints.
|
||||
- Plugins targeting specific third party bots. Official Discord apps are fine.
|
||||
- CSS only plugins that just hide or restyle UI.
|
||||
- Functionality already trivial via built in plugins or Discord features.
|
||||
- New dependencies without strong justification.
|
||||
|
||||
### Code
|
||||
## Plugin Structure
|
||||
|
||||
- **No raw DOM manipulation.** No `document.querySelector`, no `MutationObserver`, no `element.style`. Always use webpack patches and React.
|
||||
- Empty catch blocks.
|
||||
- Hardcoded minified vars in patches (`e,t,n`).
|
||||
- **NEVER** use `settings.use()` with arrays. Mutate then reassign.
|
||||
- Folder per plugin. `src/plugins/<name>/index.tsx` or `src/equicordplugins/<name>/index.tsx`. Single file plugins still get a folder.
|
||||
- Folder suffixes the build system filters on: `.desktop`, `.web`, `.dev`, `.discordDesktop`, `.vesktop`, `.equibop`.
|
||||
- `export default definePlugin({ ... })`. Never named.
|
||||
- Required fields: `name`, `description`, `authors`. Use `Devs` or `EquicordDevs`, never inline objects.
|
||||
- List `dependencies` for every API plugin used.
|
||||
- Prefer the folder suffix to lock environment. The `target?: "WEB" | "DESKTOP" | "BOTH"` field exists but is rarely used. Runtime `IS_DISCORD_DESKTOP`, `IS_WEB`, `IS_VESKTOP` (build time replaced) for partial gating inside a mixed plugin.
|
||||
- `requiresRestart` defaults to true when `patches` exist. Set it only to override.
|
||||
|
||||
### Plugins
|
||||
### Declarative over imperative
|
||||
|
||||
- **No selfbot or API abuse.** Nothing that automates user actions, spoofs client state, or abuses Discord's API in any way.
|
||||
- No plugins that only do something trivially achievable with existing built-in plugins or Discord features.
|
||||
- No CSS-only plugins that only hide or redesign UI elements.
|
||||
- No plugins targeting specific third-party bots (official Discord apps are fine).
|
||||
- No untrusted third-party APIs. No plugins requiring users to supply their own API keys.
|
||||
- Do not introduce new dependencies unless strictly necessary and well justified.
|
||||
Use these plugin object fields instead of calling `add*` or `remove*` manually. `PluginManager` handles lifecycle and auto enables the matching API plugin.
|
||||
|
||||
## Anti-Patterns
|
||||
`commands`, `contextMenus`, `flux`, `onBeforeMessageSend`, `onBeforeMessageEdit`, `onMessageClick`, `chatBarButton`, `chatBarButtonWrapper`, `messagePopoverButton`, `renderMessageAccessory`, `renderMessageDecoration`, `renderMemberListDecorator`, `renderNicknameIcon`, `renderProfileCollection`, `renderProfileSection`, `headerBarButton`, `userAreaButton`, `userProfileBadge`, `managedStyle`, `toolboxActions`, `audioProcessor`.
|
||||
|
||||
- `value !== null && value !== undefined` ⟹ `value != null`
|
||||
- `array && array.length > 0` ⟹ `array.length` (only when array is guaranteed to exist)
|
||||
- `settings?.store?.value` ⟹ `settings.store.value`
|
||||
- `value || defaultValue` ⟹ `value ?? defaultValue`
|
||||
Wrapper style fields take `{ render | wrapper, priority }`, never a bare function.
|
||||
|
||||
Imperative `add*` and `remove*` is only valid when registering multiple items of the same kind, registering conditionally, or registering after `start()`. Always pair with removal in `stop()`. Never mix the two for the same item.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
- `start()` runs at `StartAt.WebpackReady` by default.
|
||||
- `stop()` must reverse everything `start()` did.
|
||||
- Never throw from `start` or `stop`.
|
||||
- Plugin methods called via `$self` must be on the plugin object. Closures over module locals aren't callable.
|
||||
- Never `Vencord.Plugins.plugins["Name"]`. Import the plugin directly and use `isPluginEnabled(name)` from `@api/PluginManager`.
|
||||
|
||||
## Settings
|
||||
|
||||
- `definePluginSettings({...})` from `@api/Settings`. Assign to `settings`.
|
||||
- Every non SELECT setting needs `default`. SELECT puts `default: true` on one option.
|
||||
- `restartNeeded: true` only when the value feeds a patch replacement or eager load decision.
|
||||
- `hidden` and `disabled` can be functions for conditional UI. Never delete keys to "remove" them. Write a migration.
|
||||
- `withPrivateSettings<T>()` for internal state. Never widen the global `Settings` interface.
|
||||
- Renames go at module top before `definePlugin`. Use `migratePluginSettings("New", "Old", ...)` and `migratePluginSetting(...)`.
|
||||
|
||||
### Reading
|
||||
|
||||
- React: `settings.use([...specificKeys])`. Hoist the keys array to module scope or memoize it. A fresh array literal resubscribes every render.
|
||||
- Never `settings.use()` with no argument inside list rendered components.
|
||||
- Hot paths: destructure `settings.store.x` once. Never re read in loops or render rows.
|
||||
- `OptionType.COMPONENT` components must be module level. Use the `setValue` and `setError` props. Never write `settings.store.x` directly inside them.
|
||||
|
||||
## Flux
|
||||
|
||||
- Plugin lifetime subscriptions: `flux: { ACTION_TYPE(payload) { ... } }` on the plugin object.
|
||||
- Component scoped or one shot: `FluxDispatcher.subscribe` inside `useEffect`, paired with `unsubscribe(sameRef)` in cleanup.
|
||||
- Never declare both `flux` and manual `subscribe` for the same event in one plugin.
|
||||
|
||||
## Patching
|
||||
|
||||
### Core Principles
|
||||
- One patch per concern. Minimum code touched.
|
||||
- `find` is a string when possible. Anchor on stable tokens: intl keys (`#{intl::KEY}`), method names with trailing punctuation, action type literals, distinct CSS or path literals.
|
||||
- Prefer intl key anchors over English copy.
|
||||
- `match` is always a RegExp. Use `\i` for every minified identifier. Never letters or named patterns.
|
||||
- Bound every gap with `.{0,N}?`. Never `.+?` or `.*?` unbounded. Cap at the smallest N that works.
|
||||
- Use lookbehind and lookahead to inject without re consuming context.
|
||||
- Capture only what you reuse. Convert reused only groups to `(?:...)` or move to lookarounds.
|
||||
- `#{intl::KEY}` in match. The build hashes it. Never hardcode hashed forms like `.aA4Vce`.
|
||||
- `/g` only when intentionally hitting multiple sites in one module.
|
||||
- Replace: `$&` keeps the match, `$1` and `$2` for captures. Call into plugin code via `$self.fn(...)`.
|
||||
- Never inject a statement where Discord expects an expression.
|
||||
- `group: true` when multiple replacements in one patch must land together.
|
||||
- Scope conditional patches with `predicate`, `fromBuild`, `toBuild`. Never branch inside `replace`.
|
||||
- `all: true` for intl patches that may match across multiple bundles (English fallback plus user locale).
|
||||
- Never `try/catch` to silence a failing replacement. Fix the match.
|
||||
- Never depend on argument names or object key order. Use `arguments[i]`.
|
||||
- Prefer multiple small patches with tight `find`s over one patch with a broad find and many replacements.
|
||||
|
||||
- Minimum code touched. Stability over cleverness. One patch per concern.
|
||||
- Try multiple approaches, keep simplifying until you find the cleanest solution.
|
||||
- Remove any regex character that doesnt change the match result.
|
||||
- Not overengineered, not overlooked, just clean and future-proof.
|
||||
|
||||
### Find Strings
|
||||
|
||||
- Use `#{intl::KEY}` in find when possible, most stable anchor.
|
||||
- If find matches multiple modules, add nearby stable string like `"),icon:"` or function name.
|
||||
- `"#{intl::PIN_MESSAGE}),icon:"` is better than generic strings.
|
||||
|
||||
### Match Patterns
|
||||
|
||||
- Match only what you need to replace, let find do the targeting.
|
||||
- Can use `#{intl::KEY}` in match too, gets canonicalized to hash.
|
||||
- `/#{intl::KEY}\)/` beats `/label:\i\.pinned\?.{0,60}#{intl::KEY}\)/`
|
||||
- `$&` keeps original, append/prepend to it.
|
||||
- Use bounded gaps: `.{0,50}` not `.+?` or `.*?`
|
||||
- Only use capture groups if reusing in replace.
|
||||
|
||||
### NEVER Do
|
||||
|
||||
- **Hardcoded minified vars:** `e,t,n,r,i,o,s,l,c,u,$_,xx,eD,eH,eW` — use `\i` instead
|
||||
- **Minified chains:** `\i\.\i` alone — surround with stable strings
|
||||
- **Unbounded gaps:** `.+?` or `.*?` — use `.{0,N}`
|
||||
- **Generic patterns:** `/className:\i/` alone — add anchor
|
||||
- **Raw intl hashes:** `.aA4Vce` — use `#{intl::KEY_NAME}`
|
||||
|
||||
### Patch Examples
|
||||
|
||||
**Clean replacement:**
|
||||
### Examples
|
||||
|
||||
```js
|
||||
// clean replacement
|
||||
find: "#{intl::PIN_MESSAGE}),icon:"
|
||||
match: /#{intl::PIN_MESSAGE}\)/
|
||||
replace: "$self.getPinLabel(arguments[0]))"
|
||||
```
|
||||
|
||||
**Appending:**
|
||||
|
||||
```js
|
||||
// appending
|
||||
find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}"
|
||||
match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/
|
||||
replace: "$&,$self.renderTooltip(arguments[0].guild)"
|
||||
```
|
||||
|
||||
**Wrapping:**
|
||||
|
||||
```js
|
||||
// wrapping with lookbehind
|
||||
find: "#{intl::SEVERAL_USERS_TYPING_STRONG}"
|
||||
match: /(?<="aria-atomic":!0,children:)\i/
|
||||
replace: "$self.renderTypingUsers({ children: $& })"
|
||||
```
|
||||
|
||||
## Plugin Interop
|
||||
## Webpack access
|
||||
|
||||
- `isPluginEnabled(name)` from `@api/PluginManager` takes a string, checks required/isDependency/enabled.
|
||||
- Import the plugin directly from its path to access `.name` and functions.
|
||||
- Dont use `Vencord.Plugins.plugins` or `plugin.started`. Dont use `"as unknown as"` casting.
|
||||
- Top level: lazy finders only. `findByPropsLazy`, `findByCodeLazy`, `findComponentByCodeLazy`, `findStoreLazy`, `findCssClassesLazy`, `getUserSettingLazy`, `proxyLazyWebpack`.
|
||||
- Direct `find(...)` only inside `start()` or later runtime callbacks.
|
||||
- Use `findComponentByCode` or `waitForComponent` for Discord components, not `findByProps`. Survives bundle reshuffles.
|
||||
- `proxyLazy` for values needing same tick destructuring. `makeLazy` for plain deferred values.
|
||||
- `@webpack/common` exports backed by `waitFor` (stores, `FluxDispatcher`, `Parser`, `Alerts`, hooks) are `undefined` until webpack is ready. Reference inside functions, not at module top.
|
||||
- No `eagerPatches` or `wreq(id)` brute forcing.
|
||||
|
||||
```ts
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import otherPlugin from "@equicordplugins/otherPlugin";
|
||||
if (!isPluginEnabled(otherPlugin.name)) return null;
|
||||
otherPlugin.someFunction();
|
||||
```
|
||||
## React
|
||||
|
||||
- Import hooks and `React` from `@webpack/common`. Never `import React from "react"`. Only `import type` from `"react"` (`ReactNode`, `ComponentType`, etc.).
|
||||
- Forbidden: `React.FC`, `React.memo`, `React.lazy`, `React.Children`, `React.cloneElement`, `React.isValidElement`. Type props with a named `interface`.
|
||||
- Conditional rendering returns `null`, never `undefined` or `false`.
|
||||
- Wrap every component injected by a patch with `ErrorBoundary.wrap(C, { noop: true })`. Same for components exported as plugin surface.
|
||||
- `LazyComponent(() => findByCode(...))` for webpack sourced components used before webpack is ready.
|
||||
- `useStateFromStores([Store1, ...], () => derive(), deps?, areEqual?)`. List every store the selector reads. Pass `areEqual` when the selector returns objects or arrays. Selector must be pure and synchronous.
|
||||
|
||||
## CSS / Classes
|
||||
|
||||
- `classNameFactory` from `@utils/css`. Never from `@api/Styles`. That re export is deprecated, kept for Vesktop backcompat.
|
||||
- Prefix is `vc-<plugin>-`. Usage: `cl("base", ["a", "b"], { active: cond })`.
|
||||
- Combine classes with `classes(a, b)` from `@utils/misc`. Never template strings, never `+`.
|
||||
- Discord mangled classes via `findCssClassesLazy("propA", ...)`. Never hardcode mangled suffixes.
|
||||
- Static stylesheet: `import "./style.css"`. Runtime toggleable: `import name from "./style.css?managed"` plus `enableStyle`, `disableStyle`, `toggleStyle`, `setStyleClassNames` from `@api/Styles`.
|
||||
- CSS variable selectors in managed CSS use `[--varName]`. The build rewrites these via the `setStyleClassNames` map. Never hardcode mangled selectors in CSS.
|
||||
- Every selector in plugin CSS starts with `.vc-<plugin>-`. No bare element selectors, no `:root`, no `@import`.
|
||||
- Prefer Discord CSS vars like `var(--background-primary)` over literal colors.
|
||||
- Inline `style={{}}` only for dynamic per render values.
|
||||
- No `!important` except to override a Discord rule unbeatable by specificity.
|
||||
|
||||
## Native / Desktop
|
||||
|
||||
- Desktop only plugin: folder suffix `.desktop`. Mixed target plugins with a `native.ts` do not use the suffix.
|
||||
- `native.ts` next to `index.tsx`. Exports auto register as IPC handlers via `~pluginNatives`.
|
||||
- Handler first arg is `IpcMainInvokeEvent`. Renderer side:
|
||||
```ts
|
||||
const Native = VencordNative.pluginHelpers.MyPlugin as PluginNative<typeof import("./native")>;
|
||||
```
|
||||
- `pluginHelpers` key matches `definePlugin.name`.
|
||||
- Renderer must never import `fs`, `path`, `net`, `https`, `child_process`, `electron`. Go through `native.ts` and validate input there.
|
||||
- File pickers: `saveFile` and `chooseFile` from `@utils/web`. Cross platform, no IPC needed.
|
||||
- Only `src/preload.ts` may `contextBridge.exposeInMainWorld`.
|
||||
|
||||
## Never hardcode
|
||||
|
||||
- CDN URLs (`cdn.discordapp.com/...`) → `IconUtils`.
|
||||
- API paths (`/api/v9`, `/users/@me`) → `Constants.Endpoints` or `RestAPI`.
|
||||
- Intl strings → `#{intl::KEY}`.
|
||||
- Mangled CSS classes → `findCssClassesLazy`.
|
||||
- Minified vars in patches (`e,t,n,r,i,o,s,...`) → `\i`.
|
||||
- `console.log`, `warn`, `error` → `new Logger("name")` from `@utils/Logger`.
|
||||
- Toast type strings → `Toasts.Type.*`.
|
||||
- Class name concatenation → `classes(...)`.
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Raw DOM. No `document.querySelector`, `MutationObserver`, `element.style`. Use webpack patches and React.
|
||||
- Empty `catch` blocks.
|
||||
- `settings.use()` with arrays mutated in place. Reassign with a new array.
|
||||
|
||||
## Reference
|
||||
|
||||
### Types
|
||||
### Types from `@vencord/discord-types`
|
||||
`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message`, `Command`, `FluxEvents`, store types.
|
||||
|
||||
`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message` from `@vencord/discord-types`
|
||||
### `@api/*`
|
||||
`Settings`, `Commands`, `DataStore`, `ContextMenu`, `Badges`, `Notices`, `Notifications`, `MessageEvents`, `MessageUpdater`, `MessageAccessories`, `MessageDecorations`, `MessagePopover`, `MemberListDecorators`, `ServerList`, `ChatButtons`, `HeaderBar`, `UserArea`, `NicknameIcons`, `ProfileCollections`, `ProfileSections`, `AudioPlayer`, `UserSettings`, `Styles`, `PluginManager`.
|
||||
|
||||
### Components
|
||||
Deprecated: `@api/Themes`, and the `classNameFactory` re export from `@api/Styles`.
|
||||
|
||||
`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard` from `@components/`
|
||||
### `@utils/*`
|
||||
- `Logger`. `new Logger(name)`.
|
||||
- `css`. `classNameFactory`, `classNameToSelector`.
|
||||
- `misc`. `classes`, `sleep`, `isObject`, `isObjectEmpty`, `pluralise`, `parseUrl`, `identity`, `interpolateIfDefined`, `tryOrElse`, `isPluginDev`, `removeFromArray`.
|
||||
- `text`. `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp`, `stripIndent`, `ZWSP`.
|
||||
- `react`. `useAwaiter`, `useForceUpdater`, `useIntersection`, `useTimer`, `useFixedTimer`, `useCleanupEffect`, `NoopComponent`.
|
||||
- `lazy`. `makeLazy`, `proxyLazy`.
|
||||
- `lazyReact`. `LazyComponent`.
|
||||
- `modal`. `openModal`, `openModalLazy`, `closeModal`, `closeAllModals`, `openMediaModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalFooter`, `ModalCloseButton`, `ModalSize`.
|
||||
- `discord`. `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openUserProfile`, `openPrivateChannel`, `sendMessage`, `insertTextIntoChatInputBox`, `copyWithToast`, `getUniqueUsername`, `fetchUserProfile`, `openImageModal`, `openInviteModal`, `getTheme`, `Theme`, `hasGuildFeature`.
|
||||
- `clipboard`. `copyToClipboard`, `readClipboard`.
|
||||
- `margins`. `Margins`.
|
||||
- `guards`. `isTruthy`, `isNonNullish`.
|
||||
- `web`. `saveFile`, `chooseFile`.
|
||||
- `constants`. `Devs`, `EquicordDevs`, `IS_WINDOWS`, `IS_MAC`, `IS_LINUX`, `IS_MOBILE`.
|
||||
- `types`. `OptionType`, `StartAt`, `ReporterTestable`, `definePlugin`.
|
||||
|
||||
### Settings
|
||||
### `@components/*`
|
||||
`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard`, `ErrorBoundary`.
|
||||
|
||||
Use `definePluginSettings` from `@api/Settings`, not inline settings object.
|
||||
### `@webpack/common`
|
||||
|
||||
### Utilities
|
||||
Stores. `UserStore`, `GuildStore`, `GuildRoleStore`, `GuildMemberStore`, `GuildChannelStore`, `ChannelStore`, `SelectedChannelStore`, `SelectedGuildStore`, `MessageStore`, `PresenceStore`, `RelationshipStore`, `PermissionStore`, `ReadStateStore`, `EmojiStore`, `StickersStore`, `ThemeStore`, `VoiceStateStore`, `TypingStore`, `DraftStore`, `WindowStore`, `AccessibilityStore`, `PendingReplyStore`, `UserProfileStore`, `StreamerModeStore`, `SpotifyStore`, `NotificationSettingsStore`, `LocaleStore`, `ExperimentStore`, `QuestStore`. Full list in `src/webpack/common/stores.ts`.
|
||||
|
||||
- **@utils/clipboard** — `copyToClipboard`
|
||||
- **@utils/discord** — `insertTextIntoChatInputBox`, `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openUserProfile`, `openPrivateChannel`, `sendMessage`, `copyWithToast`, `getUniqueUsername`, `fetchUserProfile`
|
||||
- **@utils/css** — `classNameFactory`, `classNameToSelector`
|
||||
- **@utils/misc** — `classes`, `sleep`, `isObject`, `isObjectEmpty`, `pluralise`, `parseUrl`, `identity`
|
||||
- **@utils/text** — `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp`
|
||||
- **@utils/modal** — `openModal`, `closeModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalCloseButton`
|
||||
- **@utils/margins** — `Margins` (`Margins.top8`, `Margins.bottom16`, etc.)
|
||||
- **@utils/guards** — `isTruthy`, `isNonNullish`
|
||||
- **@utils/web** — `saveFile`, `chooseFile`
|
||||
React and hooks. `React`, `useState`, `useEffect`, `useLayoutEffect`, `useMemo`, `useRef`, `useReducer`, `useCallback`, `useStateFromStores`.
|
||||
|
||||
### Webpack Common
|
||||
Components. `Tooltip`, `TextInput`, `TextArea`, `Select`, `SearchableSelect`, `Slider`, `Checkbox`, `Avatar`, `Menu`, `Popout`, `Dialog`, `TabBar`, `Clickable`, `MaskedLink`, `Timestamp`, `FocusLock`, `UserSummaryItem`, `ColorPicker`, `OAuth2AuthorizeModal`, `ScrollerThin`, `ScrollerAuto`, `ScrollerNone`, `ListScrollerThin`, `ListScrollerAuto`.
|
||||
|
||||
#### IconUtils
|
||||
Utils. `FluxDispatcher`, `RestAPI`, `Constants`, `SnowflakeUtils`, `Parser`, `PermissionsBits`, `Alerts`, `Toasts`, `showToast`, `moment`, `lodash`, `i18n`, `IconUtils`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`, `URLUtils`, `Humanize`, `EmojiUtils`, `UserUtils`, `UploadHandler`, `UploadManager`, `ComponentDispatch`, `ExpressionPickerStore`, `ReadStateUtils`, `PopoutActions`, `ApplicationAssetUtils`.
|
||||
|
||||
From `@webpack/common`. **NEVER** hardcode `cdn.discordapp.com` URLs.
|
||||
Routers. `NavigationRouter`, `ChannelRouter`, `SettingsRouter`.
|
||||
|
||||
- `IconUtils.getUserAvatarURL(user, canAnimate?, size?)`
|
||||
- `IconUtils.getDefaultAvatarURL(id)`
|
||||
- `IconUtils.getUserBannerURL({ id, banner, canAnimate?, size })`
|
||||
- `IconUtils.getGuildIconURL({ id, icon, size?, canAnimate? })`
|
||||
- `IconUtils.getGuildBannerURL(guild, canAnimate?)`
|
||||
- `IconUtils.getChannelIconURL({ id, icon })`
|
||||
- `IconUtils.getEmojiURL({ id, animated, size })`
|
||||
- `IconUtils.getApplicationIconURL(data)`
|
||||
- `IconUtils.getGameAssetURL(data)`
|
||||
Action creators. `MessageActions`, `ChannelActionCreators`, `UserProfileActions`, `InviteActions`, `GuildActions`, `VoiceActions`, `DraftActions`, `PinActions`, `MessageCache`.
|
||||
|
||||
#### Stores
|
||||
Menu. `Menu`, `ContextMenuApi` (`openContextMenu`, `closeContextMenu`, `openContextMenuLazy`).
|
||||
|
||||
`UserStore`, `GuildStore`, `ChannelStore`, `GuildMemberStore`, `SelectedChannelStore`, `SelectedGuildStore`, `PresenceStore`, `RelationshipStore`, `MessageStore`, `EmojiStore`, `ThemeStore`, `PermissionStore`, `VoiceStateStore` from `@webpack/common`
|
||||
### Lazy webpack imports
|
||||
From `@webpack`. `findByPropsLazy`, `findByCodeLazy`, `findComponentLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`, `findStoreLazy`, `findCssClassesLazy`, `findLazy`, `proxyLazyWebpack`, `LazyComponentWebpack`, `mapMangledModuleLazy`, `extractAndLoadChunksLazy`, `waitFor`.
|
||||
|
||||
#### Actions
|
||||
`waitForStore` and `waitForComponent` live in `@webpack/common/internal.tsx`. They're used by `@webpack/common` itself, not directly by plugins.
|
||||
|
||||
`RestAPI`, `FluxDispatcher`, `MessageActions`, `NavigationRouter`, `ChannelRouter`, `ChannelActionCreators`, `SettingsRouter` from `@webpack/common`
|
||||
|
||||
#### Utils
|
||||
|
||||
`Constants` (`Constants.Endpoints`), `SnowflakeUtils`, `Parser`, `PermissionsBits`, `moment`, `lodash`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`, `URLUtils`, `Humanize`, `EmojiUtils` from `@webpack/common`
|
||||
|
||||
#### Components
|
||||
|
||||
`Tooltip`, `TextInput`, `TextArea`, `Select`, `Slider`, `Avatar`, `Menu`, `Popout`, `ScrollerThin`, `Timestamp` from `@webpack/common`
|
||||
|
||||
#### Toasts
|
||||
|
||||
`Toasts`, `showToast` from `@webpack/common`
|
||||
|
||||
### Imports
|
||||
|
||||
- **Lazy** from `@webpack` — `findByPropsLazy`, `findStoreLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`
|
||||
- **Common** from `@webpack/common` — `useState`, `useEffect`, `useCallback`, `useStateFromStores`
|
||||
|
||||
### Never Hardcode
|
||||
|
||||
- `"cdn.discordapp.com/avatars/"` etc. ⟹ `IconUtils`
|
||||
- `"/api/v9"`, `"/users/@me"` ⟹ `Constants.Endpoints` or `RestAPI`
|
||||
- `console.log/warn/error` ⟹ `Logger` from `@utils/Logger`
|
||||
- `` `${a} ${b}` `` for class names ⟹ `classes(a, b)` from `@utils/misc`
|
||||
### `IconUtils` methods
|
||||
`getUserAvatarURL(user, canAnimate?, size?, format?)`, `getUserBannerURL({ id, banner, canAnimate?, size })`, `getGuildIconURL({ id, icon, size?, canAnimate? })`, `getGuildBannerURL(guild, canAnimate?)`, `getGuildSplashURL`, `getGuildDiscoverySplashURL`, `getGuildHomeHeaderURL`, `getChannelIconURL({ id, icon })`, `getResourceChannelIconURL`, `getEmojiURL({ id, animated, size })`, `getApplicationIconURL`, `getGameAssetURL`, `getVideoFilterAssetURL`, `getAvatarDecorationURL`, `getGuildMemberAvatarURL`, `getGuildMemberAvatarURLSimple`, `getGuildMemberBannerURL`, `getDefaultAvatarURL(id)`, `hasAnimatedGuildIcon`, `isAnimatedIconHash`.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ Windows
|
|||
|
||||
MacOS
|
||||
|
||||
- [GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl.MacOS.zip)
|
||||
- [X64 GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-darwin-x64.zip)
|
||||
- [ARM64 GUI](https://github.com/Equicord/Equilotl/releases/latest/download/Equilotl-darwin-arm64.zip)
|
||||
|
||||
Linux
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ window.VencordNative = {
|
|||
|
||||
native: {
|
||||
getVersions: () => ({}),
|
||||
supportsWindowsMaterial: () => false,
|
||||
openExternal: async (url) => void open(url, "_blank"),
|
||||
getRendererCss: async () => {
|
||||
if (IS_USERSCRIPT)
|
||||
|
|
|
|||
33
package.json
33
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "equicord",
|
||||
"private": "true",
|
||||
"version": "1.14.12.0",
|
||||
"version": "1.14.13.0",
|
||||
"description": "The other cutest Discord client mod",
|
||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||
"bugs": {
|
||||
|
|
@ -33,8 +33,11 @@
|
|||
"repair": "node scripts/runInstaller.mjs -- --repair",
|
||||
"lint": "eslint",
|
||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||
"lint-styles:fix": "pnpm lint-styles --fix",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm generatePluginJson",
|
||||
"lint:intl": "node scripts/lintIntl.mjs",
|
||||
"lint:patches": "node scripts/lintPatches.mjs",
|
||||
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm lint:intl && pnpm lint:patches && pnpm generatePluginJson",
|
||||
"testWeb": "pnpm lint:fix && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit",
|
||||
"depCheck": "node scripts/depCheck.mjs"
|
||||
|
|
@ -98,31 +101,7 @@
|
|||
"typescript-transform-paths": "^3.5.6",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@types/less@3.0.8": "patches/@types__less@3.0.8.patch"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"eslint-plugin-import",
|
||||
"eslint"
|
||||
]
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"source-map-resolve": "*",
|
||||
"resolve-url": "*",
|
||||
"source-map-url": "*",
|
||||
"urix": "*",
|
||||
"q": "*"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
],
|
||||
"overrides": {
|
||||
"@eslint/plugin-kit@<0.3.4": ">=0.3.4"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@11.0.9",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
|
|||
1
packages/discord-types/src/components.d.ts
vendored
1
packages/discord-types/src/components.d.ts
vendored
|
|
@ -192,6 +192,7 @@ export type TextInput = ComponentType<PropsWithChildren<{
|
|||
export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
|
||||
onChange(v: string): void;
|
||||
inputRef?: Ref<HTMLTextAreaElement>;
|
||||
autosize?: boolean;
|
||||
}>;
|
||||
|
||||
export interface SelectOption {
|
||||
|
|
|
|||
136
packages/discord-types/src/components/Modal.d.ts
vendored
Normal file
136
packages/discord-types/src/components/Modal.d.ts
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { ComponentType, ReactNode, Ref, RefObject } from "react";
|
||||
import { LiteralUnion } from "type-fest";
|
||||
|
||||
export type ModalActionVariant = LiteralUnion<"primary" | "secondary" | "critical-primary", string>;
|
||||
export type ModalSize = LiteralUnion<"sm" | "md" | "lg" | "xl" | "xxl", string>;
|
||||
|
||||
export interface ModalAction {
|
||||
text: string;
|
||||
variant: ModalActionVariant;
|
||||
onClick(): void;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal with all options: https://files.catbox.moe/c8qxt0.png
|
||||
*/
|
||||
export interface ModalProps extends RenderModalProps {
|
||||
size?: ModalSize;
|
||||
role?: "alertdialog" | "dialog";
|
||||
|
||||
title: ReactNode;
|
||||
/** Optional subtitle, will render below title (duh) */
|
||||
subtitle?: ReactNode;
|
||||
|
||||
/** Optional Modal content */
|
||||
children?: ReactNode;
|
||||
input?: ReactNode;
|
||||
preview?: ReactNode;
|
||||
|
||||
listProps?: any;
|
||||
onScroll?(): void;
|
||||
scrollerRef?: Ref<HTMLDivElement>;
|
||||
|
||||
/** Action buttons at the bottom of the Modal */
|
||||
actions?: ModalAction[];
|
||||
/** Custom component to show before actions, useful for e.g. "Don't show again" checkbox */
|
||||
actionBarInput?: ReactNode;
|
||||
actionBarInputLayout?: "default" | "chat-input";
|
||||
|
||||
/** Shows a Notice (card) between (sub-)title and content (children) */
|
||||
notice?: {
|
||||
message: string;
|
||||
type: LiteralUnion<"critical", string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RenderModalProps {
|
||||
transitionState: number;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
/** Wrapper around Modal */
|
||||
export interface ConfirmModalProps extends ModalProps {
|
||||
/** Variant for the confirm button, defaults to critical */
|
||||
variant?: ModalActionVariant;
|
||||
confirmText: ModalAction["text"];
|
||||
/** Defaults to "Cancel" */
|
||||
cancelText?: ModalAction["text"];
|
||||
/**
|
||||
* If onConfirm runs without error, the modal will close.
|
||||
* To indicate an error, throw an Error. This will show the user a notice inside the Modal and keep it open.
|
||||
* This notice will either be a generic message if you just throw. If you call setError, it will show that message.
|
||||
* Even if you call setError, you still have to throw to keep the Modal open
|
||||
*/
|
||||
onConfirm?(setError: (error: string) => void): void;
|
||||
/** Optional callback that runs when the user cancels the action. Whether provided or not, the Modal will close when user clicks Cancel. */
|
||||
onCancel?(): void;
|
||||
onCloseCallback?(): void;
|
||||
/** Checkbox that shows before the action buttons */
|
||||
checkboxProps?: {
|
||||
/** Defaults to "Don't show again" */
|
||||
label?: string;
|
||||
checked: boolean;
|
||||
onChange(checked: boolean): void;
|
||||
};
|
||||
|
||||
/** Custom Modal content */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export type Modal = ComponentType<ModalProps>;
|
||||
export type ConfirmModal = ComponentType<ConfirmModalProps>;
|
||||
|
||||
export type RenderModal = (props: RenderModalProps) => ReactNode;
|
||||
|
||||
export interface ModalOptions {
|
||||
modalKey?: string;
|
||||
onCloseRequest?(): void;
|
||||
onCloseCallback?(): void;
|
||||
}
|
||||
|
||||
export interface ModalAPI {
|
||||
/**
|
||||
* Wait for the render promise to resolve, then open a modal with it.
|
||||
* This is usually preferable to render().then(openModal), because it shows a Loading modal
|
||||
*/
|
||||
openModalLazy: (renderModal: () => Promise<RenderModal>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;
|
||||
/**
|
||||
* Open a Modal with the given render function.
|
||||
*/
|
||||
openModal: (renderModal: RenderModal, options?: ModalOptions, contextKey?: string) => string;
|
||||
/**
|
||||
* Close a modal by its key
|
||||
*/
|
||||
closeModal: (modalKey: string, contextKey?: string) => void;
|
||||
/**
|
||||
* Close all open modals
|
||||
*/
|
||||
closeAllModals: () => void;
|
||||
}
|
||||
|
||||
export interface MediaModalItem {
|
||||
url: string;
|
||||
type: "IMAGE" | "VIDEO" | "CLIP";
|
||||
original?: string;
|
||||
alt?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
animated?: boolean;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export interface MediaModalProps {
|
||||
location?: string;
|
||||
contextKey?: string;
|
||||
onCloseCallback?: () => void;
|
||||
className?: string;
|
||||
items: MediaModalItem[];
|
||||
startingIndex?: number;
|
||||
onIndexChange?: (...args: any[]) => void;
|
||||
fit?: string;
|
||||
shouldRedactExplicitContent?: boolean;
|
||||
shouldHideMediaOptions?: boolean;
|
||||
}
|
||||
2
packages/discord-types/src/index.d.ts
vendored
2
packages/discord-types/src/index.d.ts
vendored
|
|
@ -1,5 +1,7 @@
|
|||
export * from "./common";
|
||||
// TODO: Splitting up components into files in the folder would be good
|
||||
export * from "./components";
|
||||
export * from "./components/Modal";
|
||||
export * from "./flux";
|
||||
export * from "./fluxEvents";
|
||||
export * from "./menu";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,23 @@
|
|||
packages:
|
||||
- packages/*
|
||||
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
|
||||
overrides:
|
||||
"@eslint/plugin-kit@<0.3.4": ">=0.3.4"
|
||||
|
||||
patchedDependencies:
|
||||
"@types/less@3.0.8": patches/@types__less@3.0.8.patch
|
||||
|
||||
peerDependencyRules:
|
||||
ignoreMissing:
|
||||
- eslint-plugin-import
|
||||
- eslint
|
||||
|
||||
allowedDeprecatedVersions:
|
||||
source-map-resolve: "*"
|
||||
resolve-url: "*"
|
||||
source-map-url: "*"
|
||||
urix: "*"
|
||||
q: "*"
|
||||
|
|
|
|||
54
scripts/lintIntl.mjs
Normal file
54
scripts/lintIntl.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, sep } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
|
||||
const VALID_MODIFIERS = new Set(["raw", "hash"]);
|
||||
const MARKER_RE = /#\{intl::([\w$+/]*)(?:::(\w+))?\}/g;
|
||||
const HASH_RE = /["'`]\s*\.([A-Za-z][A-Za-z0-9+/]{5})\b/g;
|
||||
|
||||
const tracked = execFileSync("git", ["ls-files", "src"], { cwd: ROOT, encoding: "utf8" })
|
||||
.split("\n")
|
||||
.filter(p => /\.(ts|tsx|js|jsx|mjs)$/.test(p))
|
||||
.map(p => p.replace(/\//g, sep));
|
||||
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
|
||||
for (const rel of tracked) {
|
||||
const text = readFileSync(join(ROOT, rel), "utf8");
|
||||
if (!text.includes("#{intl::") && !text.includes("find:") && !text.includes("match:")) continue;
|
||||
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const at = `${rel.replace(/\\/g, "/")}:${i + 1}`;
|
||||
|
||||
if (/#\{intl::[^}]*$/.test(line)) {
|
||||
console.error(`${at}: ERROR unclosed #{intl:: marker`);
|
||||
errors++;
|
||||
}
|
||||
for (const [full, key, modifier] of line.matchAll(MARKER_RE)) {
|
||||
if (!key) {
|
||||
console.error(`${at}: ERROR empty intl key: ${full}`);
|
||||
errors++;
|
||||
}
|
||||
if (modifier && !VALID_MODIFIERS.has(modifier)) {
|
||||
console.error(`${at}: ERROR unknown intl modifier "${modifier}": ${full}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
if (/\b(find|match)\s*:/.test(line) && !line.includes("#{intl::")) {
|
||||
for (const [, hash] of line.matchAll(HASH_RE)) {
|
||||
if (!/[0-9+/]/.test(hash)) continue;
|
||||
console.warn(`${at}: WARN baked intl hash ".${hash}", prefer #{intl::KEY}`);
|
||||
warnings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nintl lint: ${errors} error(s), ${warnings} warning(s)`);
|
||||
process.exit(errors > 0 ? 1 : 0);
|
||||
70
scripts/lintPatches.mjs
Normal file
70
scripts/lintPatches.mjs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, sep } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
|
||||
const VERBOSE = process.env.LINT_PATCHES_VERBOSE === "1" || process.argv.includes("--verbose");
|
||||
|
||||
const tracked = execFileSync("git", ["ls-files", "src"], { cwd: ROOT, encoding: "utf8" })
|
||||
.split("\n")
|
||||
.filter(p => /^src\/(plugins|equicordplugins)\/.*\.(ts|tsx)$/.test(p))
|
||||
.map(p => p.replace(/\//g, sep));
|
||||
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
|
||||
function fail(at, rule, msg) {
|
||||
console.error(`${at}: ERROR ${rule} ${msg}`);
|
||||
errors++;
|
||||
}
|
||||
function warn(at, rule, msg) {
|
||||
if (VERBOSE) console.warn(`${at}: WARN ${rule} ${msg}`);
|
||||
warnings++;
|
||||
}
|
||||
|
||||
for (const rel of tracked) {
|
||||
const text = readFileSync(join(ROOT, rel), "utf8");
|
||||
if (!text.includes("match:") && !text.includes("find:")) continue;
|
||||
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const at = `${rel.replace(/\\/g, "/")}:${i + 1}`;
|
||||
|
||||
const matchM = line.match(/\bmatch\s*:\s*\/((?:\\.|[^/\\\n])+)\/[gimsuy]*/);
|
||||
if (matchM) {
|
||||
const src = matchM[1];
|
||||
const unbounded = src.match(/\.[+*]\??/);
|
||||
if (unbounded) warn(at, "P002", `unbounded ${unbounded[0]} in match`);
|
||||
const minified = src.match(/\b[a-z]\.[a-z]\b/);
|
||||
if (minified) fail(at, "P001", `hardcoded minified var "${minified[0]}", use \\i.\\i`);
|
||||
}
|
||||
|
||||
const findM = line.match(/\bfind\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\/(?:\\.|[^/\\\n])+\/[gimsuy]*)/);
|
||||
if (findM) {
|
||||
const window = lines.slice(i, i + 12).join("\n");
|
||||
if (/\b(replacement|match)\s*:/.test(window)) {
|
||||
const v = findM[1];
|
||||
if (v === '""' || v === "''" || v === "``") {
|
||||
fail(at, "P003", "find is empty");
|
||||
} else if (v.startsWith("/")) {
|
||||
const src = v.slice(1, v.lastIndexOf("/"));
|
||||
if (/\\i/.test(src) && !/[A-Za-z]{3,}/.test(src) && !src.includes("#{intl::")) {
|
||||
warn(at, "P004", "find regex has no string anchor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const replaceM = line.match(/\breplace\s*:\s*(["'`])((?:\\.|(?!\1).)*)\1/);
|
||||
if (replaceM && /\btry\s*\{[\s\S]*?\}\s*catch\b/.test(replaceM[2])) {
|
||||
warn(at, "P005", "replace uses try/catch, fix the match instead");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tail = warnings && !VERBOSE ? " (run with --verbose to list)" : "";
|
||||
console.log(`\npatch lint: ${errors} error(s), ${warnings} warning(s)${tail}`);
|
||||
process.exit(errors > 0 ? 1 : 0);
|
||||
|
|
@ -38,7 +38,14 @@ function getFilename() {
|
|||
case "win32":
|
||||
return "EquilotlCli.exe";
|
||||
case "darwin":
|
||||
return "Equilotl.MacOS.zip";
|
||||
switch (process.arch) {
|
||||
case "x64":
|
||||
return "Equilotl-darwin-x64.zip";
|
||||
case "arm64":
|
||||
return "Equilotl-darwin-arm64.zip";
|
||||
default:
|
||||
throw new Error("Unsupported macOS architecture: " + process.arch);
|
||||
}
|
||||
case "linux":
|
||||
return "EquilotlCli-linux";
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
// DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!!
|
||||
import "~plugins";
|
||||
import "./fixWeirdAppRegionBug.css";
|
||||
|
||||
export * as Api from "./api";
|
||||
export * as Plugins from "./api/PluginManager";
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export default {
|
|||
|
||||
native: {
|
||||
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
|
||||
supportsWindowsMaterial: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_MATERIAL),
|
||||
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url),
|
||||
getRendererCss: () => invoke<string>(IpcEvents.GET_RENDERER_CSS),
|
||||
onRendererCssUpdate: (cb: (newCss: string) => void) => {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
|||
return (
|
||||
<Tooltip text={props.tooltip}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""}`}>
|
||||
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
|
||||
<Clickable
|
||||
aria-label={props.tooltip}
|
||||
onMouseEnter={onMouseEnter}
|
||||
|
|
@ -191,20 +191,28 @@ addContextMenuPatch("textarea-context", (children, args) => {
|
|||
);
|
||||
});
|
||||
|
||||
// Wrapper stuff is down here to avoid conflicts with above
|
||||
export type ChatBarButtonWrapper = (buttons: ReactNode) => ReactNode;
|
||||
|
||||
/**
|
||||
* Registry for plugins that need to wrap the entire chat bar button container
|
||||
*/
|
||||
export const ChatBarButtonWrappers = new Map<string, ChatBarButtonWrapper>();
|
||||
export interface ChatBarButtonWrapperData {
|
||||
wrapper: ChatBarButtonWrapper;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export const addChatBarButtonWrapper = (id: string, wrapper: ChatBarButtonWrapper) => ChatBarButtonWrappers.set(id, wrapper);
|
||||
/**
|
||||
* Registry for plugins that need to wrap the entire chat bar button container.
|
||||
* Wrappers are applied in ascending priority order (lower number = outermost wrapper).
|
||||
*/
|
||||
export const ChatBarButtonWrappers = new Map<string, ChatBarButtonWrapperData>();
|
||||
|
||||
export const addChatBarButtonWrapper = (id: string, wrapper: ChatBarButtonWrapper, priority: number = 0) => ChatBarButtonWrappers.set(id, { wrapper, priority });
|
||||
export const removeChatBarButtonWrapper = (id: string) => ChatBarButtonWrappers.delete(id);
|
||||
|
||||
export function _wrapButtons(buttons: ReactNode) {
|
||||
const sorted = [...ChatBarButtonWrappers.values()]
|
||||
.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
let wrapped = buttons;
|
||||
for (const wrapper of ChatBarButtonWrappers.values()) {
|
||||
for (const { wrapper } of sorted) {
|
||||
wrapped = wrapper(wrapped);
|
||||
}
|
||||
return wrapped;
|
||||
|
|
|
|||
|
|
@ -18,15 +18,13 @@
|
|||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { t } from "@utils/translation";
|
||||
import { Alerts, Button, ListScrollerThin, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { ConfirmModal, ListScrollerThin, Modal, openModal, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DispatchWithoutAction } from "react";
|
||||
|
||||
|
|
@ -133,7 +131,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|||
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
|
||||
if (!log.length && !pending)
|
||||
return (
|
||||
<div className={cl("container")}>
|
||||
<div>
|
||||
<div className={cl("empty")} />
|
||||
<Paragraph style={{ textAlign: "center" }}>
|
||||
{t("api.notifications.noNotifications")}
|
||||
|
|
@ -153,56 +151,47 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD
|
|||
);
|
||||
}
|
||||
|
||||
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
|
||||
function LogModal(props: RenderModalProps) {
|
||||
const [log, pending] = useLogs();
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{t("api.notifications.notificationLog")}</BaseText>
|
||||
<ModalCloseButton onClick={close} />
|
||||
</ModalHeader>
|
||||
|
||||
<div style={{ width: "100%" }}>
|
||||
<NotificationLog log={log} pending={pending} />
|
||||
</div>
|
||||
|
||||
<ModalFooter>
|
||||
<Flex>
|
||||
<Button onClick={openNotificationSettingsModal}>
|
||||
{t("api.notifications.notificationSettings")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={log.length === 0}
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
Alerts.show({
|
||||
title: t("api.notifications.logs.title"),
|
||||
body: t("api.notifications.logs.body", { count: log.length }),
|
||||
async onConfirm() {
|
||||
<Modal
|
||||
{...props}
|
||||
size="xl"
|
||||
title={t("api.notifications.notificationLog")}
|
||||
actions={[
|
||||
{
|
||||
text: t("api.notifications.notificationSettings"),
|
||||
variant: "primary",
|
||||
onClick: openNotificationSettingsModal
|
||||
},
|
||||
{
|
||||
text: t("api.notifications.logs.clearNotificationLog"),
|
||||
variant: "critical-primary",
|
||||
disabled: !log.length,
|
||||
onClick() {
|
||||
openModal(props =>
|
||||
<ConfirmModal
|
||||
{...props}
|
||||
title={t("api.notifications.logs.title")}
|
||||
subtitle={t("api.notifications.logs.body", { count: log.length })}
|
||||
confirmText={t("api.notifications.logs.confirm")}
|
||||
cancelText={t("api.notifications.logs.cancel")}
|
||||
onConfirm={async () => {
|
||||
await DataStore.set(KEY, []);
|
||||
signals.forEach(x => x());
|
||||
},
|
||||
confirmText: t("api.notifications.logs.confirm"),
|
||||
confirmColor: "vc-notification-log-danger-btn",
|
||||
cancelText: t("api.notifications.cancel")
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("api.notifications.clearNotificationLog")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<NotificationLog log={log} pending={pending} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function openNotificationLogModal() {
|
||||
const key = openModal(modalProps => (
|
||||
<LogModal
|
||||
modalProps={modalProps}
|
||||
close={() => closeModal(key)}
|
||||
/>
|
||||
));
|
||||
openModal(props => <LogModal {...props} />);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,12 +89,6 @@
|
|||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.vc-notification-log-container {
|
||||
padding: 1em;
|
||||
max-height: min(750px, 75vh);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-notification-log-wrapper {
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -288,9 +288,9 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
}
|
||||
if (audioProcessor) addAudioProcessor(name, audioProcessor);
|
||||
if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority);
|
||||
if (renderProfileCollection) addProfileCollection(name, renderProfileCollection);
|
||||
if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper);
|
||||
if (renderProfileSection) addProfileSection(name, renderProfileSection);
|
||||
if (renderProfileCollection) addProfileCollection(name, renderProfileCollection.render, renderProfileCollection.priority);
|
||||
if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper.wrapper, chatBarButtonWrapper.priority);
|
||||
if (renderProfileSection) addProfileSection(name, renderProfileSection.render, renderProfileSection.priority);
|
||||
|
||||
return true;
|
||||
}, p => `startPlugin ${p.name}`);
|
||||
|
|
@ -382,7 +382,7 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
|
||||
"renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration",
|
||||
// Custom
|
||||
"renderNicknameIcon", "renderProfileCollection"
|
||||
"renderNicknameIcon"
|
||||
];
|
||||
|
||||
const neededApiPlugins = new Set<string>();
|
||||
|
|
@ -429,7 +429,7 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
if (p.renderProfileSection) neededApiPlugins.add("ProfileSectionsAPI");
|
||||
|
||||
for (const key of pluginKeysToBind) {
|
||||
p[key] &&= p[key].bind(p) as any;
|
||||
p[key] &&= (p[key] as Function).bind(p) as any;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ const logger = new Logger("ProfileCollectionAPI");
|
|||
|
||||
export type ProfileCollectionFactory = (props: { user: User; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null;
|
||||
|
||||
interface CollectionEntry {
|
||||
export interface ProfileCollectionData {
|
||||
render: ProfileCollectionFactory;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
const profileCollections = new Map<string, CollectionEntry>();
|
||||
const profileCollections = new Map<string, ProfileCollectionData>();
|
||||
const profileCollectionListeners = new Set<() => void>();
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +35,7 @@ const profileCollectionListeners = new Set<() => void>();
|
|||
* ));
|
||||
*/
|
||||
export function addProfileCollection(id: string, render: ProfileCollectionFactory, priority = 0) {
|
||||
profileCollections.set(id, { render });
|
||||
profileCollections.set(id, { render, priority });
|
||||
profileCollectionListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ function ProfileCollections({ props }: { props: any; }) {
|
|||
}, []);
|
||||
|
||||
return Array.from(profileCollections)
|
||||
.sort(([, a], [, b]) => b.priority - a.priority)
|
||||
.map(([id, { render: Collection }]) => (
|
||||
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render profile collection: ${id}`, e.error)}>
|
||||
<Collection {...props} user={props.user ?? props.currentUser} />
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ const logger = new Logger("ProfileSectionsAPI");
|
|||
|
||||
export type ProfileSectionFactory = (props: { userId: string; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null;
|
||||
|
||||
interface SectionEntry {
|
||||
export interface ProfileSectionData {
|
||||
render: ProfileSectionFactory;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
const profileSections = new Map<string, SectionEntry>();
|
||||
const profileSections = new Map<string, ProfileSectionData>();
|
||||
const profileSectionListeners = new Set<() => void>();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export interface Settings {
|
|||
| "under-page"
|
||||
| "window"
|
||||
| undefined;
|
||||
windowsMaterial: "none" | "mica" | "tabbed" | "acrylic";
|
||||
disableMinSize: boolean;
|
||||
winNativeTitleBar: boolean;
|
||||
plugins: {
|
||||
|
|
@ -126,6 +127,7 @@ const DefaultSettings: Settings = {
|
|||
transparent: false,
|
||||
winCtrlQ: false,
|
||||
macosVibrancyStyle: undefined,
|
||||
windowsMaterial: "none",
|
||||
disableMinSize: false,
|
||||
winNativeTitleBar: false,
|
||||
plugins: {},
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ import * as DataStore from "@api/DataStore";
|
|||
import { showNotification } from "@api/Notifications";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
||||
import { OAuth2AuthorizeModal, openModal, UserStore } from "@webpack/common";
|
||||
|
||||
export const logger = new Logger("SettingsSync:CloudSetup", "#39b7e0");
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
letter-spacing: 0.02em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vc-badge-icon {
|
||||
|
|
@ -51,4 +52,4 @@
|
|||
.vc-badge-premium {
|
||||
background-color: var(--premium-tier-2-purple-for-gradients);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@
|
|||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-card-normal {
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.vc-card-danger {
|
||||
background-color: var(--background-feedback-critical);
|
||||
border-color: var(--background-feedback-critical);
|
||||
|
|
@ -56,4 +60,4 @@
|
|||
|
||||
.vc-card-defaultPadding {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,6 @@ import { classNameFactory } from "@utils/css";
|
|||
import { copyWithToast, openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||
import type { ThemeHeader } from "@utils/themes";
|
||||
|
|
@ -46,7 +45,7 @@ import { usercssParse } from "@utils/themes/usercss";
|
|||
import { t } from "@utils/translation";
|
||||
import { getStylusWebStoreUrl } from "@utils/web";
|
||||
import { findComponentByCodeLazy, findLazy } from "@webpack";
|
||||
import { Alerts, Menu, React, Select, showToast, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common";
|
||||
import { Alerts, Menu, openModal,React, Select, showToast, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common";
|
||||
import { ContextMenuApi } from "@webpack/common/menu";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
import type { UserstyleHeader } from "usercss-meta";
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ import { BaseText } from "@components/BaseText";
|
|||
import { Flex } from "@components/Flex";
|
||||
import { CopyIcon, PasteIcon, ResetIcon } from "@components/Icons";
|
||||
import { copyWithToast } from "@utils/discord";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { showToast, Toasts, Tooltip } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, showToast, Toasts, Tooltip } from "@webpack/common";
|
||||
import { type ReactNode } from "react";
|
||||
import { UserstyleHeader } from "usercss-meta";
|
||||
|
||||
import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent, SettingRangeComponent, SettingSelectComponent, SettingTextComponent } from "./components";
|
||||
|
||||
interface UserCSSSettingsModalProps {
|
||||
modalProps: ModalProps;
|
||||
modalProps: RenderModalProps;
|
||||
theme: UserstyleHeader;
|
||||
onSettingsReset: () => void;
|
||||
}
|
||||
|
|
@ -186,21 +186,22 @@ export function UserCSSSettingsModal({ modalProps, theme, onSettingsReset }: Use
|
|||
}
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader separator={false}>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Settings for {theme.name}</BaseText>
|
||||
<Flex style={{ gap: 4, marginRight: 4 }} className="vc-settings-usercss-ie-buttons">
|
||||
<ExportButton themeSettings={themeVars} />
|
||||
<ImportButton themeSettings={themeVars} />
|
||||
<ResetButton themeSettings={userCssVars} themeId={theme.id} close={modalProps.onClose} onReset={onSettingsReset} />
|
||||
<Modal
|
||||
{...modalProps}
|
||||
title={
|
||||
<Flex style={{ alignItems: "center", gap: 4 }}>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Settings for {theme.name}</BaseText>
|
||||
<Flex style={{ gap: 4, marginRight: 4 }} className="vc-settings-usercss-ie-buttons">
|
||||
<ExportButton themeSettings={themeVars} />
|
||||
<ImportButton themeSettings={themeVars} />
|
||||
<ResetButton themeSettings={userCssVars} themeId={theme.id} close={modalProps.onClose} onReset={onSettingsReset} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>
|
||||
{controls}
|
||||
</Flex>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
}
|
||||
>
|
||||
<Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>
|
||||
{controls}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
}
|
||||
|
||||
.vc-addon-note {
|
||||
height: 36px;
|
||||
height: 34px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
|
@ -105,4 +105,4 @@
|
|||
|
||||
.vc-addon-footer {
|
||||
font-size: small;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,12 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { onlyOnce } from "@utils/onlyOnce";
|
||||
import { t } from "@utils/translation";
|
||||
import { Modal, openModal } from "@webpack/common";
|
||||
import type { ComponentType, PropsWithChildren } from "react";
|
||||
|
||||
export function SettingsTab({ children }: PropsWithChildren) {
|
||||
|
|
@ -43,19 +42,18 @@ export function wrapTab(component: ComponentType<any>, tab: string) {
|
|||
}
|
||||
|
||||
export function openSettingsTabModal(Tab: ComponentType<any>) {
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM} className="vc-settings-modal-root">
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{Tab.displayName?.replace("SettingsTab", "") || "Settings"}</BaseText>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
Tab = wrapTab(Tab, Tab.displayName || "SettingsTab");
|
||||
|
||||
<ModalContent className="vc-settings-modal">
|
||||
<Tab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), Tab.displayName || "SettingsTab"));
|
||||
try {
|
||||
openModal(props => (
|
||||
<Modal
|
||||
{...props}
|
||||
size="lg"
|
||||
title={Tab.displayName?.replace("SettingsTab", "") || "Settings"}
|
||||
>
|
||||
<Tab />
|
||||
</Modal>
|
||||
));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { ReplaceFn } from "@utils/types";
|
|||
import { Button, Parser, useMemo, useState } from "@webpack/common";
|
||||
import type { Change } from "diff";
|
||||
|
||||
// Do not include diff in non dev builds (side effects import)
|
||||
if (IS_DEV) {
|
||||
// Do not include diff in standalone builds (side effects import)
|
||||
if (!IS_STANDALONE) {
|
||||
var differ = require("diff") as typeof import("diff");
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,10 @@ export function PatchPreview({ module, match, replacement, setReplacementError }
|
|||
className={Margins.top20}
|
||||
onClick={() => {
|
||||
try {
|
||||
Function(patchedCode.replace(/^(?=function\()/, "0,"));
|
||||
const isArrowFunction = patchedCode.startsWith("(");
|
||||
const wrappedCode = "0," + (!isArrowFunction ? "function" : "") + patchedCode.slice(patchedCode.indexOf("("));
|
||||
Function(wrappedCode);
|
||||
|
||||
setCompileResult([true, "Compiled successfully"]);
|
||||
} catch (err) {
|
||||
setCompileResult([false, (err as Error).message]);
|
||||
|
|
|
|||
|
|
@ -183,4 +183,4 @@ function PatchHelper() {
|
|||
);
|
||||
}
|
||||
|
||||
export default IS_DEV ? wrapTab(PatchHelper, "Patch Helper") : null;
|
||||
export default !IS_STANDALONE ? wrapTab(PatchHelper, "PatchHelper") : null;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.vc-author-modal-root {
|
||||
padding: 1em;
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
.vc-author-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vc-author-modal-name {
|
||||
|
|
@ -50,18 +50,16 @@
|
|||
0 2px 6px rgb(0 0 0 / 35%);
|
||||
}
|
||||
|
||||
.vc-author-modal-root+[class*="footer"] {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.vc-author-modal-links {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vc-author-modal-plugins {
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,17 +7,15 @@
|
|||
import "./ContributorModal.css";
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { HeadingPrimary } from "@components/Heading";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Link } from "@components/Link";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { EquicordDevsById, VencordDevsById } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { fetchUserProfile } from "@utils/discord";
|
||||
import { ModalContent, ModalFooter, ModalRoot, openModal } from "@utils/modal";
|
||||
import { Translate } from "@utils/translation";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
import { RenderModalProps, User } from "@vencord/discord-types";
|
||||
import { Modal, openModal, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import Plugins, { PluginMeta } from "~plugins";
|
||||
|
||||
|
|
@ -27,16 +25,10 @@ import { PluginCard } from "./PluginCard";
|
|||
const cl = classNameFactory("vc-author-modal-");
|
||||
|
||||
export function openContributorModal(user: User) {
|
||||
openModal(modalProps =>
|
||||
<ModalRoot {...modalProps}>
|
||||
<ErrorBoundary>
|
||||
<ContributorModal user={user} />
|
||||
</ErrorBoundary>
|
||||
</ModalRoot>
|
||||
);
|
||||
openModal(modalProps => <ContributorModal user={user} modalProps={modalProps} />);
|
||||
}
|
||||
|
||||
function ContributorModal({ user }: { user: User; }) {
|
||||
function ContributorModal({ user, modalProps }: { user: User; modalProps: RenderModalProps; }) {
|
||||
useSettings();
|
||||
|
||||
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
|
||||
|
|
@ -68,18 +60,20 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
const hasLinks = website || githubName;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalContent className={cl("root")}>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
title={
|
||||
<div className={cl("header")}>
|
||||
<img
|
||||
className={cl("avatar")}
|
||||
src={user.getAvatarURL(void 0, 512, true)}
|
||||
alt=""
|
||||
/>
|
||||
<HeadingPrimary className={cl("name")}>{user.username}</HeadingPrimary>
|
||||
<Heading tag="h2" className={cl("name")}>{user.username}</Heading>
|
||||
</div>
|
||||
|
||||
{plugins.length ? (
|
||||
}
|
||||
subtitle={
|
||||
plugins.length ? (
|
||||
<Paragraph>
|
||||
<Translate
|
||||
i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.plugins"
|
||||
|
|
@ -94,25 +88,14 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
to Equicord in other ways!
|
||||
</Translate>
|
||||
</Paragraph >
|
||||
)}
|
||||
|
||||
{!!plugins.length && (
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
|
||||
{hasLinks && (
|
||||
<ModalFooter>
|
||||
<div className={cl("links")}>
|
||||
)
|
||||
}
|
||||
actionBarInput={
|
||||
hasLinks && (
|
||||
<div
|
||||
className={cl("links")}
|
||||
style={{ width: "100%", justifyContent: "flex-end" }}
|
||||
>
|
||||
{website && (
|
||||
<WebsiteButton
|
||||
text={website}
|
||||
|
|
@ -126,8 +109,23 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={cl("root")}>
|
||||
{!!plugins.length && (
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
.vc-plugin-modal-header {
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
padding: 16px 16px 0 24px;
|
||||
}
|
||||
|
||||
.vc-plugin-modal-header-content {
|
||||
|
|
@ -70,7 +69,7 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-block: 8px 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vc-plugin-modal-tag {
|
||||
|
|
@ -82,3 +81,9 @@
|
|||
padding: 6px 12px;
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-plugin-modal-about-box {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-normal, var(--background-modifier-accent));
|
||||
}
|
||||
|
|
@ -32,11 +32,10 @@ import { classNameFactory } from "@utils/css";
|
|||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { OptionType, Plugin, PluginTag } from "@utils/types";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { RenderModalProps, User } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
|
||||
import { Clickable, FluxDispatcher, React, Toasts, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
|
||||
import { Clickable, FluxDispatcher, Modal, openModal, React, Text, Toasts, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
|
||||
import { Constructor } from "type-fest";
|
||||
|
||||
import { PluginMeta } from "~plugins";
|
||||
|
|
@ -53,7 +52,7 @@ const ConfirmModal = findComponentByCodeLazy('parentComponent:"ConfirmModal"');
|
|||
const WarningIcon = findComponentByCodeLazy("3.15H3.29c-1.74");
|
||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||
|
||||
interface PluginModalProps extends ModalProps {
|
||||
interface PluginModalProps extends RenderModalProps {
|
||||
plugin: Plugin;
|
||||
onRestartNeeded(key: string): void;
|
||||
}
|
||||
|
|
@ -142,6 +141,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
onChange={debounce(onChange)}
|
||||
pluginSettings={pluginSettings}
|
||||
definedSettings={settings}
|
||||
closePluginSettings={onClose}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
@ -176,28 +176,36 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
const isEquicordPlugin = pluginMeta.folderName.startsWith("src/equicordplugins/") ?? false;
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader separator={false} className={cl("header")}>
|
||||
<div className={cl("header-content")}>
|
||||
<BaseText size="lg" weight="semibold" className={cl("title")}>{plugin.name}</BaseText>
|
||||
<BaseText size="sm" className={cl("description")}>{plugin.description}</BaseText>
|
||||
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />}
|
||||
{!!plugin.settingsAboutComponent && (
|
||||
<div className={Margins.top8}>
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
||||
<plugin.settingsAboutComponent />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
transitionState={transitionState}
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
title={
|
||||
<div className={cl("header")}>
|
||||
<BaseText tag="h1" weight="semibold" size="lg">{plugin.name}</BaseText>
|
||||
</div>
|
||||
<div className={cl("header-trailing")}>
|
||||
<CloseButton onClick={onClose} />
|
||||
}
|
||||
subtitle={
|
||||
<div className={cl("info")}>
|
||||
<div>
|
||||
<Paragraph size="md">{plugin.description}</Paragraph>
|
||||
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />}
|
||||
</div>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent className={"vc-settings-modal-content"}>
|
||||
}
|
||||
>
|
||||
{!!plugin.settingsAboutComponent && (
|
||||
<div className={classes(Margins.top16, cl("about-box"))}>
|
||||
<section>
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
||||
<plugin.settingsAboutComponent />
|
||||
</ErrorBoundary>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
<div className={"vc-settings-modal-content"}>
|
||||
<section>
|
||||
<BaseText size="lg" weight="semibold" color="text-strong" className={Margins.bottom8}>Authors</BaseText>
|
||||
<Text variant="heading-lg/semibold" className={classes(Margins.top8, Margins.bottom8)}>Authors</Text>
|
||||
<div style={{ width: "fit-content" }}>
|
||||
<ErrorBoundary noop>
|
||||
<UserSummaryItem
|
||||
|
|
@ -228,8 +236,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
<BaseText size="lg" weight="semibold" color="text-strong" className={classes(Margins.top16, Margins.bottom8)}>Settings</BaseText>
|
||||
{renderSettings()}
|
||||
</section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
</div>
|
||||
<div>
|
||||
<Flex flexDirection="column" style={{ width: "100%" }}>
|
||||
<Flex style={{ justifyContent: "space-between", alignItems: "center" }}>
|
||||
{hasSettings ? (
|
||||
|
|
@ -262,8 +270,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalRoot >
|
||||
</div>
|
||||
</Modal >
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@
|
|||
}
|
||||
|
||||
.vc-plugin-ui-elements-modal-content {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5em;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.vc-plugin-ui-elements-switches {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ import { Switch } from "@components/Switch";
|
|||
import { classNameFactory } from "@utils/css";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { IconComponent } from "@utils/types";
|
||||
import { Clickable } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Clickable, Modal,openModal } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-plugin-ui-elements-");
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ function Section(props: {
|
|||
|
||||
return (
|
||||
<section>
|
||||
<BaseText tag="h3" size="xl" weight="bold">{title}</BaseText>
|
||||
<BaseText tag="h3" size="lg" weight="semibold">{title}</BaseText>
|
||||
<Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>
|
||||
|
||||
<div className={cl("switches")}>
|
||||
|
|
@ -89,12 +89,12 @@ function Section(props: {
|
|||
);
|
||||
}
|
||||
|
||||
function UIElementsModal(props: ModalProps) {
|
||||
function UIElementsModal(props: RenderModalProps) {
|
||||
const { uiElements } = useSettings(["uiElements.*"]);
|
||||
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<Modal {...props} size="md" title="Manage plugin UI elements">
|
||||
<div className={cl("modal-content")}>
|
||||
<Section
|
||||
title="Chatbar Buttons"
|
||||
description="These are the buttons on the right side of the chat input bar"
|
||||
|
|
@ -107,7 +107,7 @@ function UIElementsModal(props: ModalProps) {
|
|||
buttonMap={MessagePopoverButtonMap}
|
||||
settings={uiElements.messagePopoverButtons}
|
||||
/>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface SettingBaseProps<T> {
|
|||
};
|
||||
id: string;
|
||||
definedSettings: DefinedSettings;
|
||||
closePluginSettings(): void;
|
||||
}
|
||||
|
||||
export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ import { PluginSettingComponentDef } from "@utils/types";
|
|||
|
||||
import { ComponentSettingProps } from "./Common";
|
||||
|
||||
export function ComponentSetting({ setting, onChange }: ComponentSettingProps<PluginSettingComponentDef>) {
|
||||
return setting.component({ setValue: onChange, option: setting });
|
||||
export function ComponentSetting({ setting, onChange, closePluginSettings }: ComponentSettingProps<PluginSettingComponentDef>) {
|
||||
return setting.component({ setValue: onChange, option: setting, closePluginSettings });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ import { isTruthy } from "@utils/guards";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter, useIntersection } from "@utils/react";
|
||||
import { useAwaiter, useCleanupEffect, useIntersection } from "@utils/react";
|
||||
import { t } from "@utils/translation";
|
||||
import { PluginTag, PluginTags } from "@utils/types";
|
||||
import { Alerts, lodash, Parser, React, SearchableSelect, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { Alerts, ConfirmModal, lodash, openModal, Parser, React, SearchableSelect, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
|
||||
|
|
@ -148,7 +148,7 @@ export default function PluginSettings() {
|
|||
const changeRef = useRef<ChangeList<string>>(null);
|
||||
const changes = changeRef.current ??= new ChangeList<string>();
|
||||
|
||||
React.useEffect(() => {
|
||||
useCleanupEffect(() => {
|
||||
return () => {
|
||||
if (!changes.hasChanges) return;
|
||||
|
||||
|
|
@ -158,23 +158,29 @@ export default function PluginSettings() {
|
|||
const displayed = pluginNames.slice(0, maxDisplay);
|
||||
const remainingCount = pluginNames.length - displayed.length;
|
||||
|
||||
Alerts.show({
|
||||
title: t("vencord.settings.restartRequired.title"),
|
||||
body: (
|
||||
<div>
|
||||
{displayed.map((s, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && ", "}
|
||||
{Parser.parse("`" + s + "`")}
|
||||
</span>
|
||||
))}
|
||||
{remainingCount > 0 && <span> and {remainingCount} more</span>}
|
||||
</div>
|
||||
),
|
||||
confirmText: t("vencord.settings.restartRequired.confirm"),
|
||||
cancelText: t("vencord.settings.restartRequired.cancel"),
|
||||
onConfirm: () => location.reload()
|
||||
});
|
||||
openModal(props => (
|
||||
<ConfirmModal
|
||||
{...props}
|
||||
title={t("vencord.settings.restartRequired.title")}
|
||||
confirmText={t("vencord.settings.restartRequired.confirm")}
|
||||
cancelText={t("vencord.settings.restartRequired.cancel")}
|
||||
variant="primary"
|
||||
onConfirm={() => location.reload()}
|
||||
>
|
||||
<>
|
||||
<p>{t("vencord.settings.restartRequired.plugins")}</p>
|
||||
<div>
|
||||
{displayed.map((s, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && ", "}
|
||||
{Parser.parse("`" + s + "`")}
|
||||
</span>
|
||||
))}
|
||||
{remainingCount > 0 && <span> and {remainingCount} more</span>}
|
||||
</div>
|
||||
</>
|
||||
</ConfirmModal>
|
||||
));
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -88,12 +88,4 @@
|
|||
|
||||
.vc-cloud-button-icon {
|
||||
height: 1.25em;
|
||||
}
|
||||
|
||||
.vc-settings-modal-root {
|
||||
background: var(--background-base-low);
|
||||
}
|
||||
|
||||
.vc-settings-modal {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import { Span } from "@components/Span";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { changes, checkForUpdates, update, updateError } from "@utils/updater";
|
||||
import { Alerts, React, Toasts, useState } from "@webpack/common";
|
||||
import { ConfirmModal, openModal, React, Toasts, useState } from "@webpack/common";
|
||||
|
||||
import { runWithDispatch } from "./runWithDispatch";
|
||||
|
||||
|
|
@ -113,17 +113,21 @@ export function Updatable(props: CommonProps) {
|
|||
setUpdates([]);
|
||||
|
||||
await new Promise<void>(r => {
|
||||
Alerts.show({
|
||||
title: "Update Success!",
|
||||
body: "Successfully updated. Restart now to apply the changes?",
|
||||
confirmText: "Restart",
|
||||
cancelText: "Not now!",
|
||||
onConfirm() {
|
||||
relaunch();
|
||||
r();
|
||||
},
|
||||
onCancel: r
|
||||
});
|
||||
openModal(props => (
|
||||
<ConfirmModal
|
||||
{...props}
|
||||
title="Update Success!"
|
||||
subtitle="Successfully updated. Restart now to apply the changes?"
|
||||
confirmText="Restart"
|
||||
cancelText="Not now!"
|
||||
variant="primary"
|
||||
onConfirm={() => {
|
||||
relaunch();
|
||||
r();
|
||||
}}
|
||||
onCancel={r}
|
||||
/>
|
||||
));
|
||||
});
|
||||
}
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { UpdateLogger } from "@utils/updater";
|
||||
import { Alerts, Parser } from "@webpack/common";
|
||||
import { ConfirmModal,openModal, Parser } from "@webpack/common";
|
||||
|
||||
function getErrorMessage(e: any) {
|
||||
if (!e?.code || !e.cmd)
|
||||
|
|
@ -33,16 +33,20 @@ export function runWithDispatch(dispatch: React.Dispatch<React.SetStateAction<bo
|
|||
|
||||
const err = getErrorMessage(e);
|
||||
|
||||
Alerts.show({
|
||||
title: "Oops!",
|
||||
body: (
|
||||
openModal(props => (
|
||||
<ConfirmModal
|
||||
{...props}
|
||||
title="Oops!"
|
||||
confirmText="OK"
|
||||
variant="primary"
|
||||
>
|
||||
<ErrorCard>
|
||||
{err.split("\n").map((line, idx) =>
|
||||
<div key={idx}>{Parser.parse(line)}</div>
|
||||
)}
|
||||
</ErrorCard>
|
||||
)
|
||||
});
|
||||
</ConfirmModal>
|
||||
));
|
||||
} finally {
|
||||
dispatch(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,80 +7,81 @@
|
|||
import { useSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { IS_MAC } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { t } from "@utils/translation";
|
||||
import { Select } from "@webpack/common";
|
||||
|
||||
export function VibrancySettings() {
|
||||
export function MacOSVibrancySettings() {
|
||||
const settings = useSettings(["macosVibrancyStyle"]);
|
||||
|
||||
if (!IS_MAC || IS_WEB) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>{t("vencord.settings.macosVibrancyStyle.title")}</Heading>
|
||||
<ErrorBoundary noop>
|
||||
<Select
|
||||
className={Margins.bottom20}
|
||||
placeholder={t("vencord.settings.macosVibrancyStyle.placeholder")}
|
||||
options={[
|
||||
// Sorted from most opaque to most transparent
|
||||
{
|
||||
label: "No vibrancy", value: undefined
|
||||
},
|
||||
{
|
||||
label: "Under Page (window tinting)",
|
||||
value: "under-page"
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
value: "content"
|
||||
},
|
||||
{
|
||||
label: "Window",
|
||||
value: "window"
|
||||
},
|
||||
{
|
||||
label: "Selection",
|
||||
value: "selection"
|
||||
},
|
||||
{
|
||||
label: "Titlebar",
|
||||
value: "titlebar"
|
||||
},
|
||||
{
|
||||
label: "Header",
|
||||
value: "header"
|
||||
},
|
||||
{
|
||||
label: "Sidebar",
|
||||
value: "sidebar"
|
||||
},
|
||||
{
|
||||
label: "Tooltip",
|
||||
value: "tooltip"
|
||||
},
|
||||
{
|
||||
label: "Menu",
|
||||
value: "menu"
|
||||
},
|
||||
{
|
||||
label: "Popover",
|
||||
value: "popover"
|
||||
},
|
||||
{
|
||||
label: "Fullscreen UI (transparent but slightly muted)",
|
||||
value: "fullscreen-ui"
|
||||
},
|
||||
{
|
||||
label: "HUD (Most transparent)",
|
||||
value: "hud"
|
||||
},
|
||||
]}
|
||||
select={v => settings.macosVibrancyStyle = v}
|
||||
isSelected={v => settings.macosVibrancyStyle === v}
|
||||
serialize={identity}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
<ErrorBoundary noop>
|
||||
<Heading tag="h5">{t("vencord.settings.macosVibrancyStyle.title")}</Heading>
|
||||
<Select
|
||||
className={Margins.bottom20}
|
||||
placeholder={t("vencord.settings.macosVibrancyStyle.placeholder")}
|
||||
options={[
|
||||
// Sorted from most opaque to most transparent
|
||||
{
|
||||
label: "No vibrancy", value: undefined
|
||||
},
|
||||
{
|
||||
label: "Under Page (window tinting)",
|
||||
value: "under-page"
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
value: "content"
|
||||
},
|
||||
{
|
||||
label: "Window",
|
||||
value: "window"
|
||||
},
|
||||
{
|
||||
label: "Selection",
|
||||
value: "selection"
|
||||
},
|
||||
{
|
||||
label: "Titlebar",
|
||||
value: "titlebar"
|
||||
},
|
||||
{
|
||||
label: "Header",
|
||||
value: "header"
|
||||
},
|
||||
{
|
||||
label: "Sidebar",
|
||||
value: "sidebar"
|
||||
},
|
||||
{
|
||||
label: "Tooltip",
|
||||
value: "tooltip"
|
||||
},
|
||||
{
|
||||
label: "Menu",
|
||||
value: "menu"
|
||||
},
|
||||
{
|
||||
label: "Popover",
|
||||
value: "popover"
|
||||
},
|
||||
{
|
||||
label: "Fullscreen UI (transparent but slightly muted)",
|
||||
value: "fullscreen-ui"
|
||||
},
|
||||
{
|
||||
label: "HUD (Most transparent)",
|
||||
value: "hud"
|
||||
},
|
||||
]}
|
||||
select={v => settings.macosVibrancyStyle = v}
|
||||
isSelected={v => settings.macosVibrancyStyle === v}
|
||||
serialize={identity}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Button } from "@components/Button";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
|
|
@ -15,9 +14,8 @@ import { Heading } from "@components/Heading";
|
|||
import { Paragraph } from "@components/Paragraph";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { Select, Slider } from "@webpack/common";
|
||||
import { Modal, openModal, Select, Slider } from "@webpack/common";
|
||||
|
||||
export function NotificationSection() {
|
||||
return (
|
||||
|
|
@ -41,16 +39,13 @@ export function NotificationSection() {
|
|||
|
||||
export function openNotificationSettingsModal() {
|
||||
openModal(props => (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Notification Settings</BaseText>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<NotificationSettings />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
<Modal
|
||||
{...props}
|
||||
size="lg"
|
||||
title={t("api.notifications.notificationSettings")}
|
||||
>
|
||||
<NotificationSettings />
|
||||
</Modal>
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -58,8 +53,8 @@ function NotificationSettings() {
|
|||
const settings = useSettings(["notifications.*"]).notifications;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "1em 0" }}>
|
||||
<Heading>Notification Style</Heading>
|
||||
<>
|
||||
<Heading tag="h5">Notification Style</Heading>
|
||||
{settings.useNative !== "never" && Notification?.permission === "denied" && (
|
||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||
<Heading>Desktop Notification Permission denied</Heading>
|
||||
|
|
@ -135,6 +130,6 @@ function NotificationSettings() {
|
|||
onValueRender={v => v === 200 ? "∞" : v}
|
||||
onMarkerRender={v => v === 200 ? "∞" : v}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Margins } from "@components/margins";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { IS_WINDOWS } from "@utils/constants";
|
||||
import { Select } from "@webpack/common";
|
||||
|
||||
export function WindowsMaterialSettings() {
|
||||
const settings = useSettings(["windowsMaterial"]);
|
||||
|
||||
if (!IS_WINDOWS || IS_WEB || !VencordNative.native.supportsWindowsMaterial()) return null;
|
||||
|
||||
return (
|
||||
<ErrorBoundary noop>
|
||||
<Heading tag="h5">Background Material</Heading>
|
||||
<Paragraph className={Margins.bottom8}>
|
||||
Windows transparent background effects. You need a theme that supports transparency or this will do nothing. A restart is required after changing this setting.
|
||||
</Paragraph>
|
||||
|
||||
<Select
|
||||
placeholder="None"
|
||||
options={[
|
||||
{
|
||||
label: "None",
|
||||
value: "none",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
|
||||
value: "mica"
|
||||
},
|
||||
{
|
||||
label: "Tabbed (variant of Mica with stronger background tinting)",
|
||||
value: "tabbed"
|
||||
},
|
||||
{
|
||||
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
|
||||
value: "acrylic"
|
||||
}
|
||||
]}
|
||||
closeOnSelect={true}
|
||||
select={v => (settings.windowsMaterial = v)}
|
||||
isSelected={v => v === settings.windowsMaterial}
|
||||
serialize={s => s}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import { plugins } from "@api/PluginManager";
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { Button } from "@components/Button";
|
||||
import { Divider } from "@components/Divider";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { FormSwitch } from "@components/FormSwitch";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "@components/Icons";
|
||||
|
|
@ -22,16 +21,18 @@ import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
|
|||
import { SpecialCard } from "@components/settings/SpecialCard";
|
||||
import BadgeAPI from "@plugins/_api/badges";
|
||||
import { gitRemote } from "@shared/vencordUserAgent";
|
||||
import { DONOR_ROLE_ID, GUILD_ID, IS_MAC, IS_WINDOWS, VC_DONOR_ROLE_ID, VC_GUILD_ID } from "@utils/constants";
|
||||
import { DONOR_ROLE_ID, GUILD_ID, IS_WINDOWS, VC_DONOR_ROLE_ID, VC_GUILD_ID } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity, isAnyPluginDev } from "@utils/misc";
|
||||
import { isAnyPluginDev } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { t } from "@utils/translation";
|
||||
import { Alerts, GuildMemberStore, React, Select, UserStore } from "@webpack/common";
|
||||
import { Alerts, GuildMemberStore, React, useMemo, UserStore } from "@webpack/common";
|
||||
|
||||
import { DonateButtonComponent } from "./DonateButton";
|
||||
import { openNotificationSettingsModal } from "./NotificationSettings";
|
||||
import { MacOSVibrancySettings } from "./MacVibrancySettings";
|
||||
import { NotificationSection } from "./NotificationSettings";
|
||||
import { WindowsMaterialSettings } from "./WindowsMaterialSettings";
|
||||
|
||||
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||
const SHIGGY_DONATE_IMAGE = "https://equicord.org/assets/favicon.png";
|
||||
|
|
@ -48,80 +49,121 @@ type KeysOfType<Object, Type> = {
|
|||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||
}[keyof Object];
|
||||
|
||||
function EquicordSettings() {
|
||||
const settings = useSettings();
|
||||
function Switches() {
|
||||
const settings = useSettings(["useQuickCss", "enableReactDevtools", "mainWindowFrameless", "frameless", "winNativeTitleBar", "transparent", "winCtrlQ", "disableMinSize"]);
|
||||
|
||||
const donateImage = React.useMemo(
|
||||
() => (Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE),
|
||||
[],
|
||||
);
|
||||
|
||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && IS_MAC;
|
||||
|
||||
const user = UserStore?.getCurrentUser();
|
||||
|
||||
const Switches: Array<false | {
|
||||
const Switches = [
|
||||
{
|
||||
key: "useQuickCss",
|
||||
title: t("vencord.settings.useQuickCss.title"),
|
||||
description: t("vencord.settings.useQuickCss.description"),
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "enableReactDevtools",
|
||||
title: t("vencord.settings.enableReactDevtools.title"),
|
||||
description: t("vencord.settings.enableReactDevtools.description"),
|
||||
restartRequired: true,
|
||||
},
|
||||
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && {
|
||||
key: "mainWindowFrameless",
|
||||
title: t("equicord.mainWindowFrameless.title"),
|
||||
description: t("equicord.mainWindowFrameless.description"),
|
||||
restartRequired: true,
|
||||
},
|
||||
!IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS
|
||||
? {
|
||||
key: "frameless",
|
||||
title: t("vencord.settings.frameless.title"),
|
||||
description: t("vencord.settings.frameless.description"),
|
||||
restartRequired: true,
|
||||
}
|
||||
: {
|
||||
key: "winNativeTitleBar",
|
||||
title: t("vencord.settings.winNativeTitleBar.title"),
|
||||
description: t("vencord.settings.winNativeTitleBar.description"),
|
||||
restartRequired: true,
|
||||
}
|
||||
),
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: t("vencord.settings.transparent.title"),
|
||||
description: t("vencord.settings.transparent.description"),
|
||||
restartRequired: true,
|
||||
warning: IS_WINDOWS
|
||||
? t("vencord.settings.transparent.noteWindows")
|
||||
: t("vencord.settings.transparent.note"),
|
||||
},
|
||||
IS_DISCORD_DESKTOP && {
|
||||
key: "disableMinSize",
|
||||
title: t("vencord.settings.disableMinSize.title"),
|
||||
description: t("vencord.settings.disableMinSize.description"),
|
||||
restartRequired: true,
|
||||
},
|
||||
!IS_WEB &&
|
||||
IS_WINDOWS && {
|
||||
key: "winCtrlQ",
|
||||
title: t("vencord.settings.winCtrlQ.title"),
|
||||
description: t("vencord.settings.winCtrlQ.description"),
|
||||
restartRequired: true,
|
||||
}
|
||||
] satisfies Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
title: string;
|
||||
description?: string;
|
||||
restartRequired?: boolean;
|
||||
warning?: string;
|
||||
}>
|
||||
= [
|
||||
{
|
||||
key: "useQuickCss",
|
||||
title: t("vencord.settings.useQuickCss.title"),
|
||||
description: t("vencord.settings.useQuickCss.description"),
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "enableReactDevtools",
|
||||
title: t("vencord.settings.enableReactDevtools.title"),
|
||||
description: t("vencord.settings.enableReactDevtools.description"),
|
||||
restartRequired: true,
|
||||
},
|
||||
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && {
|
||||
key: "mainWindowFrameless",
|
||||
title: t("equicord.mainWindowFrameless.title"),
|
||||
description: t("equicord.mainWindowFrameless.description"),
|
||||
restartRequired: true,
|
||||
},
|
||||
!IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS
|
||||
? {
|
||||
key: "frameless",
|
||||
title: t("vencord.settings.frameless.title"),
|
||||
description: t("vencord.settings.frameless.description"),
|
||||
restartRequired: true,
|
||||
}>;
|
||||
|
||||
return Switches.map(setting => {
|
||||
if (!setting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { key, title, description, restartRequired, warning } = setting;
|
||||
|
||||
return (
|
||||
<FormSwitch
|
||||
key={key}
|
||||
title={title}
|
||||
description={
|
||||
warning ? (
|
||||
<>
|
||||
{description}
|
||||
<Notice.Warning className={Margins.top8} style={{ width: "100%" }}>
|
||||
{warning}
|
||||
</Notice.Warning>
|
||||
</>
|
||||
) : (
|
||||
description
|
||||
)
|
||||
}
|
||||
: {
|
||||
key: "winNativeTitleBar",
|
||||
title: t("vencord.settings.winNativeTitleBar.title"),
|
||||
description: t("vencord.settings.winNativeTitleBar.description"),
|
||||
restartRequired: true,
|
||||
}
|
||||
),
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: t("vencord.settings.transparent.title"),
|
||||
description: t("vencord.settings.transparent.description"),
|
||||
restartRequired: true,
|
||||
warning: IS_WINDOWS
|
||||
? t("vencord.settings.transparent.noteWindows")
|
||||
: t("vencord.settings.transparent.note"),
|
||||
},
|
||||
IS_DISCORD_DESKTOP && {
|
||||
key: "disableMinSize",
|
||||
title: t("vencord.settings.disableMinSize.title"),
|
||||
description: t("vencord.settings.disableMinSize.description"),
|
||||
restartRequired: true,
|
||||
},
|
||||
!IS_WEB &&
|
||||
IS_WINDOWS && {
|
||||
key: "winCtrlQ",
|
||||
title: t("vencord.settings.winCtrlQ.title"),
|
||||
description: t("vencord.settings.winCtrlQ.description"),
|
||||
restartRequired: true,
|
||||
}
|
||||
];
|
||||
value={settings[key]}
|
||||
onChange={v => {
|
||||
settings[key] = v;
|
||||
|
||||
if (restartRequired) {
|
||||
Alerts.show({
|
||||
title: t("vencord.settings.restartRequired.title"),
|
||||
body: t("vencord.settings.restartRequired.body"),
|
||||
confirmText: t("vencord.settings.restartRequired.confirm"),
|
||||
cancelText: t("vencord.settings.restartRequired.cancel"),
|
||||
onConfirm: relaunch
|
||||
});
|
||||
}
|
||||
}}
|
||||
hideBorder
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function EquicordSettings() {
|
||||
const donateImage = useMemo(() =>
|
||||
Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE,
|
||||
[]
|
||||
);
|
||||
|
||||
const user = UserStore?.getCurrentUser();
|
||||
|
||||
return (
|
||||
<SettingsTab>
|
||||
|
|
@ -233,131 +275,13 @@ function EquicordSettings() {
|
|||
</a>.
|
||||
</Notice.Info>
|
||||
|
||||
{Switches.filter((s): s is Exclude<typeof s, false> => !!s).map(
|
||||
s => (
|
||||
<FormSwitch
|
||||
key={s.key}
|
||||
value={settings[s.key]}
|
||||
onChange={v => {
|
||||
settings[s.key] = v;
|
||||
<Switches />
|
||||
|
||||
if (s.restartRequired) {
|
||||
Alerts.show({
|
||||
title: "Restart Required",
|
||||
body: "A restart is required to apply this change",
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: relaunch
|
||||
});
|
||||
}
|
||||
}}
|
||||
title={s.title}
|
||||
description={
|
||||
s.warning ? (
|
||||
<>
|
||||
{s.description}
|
||||
<Notice.Warning className={Margins.top8} style={{ width: "100%" }}>
|
||||
{s.warning}
|
||||
</Notice.Warning>
|
||||
</>
|
||||
) : (
|
||||
s.description
|
||||
)
|
||||
}
|
||||
hideBorder
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<MacOSVibrancySettings />
|
||||
<WindowsMaterialSettings />
|
||||
|
||||
{needsVibrancySettings && (
|
||||
<>
|
||||
<Divider className={Margins.top20} />
|
||||
|
||||
<Heading className={Margins.top20}>Window Vibrancy</Heading>
|
||||
<Paragraph className={Margins.bottom16}>
|
||||
Customize the macOS window vibrancy effect. This controls the blur and transparency style of the Discord window. Changes require a restart to take effect.
|
||||
</Paragraph>
|
||||
<Select
|
||||
className={Margins.bottom20}
|
||||
placeholder="Window vibrancy style"
|
||||
options={[
|
||||
// Sorted from most opaque to most transparent
|
||||
{
|
||||
label: "No vibrancy",
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "Under Page (window tinting)",
|
||||
value: "under-page",
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
value: "content",
|
||||
},
|
||||
{
|
||||
label: "Window",
|
||||
value: "window",
|
||||
},
|
||||
{
|
||||
label: "Selection",
|
||||
value: "selection",
|
||||
},
|
||||
{
|
||||
label: "Titlebar",
|
||||
value: "titlebar",
|
||||
},
|
||||
{
|
||||
label: "Header",
|
||||
value: "header",
|
||||
},
|
||||
{
|
||||
label: "Sidebar",
|
||||
value: "sidebar",
|
||||
},
|
||||
{
|
||||
label: "Tooltip",
|
||||
value: "tooltip",
|
||||
},
|
||||
{
|
||||
label: "Menu",
|
||||
value: "menu",
|
||||
},
|
||||
{
|
||||
label: "Popover",
|
||||
value: "popover",
|
||||
},
|
||||
{
|
||||
label: "Fullscreen UI (transparent but slightly muted)",
|
||||
value: "fullscreen-ui",
|
||||
},
|
||||
{
|
||||
label: "HUD (Most transparent)",
|
||||
value: "hud",
|
||||
},
|
||||
]}
|
||||
select={v => (settings.macosVibrancyStyle = v)}
|
||||
isSelected={v => settings.macosVibrancyStyle === v}
|
||||
serialize={identity}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider className={Margins.top20} />
|
||||
|
||||
<Heading className={Margins.top20}>Notifications</Heading>
|
||||
<Paragraph className={Margins.bottom16}>
|
||||
Configure how Equicord handles notifications. You can customize when and how you receive alerts, or view a history of past notifications.
|
||||
</Paragraph>
|
||||
|
||||
<Flex gap="16px">
|
||||
<Button onClick={openNotificationSettingsModal}>
|
||||
Notification Settings
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={openNotificationLogModal}>
|
||||
View Notification Log
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingsTab>
|
||||
<NotificationSection />
|
||||
</SettingsTab >
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default definePlugin({
|
|||
description: "API to add collections to the user profile panel like discords game collection.",
|
||||
authors: [Devs.thororen],
|
||||
patches: [
|
||||
// message and member list popouts
|
||||
{
|
||||
find: "#{intl::USER_PROFILE_FRIEND_REQUEST_TOAST}",
|
||||
replacement: {
|
||||
|
|
@ -19,6 +20,7 @@ export default definePlugin({
|
|||
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
|
||||
}
|
||||
},
|
||||
// user panel popout
|
||||
{
|
||||
find: '"UserProfileAccountPopout"',
|
||||
replacement: {
|
||||
|
|
@ -26,6 +28,7 @@ export default definePlugin({
|
|||
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
|
||||
},
|
||||
},
|
||||
// dm sidebar
|
||||
{
|
||||
find: ".SIDEBAR,disableToolbar:",
|
||||
replacement: {
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ export default definePlugin({
|
|||
description: "API to add sections near the 'Member Since' area of user profile panels.",
|
||||
authors: [Devs.thororen],
|
||||
patches: [
|
||||
// DM User Sidebar
|
||||
// dm user sidebar
|
||||
{
|
||||
find: "#{intl::PREMIUM_GIFTING_BUTTON}),action:",
|
||||
find: '"UserProfileSidebar"',
|
||||
replacement: {
|
||||
match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\))/,
|
||||
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:true})",
|
||||
}
|
||||
},
|
||||
// User Profile Modal
|
||||
// user profile modal
|
||||
{
|
||||
find: ",applicationRoleConnection:",
|
||||
replacement: {
|
||||
|
|
@ -28,7 +28,7 @@ export default definePlugin({
|
|||
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:false}),",
|
||||
}
|
||||
},
|
||||
// User Profile Modal v2
|
||||
// user profile modal v2
|
||||
{
|
||||
find: ".MODAL_V2,onClose:",
|
||||
replacement: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { t } from "@utils/translation";
|
|||
import definePlugin from "@utils/types";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
|
||||
const { iconForeground } = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper");
|
||||
const accountClasses = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper");
|
||||
|
||||
export default definePlugin({
|
||||
name: "UserAreaAPI",
|
||||
|
|
@ -38,7 +38,7 @@ export default definePlugin({
|
|||
renderButtons(props: { nameplate?: any; }) {
|
||||
return Vencord.Api.UserArea._renderButtons({
|
||||
nameplate: props.nameplate,
|
||||
iconForeground: props.nameplate != null ? iconForeground : void 0,
|
||||
iconForeground: accountClasses.iconForeground,
|
||||
hideTooltips: this.shouldHideTooltips()
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,19 +14,20 @@ export default definePlugin({
|
|||
name: "ConcatenatedModules",
|
||||
description: "Extract modules that have been concatenated by the bundler",
|
||||
authors: [Devs.thororen],
|
||||
|
||||
required: true,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "AngleBracketsIcon",
|
||||
replacement: {
|
||||
match: /\i\.\i\((\i)\),\i\.\i\(\i,\{AIcon/,
|
||||
replace: "$self.iconsModule($1),$&"
|
||||
replace: "$self.setIconsModule($1),$&"
|
||||
}
|
||||
}
|
||||
],
|
||||
iconsModule(value: IconsDef) {
|
||||
setIconsModule(value: IconsDef) {
|
||||
iconsModule = value;
|
||||
|
||||
// incase you dont want to use iconviewer you can do
|
||||
// Vencord.Plugins.plugins.ConcatenatedModules.iconsModule instead for icons and viewing paths
|
||||
this.iconsModule = value;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,17 +17,14 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { copyWithToast } from "@utils/discord";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, ChannelStore } from "@webpack/common";
|
||||
import { ChannelStore, Modal, openModal } from "@webpack/common";
|
||||
|
||||
function DecodeIcon() {
|
||||
return (
|
||||
|
|
@ -65,32 +62,26 @@ function decodeBase64Strings(base64Strings) {
|
|||
}
|
||||
|
||||
function openDecodedBase64Modal(decodedContent) {
|
||||
const key = openModal(props => (
|
||||
openModal(props => (
|
||||
<ErrorBoundary>
|
||||
<ModalRoot {...props} size={ModalSize.LARGE}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{t("equicord.baseDecoder.ui.modalTitle")}</BaseText>
|
||||
<ModalCloseButton onClick={() => closeModal(key)} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<Heading>{t("equicord.baseDecoder.ui.decodedContent")}</Heading>
|
||||
{decodedContent.map((content, index) => (
|
||||
<CodeBlock key={index} content={content} lang="" />
|
||||
))}
|
||||
</div>
|
||||
</ModalContent >
|
||||
<ModalFooter>
|
||||
<Flex gap={10}>
|
||||
{decodedContent.map((content, index) => (
|
||||
<Button key={index} onClick={() => copyWithToast(content, t("equicord.baseDecoder.ui.copiedToast"))}>
|
||||
{t("equicord.baseDecoder.ui.copyDecoded")} {index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalRoot >
|
||||
</ErrorBoundary >
|
||||
<Modal
|
||||
{...props}
|
||||
size="lg"
|
||||
title={t("equicord.baseDecoder.ui.modalTitle")}
|
||||
actions={decodedContent.map((content, index) => ({
|
||||
text: `${t("equicord.baseDecoder.ui.copyDecoded")} ${index + 1}`,
|
||||
variant: "primary",
|
||||
onClick: () => copyWithToast(content, t("equicord.baseDecoder.ui.copiedToast"))
|
||||
}))}
|
||||
>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<Heading>{t("equicord.baseDecoder.ui.decodedContent")}</Heading>
|
||||
{decodedContent.map((content, index) => (
|
||||
<CodeBlock key={index} content={content} lang="" />
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
193
src/equicordplugins/blockKeywords/index.tsx
Normal file
193
src/equicordplugins/blockKeywords/index.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Card } from "@components/Card";
|
||||
import { HeadingTertiary } from "@components/Heading";
|
||||
import { ErrorBoundary } from "@components/index";
|
||||
import { Margins } from "@components/margins";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { React, TextInput } from "@webpack/common";
|
||||
|
||||
let blockedKeywords: Array<RegExp>;
|
||||
const cl = classNameFactory("vc-block-keywords-");
|
||||
|
||||
function splitPatterns(input: string): string[] {
|
||||
return input
|
||||
.replace(/\[([^\]]*)\]/g, m => m.replace(/,/g, "\x00")) // protect commas in [...]
|
||||
.replace(/\{(\d+,?\d*|,\d+)\}/g, m => m.replace(",", "\x00")) // protect commas in {n,m}
|
||||
.split(",")
|
||||
.map(s => s.replace(/\x00/g, ",").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function RegexHelper() {
|
||||
const [testInput, setTestInput] = React.useState("");
|
||||
const { blockedWords, caseSensitive, useRegex } = settings.use(["blockedWords", "caseSensitive", "useRegex"]);
|
||||
|
||||
const results = React.useMemo(() => {
|
||||
const caseSensitiveFlag = caseSensitive ? "" : "i";
|
||||
return splitPatterns(blockedWords)
|
||||
.map(pattern => {
|
||||
try {
|
||||
const regex = useRegex
|
||||
? new RegExp(pattern, caseSensitiveFlag)
|
||||
: new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag);
|
||||
return { pattern, matches: regex.test(testInput) };
|
||||
} catch (e: unknown) {
|
||||
return { pattern, matches: false, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
});
|
||||
}, [testInput, blockedWords, caseSensitive, useRegex]);
|
||||
|
||||
return (
|
||||
<Card className={cl("regex")}>
|
||||
<HeadingTertiary className={Margins.bottom8}>Regex Helper</HeadingTertiary>
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder="Input to test..."
|
||||
value={testInput}
|
||||
onChange={setTestInput}
|
||||
maxLength={null}
|
||||
/>
|
||||
{results.length === 0 ?
|
||||
<Card
|
||||
key="vc-no-patterns-regex"
|
||||
variant="warning"
|
||||
className={classes(cl("card"), Margins.top8)}
|
||||
>
|
||||
<code>No patterns configured</code>
|
||||
</Card> : (
|
||||
results.map(({ pattern, matches, error }, i) => (
|
||||
<Card
|
||||
key={`vc-pattern-card-${i}`}
|
||||
variant={error ? "danger" : matches ? "success" : "primary"}
|
||||
className={classes(cl("card"), Margins.top8)}
|
||||
>
|
||||
<code>{pattern}</code>
|
||||
{error && <span className={cl("error")}>{error}</span>}
|
||||
</Card>
|
||||
)))}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
blockedWords: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma-separated list of words to block",
|
||||
default: "",
|
||||
restartNeeded: true
|
||||
},
|
||||
useRegex: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use each value as a regular expression when checking message content (advanced)",
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
regexHelper: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Test your regular expressions against a sample input",
|
||||
component: () => <ErrorBoundary noop><RegexHelper /></ErrorBoundary>,
|
||||
},
|
||||
caseSensitive: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to use a case sensitive search or not",
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
ignoreBlockedMessages: {
|
||||
description: "Completely ignores (recent) new messages bar",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
}, {
|
||||
regexHelper: {
|
||||
hidden() { return !this.store.useRegex; }
|
||||
}
|
||||
});
|
||||
|
||||
export function containsBlockedKeywords(message: Message) {
|
||||
if (!blockedKeywords) return false;
|
||||
|
||||
// test a nullable string against all keywords
|
||||
const testField = (text: string | null | undefined) => text != null && blockedKeywords.some(regex => regex.test(text));
|
||||
|
||||
return blockedKeywords.some(regex =>
|
||||
regex.test(message.content)) || message.embeds.some(embed =>
|
||||
testField(embed.rawDescription) || testField(embed.rawTitle));
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "BlockKeywords",
|
||||
description: "Blocks messages containing specific user-defined keywords, as if the user sending them was blocked.",
|
||||
tags: ["Appearance", "Customisation", "Privacy"],
|
||||
authors: [EquicordDevs.catcraft, EquicordDevs.secp192k1],
|
||||
patches: [
|
||||
{
|
||||
find: "_channelMessages={}",
|
||||
predicate: () => settings.store.blockedWords !== "",
|
||||
replacement: {
|
||||
match: /static commit\((\i)\)\{/g,
|
||||
replace: "$&$1=$self.blockMessagesWithKeywords($1);"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: '"MessageStore"',
|
||||
predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=MESSAGE_CREATE:function\((\i)\){)/,
|
||||
replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: '"ReadStateStore"',
|
||||
predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=MESSAGE_CREATE:function\((\i)\){)/,
|
||||
replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;`
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
settings,
|
||||
containsBlockedKeywords,
|
||||
|
||||
start() {
|
||||
const blockedWordsList = splitPatterns(settings.store.blockedWords);
|
||||
const caseSensitiveFlag = settings.store.caseSensitive ? "" : "i";
|
||||
|
||||
if (blockedWordsList.length === 0) return;
|
||||
|
||||
if (settings.store.useRegex) {
|
||||
blockedKeywords = blockedWordsList.map(word => {
|
||||
return new RegExp(word, caseSensitiveFlag);
|
||||
});
|
||||
} else {
|
||||
blockedKeywords = blockedWordsList.map(word => {
|
||||
// escape regex chars in word https://stackoverflow.com/a/6969486
|
||||
return new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
blockMessagesWithKeywords(messageList) {
|
||||
return messageList.reset(messageList.map(
|
||||
message => message.set("blocked", message.blocked || this.containsBlockedKeywords(message))
|
||||
));
|
||||
}
|
||||
});
|
||||
16
src/equicordplugins/blockKeywords/styles.css
Normal file
16
src/equicordplugins/blockKeywords/styles.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.vc-block-keywords-regex {
|
||||
padding: 0.75em
|
||||
}
|
||||
|
||||
.vc-block-keywords-card {
|
||||
padding: 0.4em 0.75em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.vc-block-keywords-error {
|
||||
font-size: 12px;
|
||||
margin-left: 1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -10,9 +10,8 @@ import { CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, NitroIcon
|
|||
import { classNameFactory } from "@utils/css";
|
||||
import { getGuildAcronym, getIntlMessage } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { closeModal, openModal } from "@utils/modal";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, TextInput, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common";
|
||||
import { Avatar, ChannelStore, closeModal, ContextMenuApi, FluxDispatcher, GuildStore, Menu, openModal,React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, TextInput, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common";
|
||||
|
||||
import { NotificationDot } from "./ChannelTab";
|
||||
import { BookmarkContextMenu, EditModal } from "./ContextMenus";
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import { bookmarkFolderColors, bookmarkPlaceholderName, closeOtherTabs, closeTab
|
|||
import { Bookmark, BookmarkFolder, Bookmarks, ChannelTabsProps, UseBookmarkMethods } from "@equicordplugins/channelTabs/util/types";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { Button, ChannelStore, ColorPicker, FluxDispatcher, Menu, ReadStateStore, ReadStateUtils, Select, TextInput, useMemo, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Button, ChannelStore, closeModal, ColorPicker, FluxDispatcher, Menu, Modal, openModal, ReadStateStore, ReadStateUtils, Select, TextInput, useMemo, useState } from "@webpack/common";
|
||||
|
||||
const legacyFolderColors: Record<string, string> = {
|
||||
"var(--channeltabs-red)": bookmarkFolderColors.Red,
|
||||
|
|
@ -116,7 +116,7 @@ function FolderChipPreview({ name, color, iconName }: { name: string; color: str
|
|||
}
|
||||
|
||||
function FolderIconPickerModal({ modalProps, modalKey, name, color, iconName, onSelect, onColorChange }: {
|
||||
modalProps: ModalProps,
|
||||
modalProps: RenderModalProps,
|
||||
modalKey: string,
|
||||
name: string,
|
||||
color: string,
|
||||
|
|
@ -134,73 +134,72 @@ function FolderIconPickerModal({ modalProps, modalKey, name, color, iconName, on
|
|||
}, [iconNames, search]);
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold">Choose Folder Icon</BaseText>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Heading className={Margins.top16}>Preview</Heading>
|
||||
<FolderChipPreview name={name} color={localColor} iconName={iconName} />
|
||||
<Heading className={Margins.top16}>Icon Color</Heading>
|
||||
<div className={Margins.top8}>
|
||||
<ColorPicker
|
||||
color={colorToInt(localColor)}
|
||||
onChange={value => {
|
||||
const nextColor = intToColor(value);
|
||||
setLocalColor(nextColor);
|
||||
onColorChange(nextColor);
|
||||
}}
|
||||
showEyeDropper={false}
|
||||
/>
|
||||
</div>
|
||||
<Heading className={Margins.top16}>Search</Heading>
|
||||
<TextInput
|
||||
value={search}
|
||||
placeholder={`Search ${iconNames.length} icons...`}
|
||||
onChange={setSearch}
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="sm"
|
||||
title={<BaseText size="lg" weight="semibold">Choose Folder Icon</BaseText>}
|
||||
actions={[
|
||||
{
|
||||
text: "Close",
|
||||
variant: "secondary",
|
||||
onClick: () => closeModal(modalKey)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Heading className={Margins.top16}>Preview</Heading>
|
||||
<FolderChipPreview name={name} color={localColor} iconName={iconName} />
|
||||
<Heading className={Margins.top16}>Icon Color</Heading>
|
||||
<div className={Margins.top8}>
|
||||
<ColorPicker
|
||||
color={colorToInt(localColor)}
|
||||
onChange={value => {
|
||||
const nextColor = intToColor(value);
|
||||
setLocalColor(nextColor);
|
||||
onColorChange(nextColor);
|
||||
}}
|
||||
showEyeDropper={false}
|
||||
/>
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(52px, 1fr))",
|
||||
marginTop: 16,
|
||||
maxHeight: 320,
|
||||
overflowY: "auto"
|
||||
}}>
|
||||
{filteredIconNames.map(name => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onSelect(name);
|
||||
closeModal(modalKey);
|
||||
}}
|
||||
style={{
|
||||
alignItems: "center",
|
||||
background: name === iconName ? "var(--background-modifier-hover)" : "var(--background-secondary)",
|
||||
border: "1px solid var(--border-subtle)",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
height: 52,
|
||||
justifyContent: "center",
|
||||
padding: 0
|
||||
}}
|
||||
title={name}
|
||||
type="button"
|
||||
>
|
||||
<FolderGlyph color={localColor} iconName={name} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.FILLED}
|
||||
onClick={() => closeModal(modalKey)}
|
||||
>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
<Heading className={Margins.top16}>Search</Heading>
|
||||
<TextInput
|
||||
value={search}
|
||||
placeholder={`Search ${iconNames.length} icons...`}
|
||||
onChange={setSearch}
|
||||
/>
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(52px, 1fr))",
|
||||
marginTop: 16,
|
||||
maxHeight: 320,
|
||||
overflowY: "auto"
|
||||
}}>
|
||||
{filteredIconNames.map(name => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onSelect(name);
|
||||
closeModal(modalKey);
|
||||
}}
|
||||
style={{
|
||||
alignItems: "center",
|
||||
background: name === iconName ? "var(--background-modifier-hover)" : "var(--background-secondary)",
|
||||
border: "1px solid var(--border-subtle)",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
height: 52,
|
||||
justifyContent: "center",
|
||||
padding: 0
|
||||
}}
|
||||
title={name}
|
||||
type="button"
|
||||
>
|
||||
<FolderGlyph color={localColor} iconName={name} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +263,7 @@ function FolderAppearanceFields({
|
|||
}
|
||||
|
||||
export function EditModal({ modalProps, modalKey, bookmark, onSave }: {
|
||||
modalProps: ModalProps,
|
||||
modalProps: RenderModalProps,
|
||||
modalKey: string,
|
||||
bookmark: Bookmark | BookmarkFolder,
|
||||
onSave: (name: string, color: string, iconName?: string) => void;
|
||||
|
|
@ -275,48 +274,47 @@ export function EditModal({ modalProps, modalKey, bookmark, onSave }: {
|
|||
const placeholder = bookmarkPlaceholderName(bookmark);
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold">Edit Bookmark</BaseText>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
{isBookmarkFolder(bookmark)
|
||||
? <FolderAppearanceFields
|
||||
name={name}
|
||||
setName={setName}
|
||||
color={color}
|
||||
setColor={setColor}
|
||||
iconName={iconName}
|
||||
setIconName={setIconName}
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="sm"
|
||||
title={<BaseText size="lg" weight="semibold">Edit Bookmark</BaseText>}
|
||||
actions={[
|
||||
{
|
||||
text: "Save",
|
||||
variant: "primary",
|
||||
onClick: () => onSave(name || placeholder, color, iconName)
|
||||
},
|
||||
{
|
||||
text: t("vencord.cancel"),
|
||||
variant: "secondary",
|
||||
onClick: () => closeModal(modalKey)
|
||||
}
|
||||
]}
|
||||
>
|
||||
{isBookmarkFolder(bookmark)
|
||||
? <FolderAppearanceFields
|
||||
name={name}
|
||||
setName={setName}
|
||||
color={color}
|
||||
setColor={setColor}
|
||||
iconName={iconName}
|
||||
setIconName={setIconName}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
: <>
|
||||
<Heading className={Margins.top16}>Bookmark Name</Heading>
|
||||
<TextInput
|
||||
value={name === placeholder ? undefined : name}
|
||||
placeholder={placeholder}
|
||||
onChange={setName}
|
||||
/>
|
||||
: <>
|
||||
<Heading className={Margins.top16}>Bookmark Name</Heading>
|
||||
<TextInput
|
||||
value={name === placeholder ? undefined : name}
|
||||
placeholder={placeholder}
|
||||
onChange={setName}
|
||||
/>
|
||||
</>}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => onSave(name || placeholder, color, iconName)}
|
||||
>Save</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.FILLED}
|
||||
onClick={() => closeModal(modalKey)}
|
||||
>
|
||||
{t("vencord.cancel")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</>}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: {
|
||||
modalProps: any,
|
||||
modalProps: RenderModalProps,
|
||||
modalKey: string,
|
||||
bookmarks: Bookmarks,
|
||||
onSave: (folderIndex: number, folderName: string, folderColor: string, iconName?: string) => void;
|
||||
|
|
@ -327,80 +325,79 @@ function AddToFolderModal({ modalProps, modalKey, bookmarks, onSave }: {
|
|||
const [iconName, setIconName] = useState<string | undefined>();
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold">Add Bookmark to Folder</BaseText>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Heading className={Margins.top16}>Select a folder</Heading>
|
||||
<Select
|
||||
options={[...Object.entries(bookmarks)
|
||||
.filter(([, bookmark]) => isBookmarkFolder(bookmark))
|
||||
.map(([index, bookmark]) => ({
|
||||
label: bookmark.name,
|
||||
value: parseInt(index, 10)
|
||||
})),
|
||||
{
|
||||
label: "Create one",
|
||||
value: -1,
|
||||
default: true
|
||||
}]}
|
||||
isSelected={v => v === folderIndex}
|
||||
select={setIndex}
|
||||
serialize={String}
|
||||
/>
|
||||
{folderIndex === -1 && <FolderAppearanceFields
|
||||
name={folderName}
|
||||
setName={setFolderName}
|
||||
color={folderColor}
|
||||
setColor={setFolderColor}
|
||||
iconName={iconName}
|
||||
setIconName={setIconName}
|
||||
/>}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => onSave(folderIndex, folderName, folderColor, iconName)}
|
||||
>Save</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.FILLED}
|
||||
onClick={() => closeModal(modalKey)}
|
||||
>
|
||||
{t("vencord.cancel")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="sm"
|
||||
title={<BaseText size="lg" weight="semibold">Add Bookmark to Folder</BaseText>}
|
||||
actions={[
|
||||
{
|
||||
text: "Save",
|
||||
variant: "primary",
|
||||
onClick: () => onSave(folderIndex, folderName, folderColor, iconName)
|
||||
},
|
||||
{
|
||||
text: t("vencord.cancel"),
|
||||
variant: "secondary",
|
||||
onClick: () => closeModal(modalKey)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Heading className={Margins.top16}>Select a folder</Heading>
|
||||
<Select
|
||||
options={[...Object.entries(bookmarks)
|
||||
.filter(([, bookmark]) => isBookmarkFolder(bookmark))
|
||||
.map(([index, bookmark]) => ({
|
||||
label: bookmark.name,
|
||||
value: parseInt(index, 10)
|
||||
})),
|
||||
{
|
||||
label: "Create one",
|
||||
value: -1,
|
||||
default: true
|
||||
}]}
|
||||
isSelected={v => v === folderIndex}
|
||||
select={setIndex}
|
||||
serialize={String}
|
||||
/>
|
||||
{folderIndex === -1 && <FolderAppearanceFields
|
||||
name={folderName}
|
||||
setName={setFolderName}
|
||||
color={folderColor}
|
||||
setColor={setFolderColor}
|
||||
iconName={iconName}
|
||||
setIconName={setIconName}
|
||||
/>}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }) {
|
||||
function DeleteFolderConfirmationModal({ modalProps, modalKey, onConfirm }: {
|
||||
modalProps: RenderModalProps,
|
||||
modalKey: string,
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold">Are you sure?</BaseText>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Paragraph className={Margins.top16}>
|
||||
Deleting a bookmark folder will also delete all bookmarks within it.
|
||||
</Paragraph>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.FILLED}
|
||||
onClick={() => closeModal(modalKey)}
|
||||
>
|
||||
{t("vencord.cancel")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="sm"
|
||||
title={<BaseText size="lg" weight="semibold">Are you sure?</BaseText>}
|
||||
actions={[
|
||||
{
|
||||
text: "Delete",
|
||||
variant: "critical-primary",
|
||||
onClick: onConfirm
|
||||
},
|
||||
{
|
||||
text: t("vencord.cancel"),
|
||||
variant: "secondary",
|
||||
onClick: () => closeModal(modalKey)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Paragraph className={Margins.top16}>
|
||||
Deleting a bookmark folder will also delete all bookmarks within it.
|
||||
</Paragraph>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default definePlugin({
|
|||
description: t("equicord.channelTabs.description"),
|
||||
tags: ["Appearance", "Customisation", "Organisation", "Servers"],
|
||||
authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke, EquicordDevs.keircn],
|
||||
dependencies: ["ContextMenuAPI"],
|
||||
dependencies: ["ContextMenuAPI", "ConcatenatedModules"],
|
||||
contextMenus: {
|
||||
"channel-mention-context": contextMenuPatch,
|
||||
"channel-context": contextMenuPatch,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const settings = definePluginSettings({
|
|||
},
|
||||
enableSpeakingIndicators: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable speaking indicators",
|
||||
description: t("equicord.clipsEnhancements.settings.enableSpeakingIndicators"),
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
|
@ -104,14 +104,6 @@ export default definePlugin({
|
|||
replace: "$self.patchTimeslots($&)"
|
||||
}
|
||||
},
|
||||
// enables clips
|
||||
{
|
||||
find: "2026-03-clips-experiment",
|
||||
replacement: {
|
||||
match: /defaultConfig:\{enableClips:!\d,ignorePlatformRestriction:!\d,enableScreenshotKeybind:!\d,enableVoiceOnlyClips:!\d,enableSpeakingIndicators:!\d,enableAdvancedSignals:!\d\}/,
|
||||
replace: "defaultConfig:{enableClips:!0,ignorePlatformRestriction:$self.settings.store.ignorePlatformRestriction,enableScreenshotKeybind:$self.settings.store.enableScreenshotKeybind,enableVoiceOnlyClips:$self.settings.store.enableVoiceOnlyClips,enableSpeakingIndicators:$self.settings.store.enableSpeakingIndicators,enableAdvancedSignals:$self.settings.store.enableAdvancedSignals}"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
patchTimeslots(timeslots: { id: string; value: number; label: string; }[]) {
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ChannelToolbarButton } from "@api/HeaderBar";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ContextMenuApi, Menu } from "@webpack/common";
|
||||
import type { ReactNode, SVGProps } from "react";
|
||||
|
||||
import { PanelId, panelRegistry, settings, toolbarPanelOrder } from "./settings";
|
||||
import managedStyle from "./style.css?managed";
|
||||
|
||||
const cl = classNameFactory("vc-collapsible-ui-");
|
||||
|
||||
type StateStore = ReturnType<typeof settings.use>;
|
||||
|
||||
function PanelsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path fill="currentColor" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3H3V5Zm0 5h6v11H5a2 2 0 0 1-2-2V10Zm8 0h10v9a2 2 0 0 1-2 2H11V10Zm2-5h8v3h-8V5Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function getStateClasses(store: StateStore) {
|
||||
const classNames = [cl("state"), cl(`collapsed-size-${store.collapsedSize}`), cl(`transition-${store.transitionMs}`)];
|
||||
|
||||
for (const panelId of toolbarPanelOrder) {
|
||||
const panel = panelRegistry[panelId];
|
||||
if (store[panel.collapsedKey]) classNames.push(cl(`${panel.classId}-collapsed`));
|
||||
}
|
||||
|
||||
return classes(...classNames);
|
||||
}
|
||||
|
||||
function togglePanel(panelId: PanelId) {
|
||||
const key = panelRegistry[panelId].collapsedKey;
|
||||
settings.store[key] = !settings.store[key];
|
||||
}
|
||||
|
||||
const ToolbarMenu = ErrorBoundary.wrap(({ onClose }: { onClose(): void; }) => {
|
||||
const store = settings.use();
|
||||
|
||||
return (
|
||||
<Menu.Menu navId="vc-collapsible-ui-toolbar-menu" onClose={onClose} aria-label="Collapsible UI">
|
||||
{toolbarPanelOrder.map(panelId => {
|
||||
const panel = panelRegistry[panelId];
|
||||
const collapsed = store[panel.collapsedKey];
|
||||
|
||||
return (
|
||||
<Menu.MenuCheckboxItem
|
||||
key={panelId}
|
||||
id={`vc-collapsible-ui-${panel.classId}`}
|
||||
label={panel.label}
|
||||
checked={!collapsed}
|
||||
action={() => togglePanel(panelId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu.Menu>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
const ToolbarButtons = ErrorBoundary.wrap(() => {
|
||||
const store = settings.use();
|
||||
const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className={getStateClasses(store)} style={{ display: "none" }} />
|
||||
<ChannelToolbarButton
|
||||
icon={PanelsIcon}
|
||||
tooltip="Collapsible UI"
|
||||
aria-label="Collapsible UI"
|
||||
selected={anyCollapsed}
|
||||
onClick={event => ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />)}
|
||||
onContextMenu={event => ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => {
|
||||
const { chatButtonsCollapsed } = settings.use();
|
||||
|
||||
if (buttons.length === 0) return <>{buttons}</>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes(
|
||||
cl("chat-buttons"),
|
||||
chatButtonsCollapsed && cl("chat-buttons-collapsed")
|
||||
)}
|
||||
>
|
||||
<div className={cl("chat-buttons-items")}>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
export default definePlugin({
|
||||
name: "CollapsibleUI",
|
||||
description: "Native collapsible channel, member, chat button, and user area surfaces.",
|
||||
tags: ["Appearance", "Customisation", "Chat", "Servers"],
|
||||
dependencies: ["HeaderBarAPI", "ChatInputButtonAPI"],
|
||||
authors: [EquicordDevs.benjii],
|
||||
searchTerms: ["ui", "sidebar", "layout"],
|
||||
managedStyle,
|
||||
settings,
|
||||
|
||||
headerBarButton: {
|
||||
icon: PanelsIcon,
|
||||
location: "channeltoolbar",
|
||||
priority: 25,
|
||||
render: () => <ToolbarButtons />,
|
||||
},
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".DISPLAY_NAME_STYLES_COACHMARK)",
|
||||
replacement: {
|
||||
match: /className:(\i\.\i),(?=style:(\i),children:\[)/,
|
||||
replace: "className:$self.userAreaControlsClass($1),"
|
||||
}
|
||||
},
|
||||
],
|
||||
chatBarButtonWrapper: (buttons: ReactNode) => {
|
||||
if (!Array.isArray(buttons) || buttons.length === 0) return buttons;
|
||||
return <ChatButtonsRow buttons={buttons} />;
|
||||
},
|
||||
|
||||
userAreaControlsClass(className: string) {
|
||||
return classes(className, cl("user-area-controls"));
|
||||
},
|
||||
});
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
export const transitionMarkers = [0, 80, 120, 160, 200, 260, 320];
|
||||
export const collapsedSizeMarkers = [8, 12, 16, 20, 24, 28, 32, 40, 48];
|
||||
|
||||
export const panelRegistry = {
|
||||
guildBar: {
|
||||
id: "guildBar",
|
||||
classId: "guild-bar",
|
||||
label: "Guild Bar",
|
||||
collapsedKey: "guildBarCollapsed",
|
||||
},
|
||||
channelList: {
|
||||
id: "channelList",
|
||||
classId: "channel-list",
|
||||
label: "Channel List",
|
||||
collapsedKey: "channelListCollapsed",
|
||||
},
|
||||
membersList: {
|
||||
id: "membersList",
|
||||
classId: "members-list",
|
||||
label: "Members List",
|
||||
collapsedKey: "membersListCollapsed",
|
||||
},
|
||||
chatButtons: {
|
||||
id: "chatButtons",
|
||||
classId: "chat-buttons",
|
||||
label: "Message Buttons",
|
||||
collapsedKey: "chatButtonsCollapsed",
|
||||
},
|
||||
titleBar: {
|
||||
id: "titleBar",
|
||||
classId: "title-bar",
|
||||
label: "Title Bar",
|
||||
collapsedKey: "titleBarCollapsed",
|
||||
},
|
||||
headerBar: {
|
||||
id: "headerBar",
|
||||
classId: "header-bar",
|
||||
label: "Header Bar",
|
||||
collapsedKey: "headerBarCollapsed",
|
||||
},
|
||||
userArea: {
|
||||
id: "userArea",
|
||||
classId: "user-area",
|
||||
label: "User Area",
|
||||
collapsedKey: "userAreaCollapsed",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PanelId = keyof typeof panelRegistry;
|
||||
|
||||
export const toolbarPanelOrder = ["guildBar", "channelList", "membersList", "chatButtons", "titleBar", "headerBar", "userArea"] as const satisfies readonly PanelId[];
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
collapsedSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Collapsed hover area size in pixels.",
|
||||
default: 12,
|
||||
markers: collapsedSizeMarkers,
|
||||
},
|
||||
transitionMs: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Panel transition speed in milliseconds.",
|
||||
default: 160,
|
||||
markers: transitionMarkers,
|
||||
},
|
||||
guildBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the guild bar as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
channelListCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the channel list as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
membersListCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the members list as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
chatButtonsCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the message button row as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
titleBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the title bar as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
headerBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the header bar as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
userAreaCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the user area as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,416 +0,0 @@
|
|||
@property --vc-cui-collapsed-size {
|
||||
syntax: "<length>";
|
||||
inherits: true;
|
||||
initial-value: 12px;
|
||||
}
|
||||
|
||||
@property --vc-cui-transition {
|
||||
syntax: "<time>";
|
||||
inherits: true;
|
||||
initial-value: 160ms;
|
||||
}
|
||||
|
||||
:root {
|
||||
--vc-cui-chat-buttons-width: min(320px, 36vw);
|
||||
--vc-cui-hover-peek: var(--vc-cui-collapsed-size);
|
||||
--vc-cui-hover-delay: 120ms;
|
||||
--vc-cui-header-bar-height: 32px;
|
||||
--vc-cui-user-area-collapsed-width: 52px;
|
||||
--vc-cui-ease: cubic-bezier(.2, 0, 0, 1);
|
||||
--vc-cui-fade-transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
|
||||
--vc-cui-inline-transition:
|
||||
width var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
inline-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
min-width var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
min-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
max-width var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
flex-basis var(--vc-cui-transition) var(--vc-cui-ease);
|
||||
--vc-cui-block-transition:
|
||||
height var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
block-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
min-height var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
min-block-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
max-height var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
max-block-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
padding var(--vc-cui-transition) var(--vc-cui-ease)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-8) {
|
||||
--vc-cui-collapsed-size: 8px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-12) {
|
||||
--vc-cui-collapsed-size: 12px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-16) {
|
||||
--vc-cui-collapsed-size: 16px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-20) {
|
||||
--vc-cui-collapsed-size: 20px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-24) {
|
||||
--vc-cui-collapsed-size: 24px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-28) {
|
||||
--vc-cui-collapsed-size: 28px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-32) {
|
||||
--vc-cui-collapsed-size: 32px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-40) {
|
||||
--vc-cui-collapsed-size: 40px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-48) {
|
||||
--vc-cui-collapsed-size: 48px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-0) {
|
||||
--vc-cui-transition: 0ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-80) {
|
||||
--vc-cui-transition: 80ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-120) {
|
||||
--vc-cui-transition: 120ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-160) {
|
||||
--vc-cui-transition: 160ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-200) {
|
||||
--vc-cui-transition: 200ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-260) {
|
||||
--vc-cui-transition: 260ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-transition-320) {
|
||||
--vc-cui-transition: 320ms
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) {
|
||||
z-index: 3
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"]), :is(:hover, :focus-within), :is(.vc-betterFolders-sidebar-grid:has(.vc-betterFolders-sidebar:is(:hover, :focus-within)) nav[class*="guilds_"])) {
|
||||
background: var(--background-gradient-low, var(--background-base-lowest)) !important;
|
||||
border-start-end-radius: var(--radius-sm, 8px);
|
||||
width: var(--vc-cui-collapsed-size) !important;
|
||||
inline-size: var(--vc-cui-collapsed-size) !important;
|
||||
min-width: var(--vc-cui-collapsed-size) !important;
|
||||
min-inline-size: var(--vc-cui-collapsed-size) !important;
|
||||
max-width: var(--vc-cui-collapsed-size) !important;
|
||||
max-inline-size: var(--vc-cui-collapsed-size) !important;
|
||||
flex: 0 0 var(--vc-cui-collapsed-size) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within)) .vc-betterFolders-sidebar {
|
||||
z-index: 3;
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:not(:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within))) .vc-betterFolders-sidebar {
|
||||
width: 0 !important;
|
||||
inline-size: 0 !important;
|
||||
min-width: 0 !important;
|
||||
min-inline-size: 0 !important;
|
||||
max-width: 0 !important;
|
||||
max-inline-size: 0 !important;
|
||||
flex: 0 0 0 !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"]:is(:hover, :focus-within) {
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within))) [class*="sidebarList_"]:not(:is(:hover, :focus-within)) {
|
||||
width: var(--vc-cui-collapsed-size) !important;
|
||||
inline-size: var(--vc-cui-collapsed-size) !important;
|
||||
min-width: var(--vc-cui-collapsed-size) !important;
|
||||
min-inline-size: var(--vc-cui-collapsed-size) !important;
|
||||
max-width: var(--vc-cui-collapsed-size) !important;
|
||||
max-inline-size: var(--vc-cui-collapsed-size) !important;
|
||||
flex: 0 0 var(--vc-cui-collapsed-size) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
|
||||
width: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
inline-size: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
min-width: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
min-inline-size: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
max-width: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
max-inline-size: var(--vc-cui-user-area-collapsed-width) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) .vc-collapsible-ui-user-area-controls {
|
||||
max-width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
max-width var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
opacity var(--vc-cui-transition) var(--vc-cui-ease) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within) .vc-collapsible-ui-user-area-controls {
|
||||
opacity: 1;
|
||||
pointer-events: auto
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:is(:hover, :focus-within) {
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:not(:is(:hover, :focus-within)) {
|
||||
width: var(--vc-cui-collapsed-size) !important;
|
||||
min-width: var(--vc-cui-collapsed-size) !important;
|
||||
max-width: var(--vc-cui-collapsed-size) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
|
||||
height: var(--vc-cui-hover-peek) !important;
|
||||
block-size: var(--vc-cui-hover-peek) !important;
|
||||
min-height: var(--vc-cui-hover-peek) !important;
|
||||
min-block-size: var(--vc-cui-hover-peek) !important;
|
||||
max-height: var(--vc-cui-hover-peek) !important;
|
||||
max-block-size: var(--vc-cui-hover-peek) !important;
|
||||
overflow: hidden !important;
|
||||
border-top-width: 0 !important;
|
||||
padding-block: 0 !important;
|
||||
transition: var(--vc-cui-block-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed):has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
|
||||
width: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
min-width: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
min-inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
max-width: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
max-inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-block-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:is(:hover, :focus-within) {
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:not(:is(:hover, :focus-within)) {
|
||||
height: var(--vc-cui-hover-peek) !important;
|
||||
block-size: var(--vc-cui-hover-peek) !important;
|
||||
min-height: var(--vc-cui-hover-peek) !important;
|
||||
min-block-size: var(--vc-cui-hover-peek) !important;
|
||||
max-height: var(--vc-cui-hover-peek) !important;
|
||||
max-block-size: var(--vc-cui-hover-peek) !important;
|
||||
padding-block: 0 !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]) {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-block-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="base_"]:has(> [class*="bar_"] > [class*="title_"]) {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-rows: [top] var(--vc-cui-hover-peek) [titleBarEnd] auto [noticeEnd] minmax(0, 1fr) [end] !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="base_"]:has(> [class*="bar_"]:is(:hover, :focus-within) > [class*="title_"]) {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-rows: [top] var(--vc-cui-header-bar-height) [titleBarEnd] auto [noticeEnd] minmax(0, 1fr) [end] !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):is(:hover, :focus-within) {
|
||||
height: var(--vc-cui-header-bar-height) !important;
|
||||
block-size: var(--vc-cui-header-bar-height) !important;
|
||||
min-height: var(--vc-cui-header-bar-height) !important;
|
||||
min-block-size: var(--vc-cui-header-bar-height) !important;
|
||||
max-height: var(--vc-cui-header-bar-height) !important;
|
||||
max-block-size: var(--vc-cui-header-bar-height) !important;
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):not(:is(:hover, :focus-within)) {
|
||||
height: var(--vc-cui-hover-peek) !important;
|
||||
block-size: var(--vc-cui-hover-peek) !important;
|
||||
min-height: var(--vc-cui-hover-peek) !important;
|
||||
min-block-size: var(--vc-cui-hover-peek) !important;
|
||||
max-height: var(--vc-cui-hover-peek) !important;
|
||||
max-block-size: var(--vc-cui-hover-peek) !important;
|
||||
padding-block: 0 !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within)) .vc-betterFolders-sidebar > *,
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: calc(var(--vc-cui-hover-delay) + var(--vc-cui-transition))
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) > [class*="unreadMentionsIndicator"] {
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:not(:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within))) .vc-betterFolders-sidebar > *,
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"]), :is(:hover, :focus-within), :is(.vc-betterFolders-sidebar-grid:has(.vc-betterFolders-sidebar:is(:hover, :focus-within)) nav[class*="guilds_"])) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within))) [class*="sidebarList_"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
||||
.vc-collapsible-ui-chat-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-inline-size: 0
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-collapsed {
|
||||
min-inline-size: var(--vc-cui-hover-peek)
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-inline-size: 0;
|
||||
max-inline-size: var(--vc-cui-chat-buttons-width);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
opacity var(--vc-cui-transition) var(--vc-cui-ease),
|
||||
margin var(--vc-cui-transition) var(--vc-cui-ease)
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-collapsed .vc-collapsible-ui-chat-buttons-items {
|
||||
max-inline-size: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-collapsed:is(:hover, :focus-within) .vc-collapsible-ui-chat-buttons-items {
|
||||
max-inline-size: var(--vc-cui-chat-buttons-width);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body:has(.vc-collapsible-ui-state) {
|
||||
--vc-cui-transition: 0ms
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Heading } from "@components/Heading";
|
||||
import { CogWheel } from "@components/Icons";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import { type ModalProps,ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { TextInput, useCallback, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, openModal, TextInput, useCallback, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
|
||||
import {
|
||||
type CommandEntry, getCategoryGroupLabel, getCategoryWeight, getCommandSearchText, getRecentRank, getRegistryVersion, listCommands, subscribeRegistry
|
||||
|
|
@ -56,7 +57,7 @@ export function openCommandPicker(props: CommandPickerProps) {
|
|||
));
|
||||
}
|
||||
|
||||
function CommandPickerModal({ modalProps, commands: providedCommands, allowMultiple = false, initialQuery = "", initialSelectedIds = [], includeHiddenInSearch = false, emptyStateText = "No commands found", filter: filterPredicate, onSelect, onComplete, onClose }: CommandPickerProps & { modalProps: ModalProps; }) {
|
||||
function CommandPickerModal({ modalProps, commands: providedCommands, allowMultiple = false, initialQuery = "", initialSelectedIds = [], includeHiddenInSearch = false, emptyStateText = "No commands found", filter: filterPredicate, onSelect, onComplete, onClose }: CommandPickerProps & { modalProps: RenderModalProps; }) {
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [registryVersion, setRegistryVersion] = useState(() => getRegistryVersion());
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
|
|
@ -64,6 +65,7 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
|
|||
const [pickedIds, setPickedIds] = useState<string[]>(() => normalizedInitialSelected);
|
||||
const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const pickedIdsRef = useRef<string[]>([]);
|
||||
const listRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (providedCommands) return;
|
||||
|
|
@ -142,7 +144,12 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
|
|||
|
||||
for (const entry of availableCommands) {
|
||||
if (!includeHiddenInSearch && entry.hiddenInSearch) continue;
|
||||
const searchText = getCommandSearchText(entry.id);
|
||||
const searchText = getCommandSearchText(entry.id) || [
|
||||
entry.label,
|
||||
entry.description ?? "",
|
||||
...entry.keywords ?? [],
|
||||
...entry.tags ?? []
|
||||
].filter(Boolean).join(" ").toLowerCase();
|
||||
if (!searchText.includes(lowerQuery)) continue;
|
||||
|
||||
const score = scoreCommand(entry);
|
||||
|
|
@ -203,8 +210,22 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
|
|||
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0) return;
|
||||
const rawContainer = listRef.current as any;
|
||||
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
|
||||
const node = itemRefs.current[selectedIndex];
|
||||
node?.scrollIntoView({ block: "nearest" });
|
||||
if (!container || !node) return;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const nodeRect = node.getBoundingClientRect();
|
||||
|
||||
if (nodeRect.bottom > containerRect.bottom) {
|
||||
container.scrollTop += nodeRect.bottom - containerRect.bottom;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeRect.top < containerRect.top) {
|
||||
container.scrollTop -= containerRect.top - nodeRect.top;
|
||||
}
|
||||
}, [selectedIndex, items]);
|
||||
|
||||
itemRefs.current = itemRefs.current.slice(0, items.length);
|
||||
|
|
@ -321,17 +342,26 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
|
|||
};
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps} size={ModalSize.SMALL} className={classes(cn("vc-command-palette"), cl("picker"))}>
|
||||
<div className={classes(cl("shell"), cl("picker-shell"))} onKeyDown={handleKeyDown}>
|
||||
<div className={cl("input")}>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder="Search commands"
|
||||
className={cl("main-input")}
|
||||
/>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="lg"
|
||||
scrollerRef={listRef}
|
||||
title={
|
||||
<div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
|
||||
<Heading tag="h1" className={cl("title")}>Command Palette</Heading>
|
||||
|
||||
<div className={cl("input")}>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder="Search commands"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
|
||||
<div className={cl("list")}>
|
||||
{!hasCommands && <div className={cl("empty")}>{emptyStateText}</div>}
|
||||
{items.map((item, index) => {
|
||||
|
|
@ -373,6 +403,6 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,8 @@ export default definePlugin({
|
|||
});
|
||||
},
|
||||
|
||||
chatBarButtonWrapper: wrapChatBarChildren,
|
||||
chatBarButtonWrapper: {
|
||||
wrapper: wrapChatBarChildren,
|
||||
priority: 0,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ export interface RegistryStore {
|
|||
getTreeCommandCache(): Map<string, CommandEntry[]>;
|
||||
}
|
||||
|
||||
let instance: RegistryStore | null = null;
|
||||
|
||||
export function createRegistryStore(): RegistryStore {
|
||||
if (instance) return instance;
|
||||
|
||||
let registryVersion = 0;
|
||||
let cachedSortedCommands: CommandEntry[] | null = null;
|
||||
const categoryCommandCache = new Map<string, CommandEntry[]>();
|
||||
|
|
@ -38,7 +42,7 @@ export function createRegistryStore(): RegistryStore {
|
|||
for (const listener of registryListeners) listener(registryVersion);
|
||||
};
|
||||
|
||||
return {
|
||||
instance = {
|
||||
getRegistryVersion: () => registryVersion,
|
||||
bumpRegistryVersion: () => {
|
||||
registryVersion += 1;
|
||||
|
|
@ -77,4 +81,6 @@ export function createRegistryStore(): RegistryStore {
|
|||
getCategoryCommandCache: () => categoryCommandCache,
|
||||
getTreeCommandCache: () => treeCommandCache
|
||||
};
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@
|
|||
|
||||
/* Command Palette Modal Container */
|
||||
.vc-command-palette {
|
||||
--cp-bg: #0f0f10;
|
||||
--cp-bg-secondary: #18181a;
|
||||
--cp-bg-tertiary: #1f1f21;
|
||||
--cp-bg-secondary: transparent;
|
||||
--cp-bg-tertiary: transparent;
|
||||
--cp-bg-dropdown: #0b0b0d;
|
||||
--cp-bg-hover: #252528;
|
||||
--cp-bg-selected: #2a2a2e;
|
||||
|
|
@ -26,12 +25,6 @@
|
|||
--cp-radius-sm: 8px;
|
||||
--cp-shadow: 0 16px 44px rgb(0 0 0 / 38%), 0 6px 18px rgb(0 0 0 / 28%);
|
||||
|
||||
width: min(900px, 92vw) !important;
|
||||
max-width: min(900px, 92vw) !important;
|
||||
background: var(--cp-bg) !important;
|
||||
border-radius: var(--cp-radius) !important;
|
||||
box-shadow: var(--cp-shadow) !important;
|
||||
border: 1px solid var(--cp-border) !important;
|
||||
overflow: visible !important;
|
||||
box-sizing: border-box;
|
||||
will-change: opacity, transform;
|
||||
|
|
@ -44,21 +37,19 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 67vh;
|
||||
max-height: 67vh;
|
||||
background: var(--cp-bg);
|
||||
border-radius: var(--cp-radius);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Input Area */
|
||||
.vc-command-palette-input {
|
||||
padding: 12px 22px 10px;
|
||||
border-bottom: 1px solid var(--cp-border);
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
width: 105%;
|
||||
box-sizing: border-box;
|
||||
background: var(--bg-overlay-3, var(--background-secondary));
|
||||
}
|
||||
|
||||
.vc-command-palette-main-input input {
|
||||
|
|
@ -303,21 +294,8 @@
|
|||
.vc-command-palette-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vc-command-palette-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.vc-command-palette-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vc-command-palette-list::-webkit-scrollbar-thumb {
|
||||
background: var(--cp-bg-tertiary);
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Section Labels */
|
||||
|
|
@ -388,7 +366,7 @@
|
|||
}
|
||||
|
||||
.vc-command-palette-row-subtitle {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: var(--cp-text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
@ -435,22 +413,17 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
background: var(--cp-bg);
|
||||
border-top: 1px solid var(--cp-border);
|
||||
gap: 16px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.vc-command-palette-picker .vc-command-palette-shell {
|
||||
max-height: 62vh;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
background: var(--bg-overlay-3, var(--background-secondary));
|
||||
}
|
||||
|
||||
.vc-command-palette-picker .vc-command-palette-input {
|
||||
padding: 12px 16px 10px;
|
||||
}
|
||||
|
||||
.vc-command-palette-picker .vc-command-palette-list {
|
||||
max-height: 54vh;
|
||||
width: 105%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vc-command-palette-action-bar-action {
|
||||
|
|
@ -520,11 +493,13 @@
|
|||
.vc-command-palette-calculator {
|
||||
padding: 12px 16px;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--cp-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.vc-command-palette-calculator-title {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--cp-text-secondary);
|
||||
margin: 0 0 10px;
|
||||
|
|
@ -749,46 +724,7 @@
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Compact Mode */
|
||||
.vc-command-palette-compact .vc-command-palette-shell {
|
||||
max-height: fit-content;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vc-command-palette-compact {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.vc-command-palette-action-bar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--cp-text-muted);
|
||||
}
|
||||
|
||||
.vc-command-palette-compact .vc-command-palette-input {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vc-command-palette-action-bar-compact-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 12px;
|
||||
background: var(--cp-bg-secondary);
|
||||
border: 1px solid var(--cp-border);
|
||||
border-radius: 8px;
|
||||
color: var(--cp-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.vc-command-palette-action-bar-compact-btn:hover {
|
||||
background: var(--cp-bg-hover);
|
||||
border-color: var(--cp-border-strong);
|
||||
}
|
||||
|
||||
.vc-command-palette-keybind-input {
|
||||
display: flex;
|
||||
|
|
@ -1029,7 +965,7 @@
|
|||
}
|
||||
|
||||
.vc-command-palette-picker .vc-command-palette-main-input input {
|
||||
min-height: 40px !important;
|
||||
width: 100px !important;
|
||||
height: 40px !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
|
@ -1052,6 +988,11 @@
|
|||
outline: none !important;
|
||||
}
|
||||
|
||||
.vc-command-palette-main-search-input {
|
||||
width: 100%;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
input.vc-command-palette-main-search-input:read-only,
|
||||
.vc-command-palette-main-search-input input:read-only {
|
||||
color: var(--cp-text-muted) !important;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { closeModal, openModal } from "@utils/modal";
|
||||
import { closeModal, openModal } from "@webpack/common";
|
||||
|
||||
import { CommandPaletteModal } from "./ui/CommandPaletteModal";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,40 +4,33 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { EquicordIcon } from "@equicordplugins/discordDevBanner/components";
|
||||
import { IS_MAC } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-command-palette-");
|
||||
|
||||
interface CommandPaletteActionBarProps {
|
||||
selectedLabel?: string;
|
||||
onOpenActions?(): void;
|
||||
compact?: boolean;
|
||||
onExpand?(): void;
|
||||
}
|
||||
|
||||
export function CommandPaletteActionBar({ selectedLabel, onOpenActions, compact, onExpand }: CommandPaletteActionBarProps) {
|
||||
const actionsShortcutLabel = IS_MAC ? "⌘L" : "Ctrl+L";
|
||||
const EquicordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 930 930"
|
||||
width="32px"
|
||||
height="32px"
|
||||
>
|
||||
<path d="M836 465.5C836 670.121 670.121 836 465.5 836C260.879 836 95 670.121 95 465.5C95 260.879 260.879 95 465.5 95C670.121 95 836 260.879 836 465.5ZM242.322 465.5C242.322 588.758 342.242 688.678 465.5 688.678C588.758 688.678 688.678 588.758 688.678 465.5C688.678 342.242 588.758 242.322 465.5 242.322C342.242 242.322 242.322 342.242 242.322 465.5Z" />
|
||||
<path d="M584.219 465.898C584.219 531.245 531.245 584.219 465.898 584.219C440.35 584.219 416.693 576.122 397.353 562.354L260.937 644.321C451.4 528.542 329.698 492.311 204.538 566.663L348.433 480.202C347.868 475.513 347.577 470.74 347.577 465.898C347.577 400.552 400.552 347.577 465.898 347.577C491.108 347.577 514.477 355.462 533.673 368.899L627.819 312.331C610.898 294.399 591.056 279.324 569.045 267.796C534.72 249.819 496.306 241.088 457.582 242.462C418.858 243.837 381.16 255.27 348.196 275.637C315.232 296.003 288.138 324.6 269.581 358.616C262.856 370.943 257.336 383.828 253.065 397.091C240.595 435.815 209.386 470.244 168.712 470.997C128.037 471.751 93.7099 439.084 101.005 399.061C108.06 360.359 121.262 322.87 140.254 288.06C171.06 231.591 216.039 184.116 270.763 150.306C325.486 116.495 388.07 97.5155 452.356 95.2335C516.641 92.9515 580.413 107.446 637.397 137.29C694.38 167.134 742.612 211.301 777.345 265.444C812.079 319.586 832.118 381.839 835.491 446.076C838.863 510.313 825.452 574.322 796.579 631.804C778.78 667.239 755.483 699.439 727.687 727.279C698.944 756.068 652.543 746.455 629.998 712.591C607.453 678.727 617.982 633.466 642.711 601.164C651.181 590.1 658.628 578.224 664.932 565.676C682.324 531.051 690.402 492.494 688.371 453.799C687.303 433.462 683.462 413.454 677.02 394.312L583.246 450.657C583.889 455.647 584.219 460.734 584.219 465.898ZM260.937 644.321C258.599 645.742 256.214 647.175 253.783 648.619L260.937 644.321Z" />
|
||||
<path d="M470.711 406.73C493.342 393.132 522.712 400.455 536.311 423.086C549.909 445.718 542.587 475.088 519.955 488.687L253.783 648.619L204.538 566.663L470.711 406.73Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={cl("action-bar")}>
|
||||
<div className={classes(cl("action-bar-label"), cl("action-bar-logo"))}>
|
||||
<EquicordIcon />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={cl("action-bar-compact-btn")}
|
||||
onClick={onExpand}
|
||||
>
|
||||
<span className={cl("action-bar-actions-label")}>Show More</span>
|
||||
<span>↓</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function CommandPaletteActionBar({ selectedLabel, onOpenActions }: CommandPaletteActionBarProps) {
|
||||
const actionsShortcutLabel = IS_MAC ? "⌘L" : "Ctrl+L";
|
||||
|
||||
return (
|
||||
<div className={cl("action-bar")}>
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@
|
|||
|
||||
import "../style.css";
|
||||
|
||||
import { Heading } from "@components/Heading";
|
||||
import { IS_MAC } from "@utils/constants";
|
||||
import { classNameFactory, classNameToSelector } from "@utils/css";
|
||||
import { copyWithToast } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { classes } from "@utils/misc";
|
||||
import { type ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { TextInput, Toasts, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, TextInput, Toasts, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
|
||||
import { buildQueryResolution } from "../actions/executors";
|
||||
|
|
@ -282,20 +283,18 @@ function createInitialPageState(ref: PalettePageRef): PalettePageValuesState {
|
|||
};
|
||||
}
|
||||
|
||||
export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: ModalProps; instanceKey: number; }) {
|
||||
export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: RenderModalProps; instanceKey: number; }) {
|
||||
const {
|
||||
compactStartEnabled = true,
|
||||
closeAfterExecute = true
|
||||
} = settings.use();
|
||||
const [query, setQuery] = useState("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||
const [keyboardSelectedKey, setKeyboardSelectedKey] = useState<string | null>(null);
|
||||
const [registryVersion, setRegistryVersion] = useState(() => getRegistryVersion());
|
||||
const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set());
|
||||
const [navigationLevel, setNavigationLevel] = useState<NavigationLevel>(() => {
|
||||
if (compactStartEnabled || !persistedCategoryId) return { type: "root" };
|
||||
if (!persistedCategoryId) return { type: "root" };
|
||||
return buildNavigationLevelForCategory(persistedCategoryId);
|
||||
});
|
||||
const [activePromptCommand, setActivePromptCommand] = useState<CommandEntry | null>(null);
|
||||
|
|
@ -326,7 +325,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
|
||||
useEffect(() => {
|
||||
setQuery("");
|
||||
setExpanded(false);
|
||||
setSelectedIndex(-1);
|
||||
setSelectedKey(null);
|
||||
setKeyboardSelectedKey(null);
|
||||
|
|
@ -343,38 +341,22 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
closeReasonRef.current = null;
|
||||
keyboardNavigationAtRef.current = 0;
|
||||
setNavigationLevel(() => {
|
||||
if (compactStartEnabled || !persistedCategoryId) return { type: "root" };
|
||||
if (!persistedCategoryId) return { type: "root" };
|
||||
return buildNavigationLevelForCategory(persistedCategoryId);
|
||||
});
|
||||
}, [compactStartEnabled, instanceKey]);
|
||||
}, [instanceKey]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (closeReasonRef.current === "explicit-root") {
|
||||
clearPersistedNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeReasonRef.current === "programmatic") {
|
||||
clearPersistedNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigationLevel.type === "category") {
|
||||
persistedCategoryId = navigationLevel.categoryId;
|
||||
return;
|
||||
}
|
||||
|
||||
clearPersistedNavigation();
|
||||
};
|
||||
}, [navigationLevel]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsActionsMenuOpen(false);
|
||||
}, [navigationLevel]);
|
||||
|
||||
const trimmedQuery = query.trim();
|
||||
const compact = compactStartEnabled && trimmedQuery.length === 0 && !expanded && navigationLevel.type === "root" && pageStack.length === 0;
|
||||
|
||||
const allCommands = useMemo(() => listCommands(), [registryVersion]);
|
||||
|
||||
|
|
@ -472,7 +454,7 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
expandedItems.push({ type: "section", id: "section-breadcrumb", label: breadcrumb });
|
||||
}
|
||||
|
||||
if (!compact && navigationLevel.type === "root" && trimmedQuery.length === 0) {
|
||||
if (navigationLevel.type === "root" && trimmedQuery.length === 0) {
|
||||
if (pinnedCandidates.length > 0) {
|
||||
expandedItems.push({ type: "section", id: "section-pinned", label: "Pinned" });
|
||||
expandedItems.push(...pinnedCandidates);
|
||||
|
|
@ -507,7 +489,7 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
}
|
||||
|
||||
return expandedItems;
|
||||
}, [compact, navigationLevel, pinnedCandidates, rankedCommandCandidates, recentCandidates, suggestedCandidates, trimmedQuery]);
|
||||
}, [navigationLevel, pinnedCandidates, rankedCommandCandidates, recentCandidates, suggestedCandidates, trimmedQuery]);
|
||||
|
||||
const emptyStateText = navigationLevel.type === "category" && navigationLevel.categoryId === MENTIONS_CATEGORY_ID
|
||||
? "All caught up."
|
||||
|
|
@ -606,12 +588,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
}, [queryCandidates, selectedPromptCandidateId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (compact) {
|
||||
setSelectedIndex(-1);
|
||||
setSelectedKey(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasCommandItems) {
|
||||
setSelectedIndex(-1);
|
||||
setSelectedKey(null);
|
||||
|
|
@ -635,12 +611,13 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
|
||||
setSelectedIndex(firstSelectable);
|
||||
setSelectedKey(items[firstSelectable].id);
|
||||
}, [hasCommandItems, items, selectedKey, compact]);
|
||||
}, [hasCommandItems, items, selectedKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0) return;
|
||||
if (selectionSource !== "keyboard") return;
|
||||
const container = listRef.current;
|
||||
const rawContainer = listRef.current as any;
|
||||
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
|
||||
const node = itemRefs.current[selectedIndex];
|
||||
if (!container || !node) return;
|
||||
|
||||
|
|
@ -662,16 +639,18 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
if (previous === trimmedQuery) return;
|
||||
lastTrimmedQueryRef.current = trimmedQuery;
|
||||
|
||||
if (compact || isPageOpen) return;
|
||||
if (isPageOpen) return;
|
||||
|
||||
setSelectedIndex(-1);
|
||||
setSelectedKey(null);
|
||||
setKeyboardSelectedKey(null);
|
||||
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = 0;
|
||||
const rawContainer = listRef.current as any;
|
||||
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
|
||||
if (container) {
|
||||
container.scrollTop = 0;
|
||||
}
|
||||
}, [compact, isPageOpen, trimmedQuery]);
|
||||
}, [isPageOpen, trimmedQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusPromptInput) return;
|
||||
|
|
@ -768,7 +747,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
setSelectedKey(null);
|
||||
setKeyboardSelectedKey(null);
|
||||
setQuery("");
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
const setActivePageValue = (fieldKey: string, value: string) => {
|
||||
|
|
@ -883,7 +861,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
setSelectedKey(null);
|
||||
setKeyboardSelectedKey(null);
|
||||
setQuery("");
|
||||
setExpanded(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -982,7 +959,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
setSelectedKey(null);
|
||||
setKeyboardSelectedKey(null);
|
||||
setQuery("");
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
const updateSelection = (index: number, item: PaletteCandidate) => {
|
||||
|
|
@ -1055,7 +1031,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
clearPromptState();
|
||||
setSelectedKey(null);
|
||||
setQuery("");
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
const onKeyDown = async (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
|
|
@ -1256,17 +1231,12 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
|
||||
if (isMainInputTarget && event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
if (compact) {
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
moveSelection(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMainInputTarget && event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
if (compact) return;
|
||||
if (selectedIndex <= 0) {
|
||||
focusSearchInput();
|
||||
return;
|
||||
|
|
@ -1289,24 +1259,12 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
if (compact) {
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
moveSelection(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
if (compact) return;
|
||||
|
||||
if (selectedIndex <= 0 && trimmedQuery.length === 0 && navigationLevel.type === "root") {
|
||||
setExpanded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
moveSelection(-1);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1368,14 +1326,8 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
return;
|
||||
}
|
||||
|
||||
if (!compact && trimmedQuery.length > 0) {
|
||||
if (trimmedQuery.length > 0) {
|
||||
setQuery("");
|
||||
setExpanded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!compact) {
|
||||
setExpanded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1469,124 +1421,126 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
}, [activePage, activePageFieldKey, activePageSpec, activePageState, activePageSuggestions]);
|
||||
|
||||
return (
|
||||
<ModalRoot
|
||||
<Modal
|
||||
{...modalProps}
|
||||
className={cn("vc-command-palette", compact && cl("compact"))}
|
||||
size={compact ? ModalSize.SMALL : ModalSize.LARGE}
|
||||
>
|
||||
<div className={cl("shell")} onKeyDown={onKeyDown}>
|
||||
{!isPageOpen && (
|
||||
<CommandPaletteInput
|
||||
inputRef={mainInputRef}
|
||||
autoFocus={!activePromptCommand}
|
||||
value={query}
|
||||
onChange={value => {
|
||||
if (activePromptCommand && value.length > 0) {
|
||||
clearPromptState();
|
||||
}
|
||||
size="lg"
|
||||
scrollerRef={listRef}
|
||||
title={
|
||||
<div className={cn("vc-command-palette", cl("shell"))} onKeyDown={onKeyDown}>
|
||||
<Heading tag="h1" className={cl("title")}>Command Palette</Heading>
|
||||
|
||||
const next = value.trim();
|
||||
setQuery(value);
|
||||
|
||||
if (next.length > 0) {
|
||||
setExpanded(true);
|
||||
} else {
|
||||
if (navigationLevel.type !== "root") {
|
||||
goBack();
|
||||
return;
|
||||
{!isPageOpen && (
|
||||
<CommandPaletteInput
|
||||
inputRef={mainInputRef}
|
||||
autoFocus={!activePromptCommand}
|
||||
value={query}
|
||||
onChange={value => {
|
||||
if (activePromptCommand && value.length > 0) {
|
||||
clearPromptState();
|
||||
}
|
||||
setExpanded(false);
|
||||
}
|
||||
}}
|
||||
placeholder={mainInputPlaceholder}
|
||||
readOnly={false}
|
||||
>
|
||||
{shouldRenderPromptPreview && promptCommand && (
|
||||
<div
|
||||
ref={promptContainerRef}
|
||||
className={classes(cl("query-preview"), !shouldShowPromptCommandLabel && cl("query-preview-without-label"))}
|
||||
>
|
||||
{shouldShowPromptCommandLabel && (
|
||||
<span className={cl("query-preview-command-label")}>{promptCommand.label}</span>
|
||||
)}
|
||||
<div className={cl("query-preview-prompt")} style={queryPreviewPromptStyle}>
|
||||
{activePromptCommand ? (
|
||||
<div
|
||||
className={classes(cl("query-prompt-capsule"), cl("query-prompt-capsule-active"))}
|
||||
style={promptChipStyle}
|
||||
onMouseDown={() => setShowPromptDropdown(true)}
|
||||
>
|
||||
<TextInput
|
||||
ref={promptInputRef}
|
||||
className={cl("prompt-active-input")}
|
||||
value={promptInputValue}
|
||||
onChange={value => {
|
||||
setPromptInputValue(value);
|
||||
setShowPromptDropdown(true);
|
||||
if (selectedPromptCandidateId) {
|
||||
setSelectedPromptCandidateId(null);
|
||||
}
|
||||
|
||||
const next = value.trim();
|
||||
setQuery(value);
|
||||
|
||||
if (next.length === 0) {
|
||||
if (navigationLevel.type !== "root") {
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={mainInputPlaceholder}
|
||||
readOnly={false}
|
||||
>
|
||||
{shouldRenderPromptPreview && promptCommand && (
|
||||
<div
|
||||
ref={promptContainerRef}
|
||||
className={classes(cl("query-preview"), !shouldShowPromptCommandLabel && cl("query-preview-without-label"))}
|
||||
>
|
||||
{shouldShowPromptCommandLabel && (
|
||||
<span className={cl("query-preview-command-label")}>{promptCommand.label}</span>
|
||||
)}
|
||||
<div className={cl("query-preview-prompt")} style={queryPreviewPromptStyle}>
|
||||
{activePromptCommand ? (
|
||||
<div
|
||||
className={classes(cl("query-prompt-capsule"), cl("query-prompt-capsule-active"))}
|
||||
style={promptChipStyle}
|
||||
onMouseDown={() => setShowPromptDropdown(true)}
|
||||
>
|
||||
<TextInput
|
||||
ref={promptInputRef}
|
||||
className={cl("prompt-active-input")}
|
||||
value={promptInputValue}
|
||||
onChange={value => {
|
||||
setPromptInputValue(value);
|
||||
setShowPromptDropdown(true);
|
||||
if (selectedPromptCandidateId) {
|
||||
setSelectedPromptCandidateId(null);
|
||||
}
|
||||
}}
|
||||
placeholder={selectedPromptCandidate ? "" : (promptCommand.queryPlaceholder ?? "Action")}
|
||||
onFocus={() => setShowPromptDropdown(true)}
|
||||
onBlur={() => {
|
||||
window.setTimeout(() => {
|
||||
const active = document.activeElement;
|
||||
if (promptContainerRef.current?.contains(active)) return;
|
||||
setShowPromptDropdown(false);
|
||||
if (selectedPromptCandidateId || promptInputValue.trim().length > 0) return;
|
||||
clearPromptState();
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={cl("query-prompt-capsule")}
|
||||
style={promptChipStyle}
|
||||
onMouseDown={event => event.preventDefault()}
|
||||
onClick={() => activatePromptCommand(promptCommand, true)}
|
||||
>
|
||||
<span className={cl("query-prompt-capsule-label")}>{promptCommand.queryPlaceholder ?? "Action"}</span>
|
||||
</button>
|
||||
)}
|
||||
{shouldShowPromptDropdown && (
|
||||
<PaletteDropdown
|
||||
className={cl("query-preview-dropdown")}
|
||||
suggestions={promptDropdownSuggestions}
|
||||
highlightedIndex={promptSelectedSuggestionIndex}
|
||||
onHover={index => {
|
||||
const candidate = promptDropdownSuggestions[index];
|
||||
if (!candidate) return;
|
||||
setSelectedPromptCandidateId(candidate.id);
|
||||
}}
|
||||
placeholder={selectedPromptCandidate ? "" : (promptCommand.queryPlaceholder ?? "Action")}
|
||||
onFocus={() => setShowPromptDropdown(true)}
|
||||
onBlur={() => {
|
||||
window.setTimeout(() => {
|
||||
const active = document.activeElement;
|
||||
if (promptContainerRef.current?.contains(active)) return;
|
||||
onPick={candidate => {
|
||||
setSelectedPromptCandidateId(candidate.id);
|
||||
if (activePromptIsSingleSelect) {
|
||||
setPromptInputValue(candidate.label);
|
||||
} else {
|
||||
setPromptInputValue("");
|
||||
}
|
||||
if (activePromptIsSingleSelect) {
|
||||
setShowPromptDropdown(false);
|
||||
if (selectedPromptCandidateId || promptInputValue.trim().length > 0) return;
|
||||
clearPromptState();
|
||||
}, 0);
|
||||
}
|
||||
setFocusPromptInput(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={cl("query-prompt-capsule")}
|
||||
style={promptChipStyle}
|
||||
onMouseDown={event => event.preventDefault()}
|
||||
onClick={() => activatePromptCommand(promptCommand, true)}
|
||||
>
|
||||
<span className={cl("query-prompt-capsule-label")}>{promptCommand.queryPlaceholder ?? "Action"}</span>
|
||||
</button>
|
||||
)}
|
||||
{shouldShowPromptDropdown && (
|
||||
<PaletteDropdown
|
||||
className={cl("query-preview-dropdown")}
|
||||
suggestions={promptDropdownSuggestions}
|
||||
highlightedIndex={promptSelectedSuggestionIndex}
|
||||
onHover={index => {
|
||||
const candidate = promptDropdownSuggestions[index];
|
||||
if (!candidate) return;
|
||||
setSelectedPromptCandidateId(candidate.id);
|
||||
}}
|
||||
onPick={candidate => {
|
||||
setSelectedPromptCandidateId(candidate.id);
|
||||
if (activePromptIsSingleSelect) {
|
||||
setPromptInputValue(candidate.label);
|
||||
} else {
|
||||
setPromptInputValue("");
|
||||
}
|
||||
if (activePromptIsSingleSelect) {
|
||||
setShowPromptDropdown(false);
|
||||
}
|
||||
setFocusPromptInput(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CommandPaletteInput>
|
||||
)}
|
||||
)}
|
||||
</CommandPaletteInput>
|
||||
)}
|
||||
|
||||
{!compact && !isPageOpen && calculatorResult && (
|
||||
<CommandPaletteCalculatorCards result={calculatorResult} mode={calculatorViewMode} />
|
||||
)}
|
||||
|
||||
{!compact && !isPageOpen && (
|
||||
<div ref={listRef} className={cl("list")}>
|
||||
{!isPageOpen && calculatorResult && (
|
||||
<CommandPaletteCalculatorCards result={calculatorResult} mode={calculatorViewMode} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={cn("vc-command-palette", cl("shell"))} onKeyDown={onKeyDown}>
|
||||
{!isPageOpen && (
|
||||
<div className={cl("list")}>
|
||||
{!hasCommandItems && <div className={cl("empty")}>{emptyStateText}</div>}
|
||||
{items.map((item, index) => {
|
||||
if (item.type === "section") {
|
||||
|
|
@ -1626,9 +1580,9 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!compact && isPageOpen && activePageView}
|
||||
{isPageOpen && activePageView}
|
||||
|
||||
{!compact && (isActionsMenuOpen || isActionsMenuClosing) && (
|
||||
{(isActionsMenuOpen || isActionsMenuClosing) && (
|
||||
<CommandPaletteActionsMenu
|
||||
actions={paletteActions}
|
||||
title={activePageSpec?.title ?? selectedLabel ?? "Actions"}
|
||||
|
|
@ -1651,11 +1605,9 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: M
|
|||
setIsActionsMenuOpen(true);
|
||||
setIsActionsMenuClosing(false);
|
||||
}}
|
||||
compact={compact}
|
||||
onExpand={() => setExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { closeModal, ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
||||
import { Button, Menu, Slider, TextInput, useState } from "@webpack/common";
|
||||
import { Button, closeModal, Menu, Modal,openModalLazy, Slider, TextInput, useState } from "@webpack/common";
|
||||
|
||||
import { folderIconsData, settings } from "./settings";
|
||||
import { folderProp, int2rgba, setFolderData } from "./util";
|
||||
|
|
@ -95,17 +94,12 @@ export function makeContextItem(a: folderProp) {
|
|||
action={() => {
|
||||
openModalLazy(async () => {
|
||||
return props => (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader >
|
||||
<div style={{
|
||||
color: "white"
|
||||
}}>
|
||||
Set a New Icon.
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<ImageModal folderId={a.folderId} folderColor={a.folderColor} />
|
||||
</ModalContent>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title="Set a New Icon."
|
||||
>
|
||||
<ImageModal folderId={a.folderId} folderColor={a.folderColor} />
|
||||
<div style={{
|
||||
color: "white",
|
||||
margin: "2.5%",
|
||||
|
|
@ -113,7 +107,7 @@ export function makeContextItem(a: folderProp) {
|
|||
}}>
|
||||
You might have to hover the folder after setting in order for it to refresh.
|
||||
</div>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
*/
|
||||
|
||||
import { set } from "@api/DataStore";
|
||||
import { HeadingPrimary, HeadingSecondary } from "@components/Heading";
|
||||
import { HeadingSecondary } from "@components/Heading";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { Button, ColorPicker, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { ColorPicker, Modal, React, useState } from "@webpack/common";
|
||||
|
||||
import { colors, DATASTORE_KEY } from "./index";
|
||||
|
||||
const cl = classNameFactory("vc-customColors-");
|
||||
|
||||
export function SetColorModal({ id, modalProps }: { id: string, modalProps: ModalProps; }) {
|
||||
export function SetColorModal({ id, modalProps }: { id: string, modalProps: RenderModalProps; }) {
|
||||
const initialColor = parseInt(colors[id], 16) || 372735;
|
||||
// color picker default to current color set for user (if null it's 0x05afff :3 )
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export function SetColorModal({ id, modalProps }: { id: string, modalProps: Moda
|
|||
setColorPickerColor(color);
|
||||
}
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
function handleKey(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter")
|
||||
saveUserColor();
|
||||
}
|
||||
|
|
@ -44,14 +44,24 @@ export function SetColorModal({ id, modalProps }: { id: string, modalProps: Moda
|
|||
}
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader className={cl("modal-header")}>
|
||||
<HeadingPrimary>
|
||||
Custom Color
|
||||
</HeadingPrimary>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")} onKeyDown={handleKey}>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="sm"
|
||||
title="Custom Color"
|
||||
actions={[
|
||||
{
|
||||
text: "Save",
|
||||
variant: "primary",
|
||||
onClick: saveUserColor
|
||||
},
|
||||
{
|
||||
text: "Delete Entry",
|
||||
variant: "dangerPrimary",
|
||||
onClick: deleteUserColor
|
||||
}
|
||||
]}
|
||||
>
|
||||
<div onKeyDown={handleKey} className={cl("modal-content")}>
|
||||
<section className={Margins.bottom16}>
|
||||
<HeadingSecondary>
|
||||
Pick a Color
|
||||
|
|
@ -62,22 +72,7 @@ export function SetColorModal({ id, modalProps }: { id: string, modalProps: Moda
|
|||
showEyeDropper={false}
|
||||
/>
|
||||
</section>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={deleteUserColor}
|
||||
>
|
||||
Delete Entry
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.BRAND}
|
||||
onClick={saveUserColor}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
|||
import { get } from "@api/DataStore";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Channel, User } from "@vencord/discord-types";
|
||||
import { extractAndLoadChunksLazy } from "@webpack";
|
||||
import { ChannelStore, Menu, SelectedChannelStore } from "@webpack/common";
|
||||
import { ChannelStore, Menu, openModal,SelectedChannelStore } from "@webpack/common";
|
||||
|
||||
import { SetColorModal } from "./SetColorModal";
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import "./style.css";
|
||||
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings, migrateOldSettingToNewPlugin, migratePluginSetting, migratePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, migratePluginSetting } from "@api/Settings";
|
||||
import { Divider } from "@components/Divider";
|
||||
import { HeadingSecondary } from "@components/Heading";
|
||||
import { Notice } from "@components/Notice";
|
||||
|
|
@ -18,21 +18,8 @@ import { t, Translate } from "@utils/translation";
|
|||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Alerts } from "@webpack/common";
|
||||
|
||||
migratePluginSettings("Declutter", "BetterUserArea", "Anammox");
|
||||
migrateOldSettingToNewPlugin("Declutter", "removeClanTag", "GuildTagSettings", "hideTags");
|
||||
|
||||
const migrationsAnammox = [
|
||||
["dms", "removeShopAboveDM"],
|
||||
["quests", "removeQuestsAboveDM"],
|
||||
["serverBoost", "removeServerBoostInfo"],
|
||||
["billing", "removeBillingSettings"],
|
||||
["gift", "removeGiftButton"],
|
||||
["emojiList", "removeUnavailableEmojiPicker"],
|
||||
];
|
||||
|
||||
for (const [oldKey, newKey] of migrationsAnammox) {
|
||||
migratePluginSetting("Declutter", newKey, oldKey);
|
||||
}
|
||||
migratePluginSetting("Declutter", "removeShopAboveDms", "removeShopAboveDM");
|
||||
migratePluginSetting("Declutter", "removeQuestsAboveDms", "removeQuestsAboveDM");
|
||||
|
||||
const cl = classNameFactory("vc-declutter-");
|
||||
|
||||
|
|
@ -86,13 +73,13 @@ export const settings = definePluginSettings({
|
|||
type: OptionType.COMPONENT,
|
||||
component: () => SectionSeparator(t("equicord.declutter.headers.friendsList")),
|
||||
},
|
||||
removeShopAboveDM: {
|
||||
removeShopAboveDms: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: t("equicord.declutter.settings.removeShopAboveDM"),
|
||||
default: false,
|
||||
restartNeeded: true,
|
||||
},
|
||||
removeQuestsAboveDM: {
|
||||
removeQuestsAboveDms: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: t("equicord.declutter.settings.removeQuestsAboveDM"),
|
||||
default: false,
|
||||
|
|
@ -179,6 +166,15 @@ export default definePlugin({
|
|||
},
|
||||
predicate: () => settings.store.removeAvatarDecoration && !isPluginEnabled(decor.name),
|
||||
},
|
||||
{
|
||||
// Avatar decoration on dms list
|
||||
find: "showCommunicationDisabledStyles",
|
||||
replacement: {
|
||||
match: /null==\i\|\|\i\?null:\(0,\i\.jsxs?\)\("img",\{className:\i\.\i,src:\i,alt:" ","aria-hidden":!0\}\)/,
|
||||
replace: "null"
|
||||
},
|
||||
predicate: () => settings.store.removeAvatarDecoration && !isPluginEnabled(decor.name),
|
||||
},
|
||||
{
|
||||
// Nameplate
|
||||
find: ".MINI_PREVIEW,[",
|
||||
|
|
@ -279,18 +275,18 @@ export default definePlugin({
|
|||
{
|
||||
match: /"nitro-tab-group"\)/,
|
||||
replace: "$&&&undefined",
|
||||
predicate: () => settings.store.removeShopAboveDM,
|
||||
predicate: () => settings.store.removeShopAboveDms,
|
||||
},
|
||||
{
|
||||
match: /"discord-shop"\)/,
|
||||
match: /NAVIGATION_LINK\}\}\},"discord-shop"\)/,
|
||||
replace: "$&&&undefined",
|
||||
predicate: () => settings.store.removeShopAboveDM
|
||||
predicate: () => settings.store.removeShopAboveDms
|
||||
|
||||
},
|
||||
{
|
||||
match: /"quests"\)/,
|
||||
match: /\.QUEST_HOME\},"quests"\)/,
|
||||
replace: "$&&&undefined",
|
||||
predicate: () => settings.store.removeQuestsAboveDM
|
||||
predicate: () => settings.store.removeQuestsAboveDms
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -307,7 +303,7 @@ export default definePlugin({
|
|||
replace: "/*$&*/",
|
||||
},
|
||||
],
|
||||
predicate: () => settings.store.removeShopAboveDM,
|
||||
predicate: () => settings.store.removeShopAboveDms,
|
||||
},
|
||||
{
|
||||
// Channel list server boost progress bar
|
||||
|
|
|
|||
101
src/equicordplugins/downloadAllAttachments/index.tsx
Normal file
101
src/equicordplugins/downloadAllAttachments/index.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { CloudDownloadIcon } from "@components/Icons";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { pluralise } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Message, MessageAttachment } from "@vencord/discord-types";
|
||||
import { ChannelStore, showToast, Toasts } from "@webpack/common";
|
||||
|
||||
const logger = new Logger("DownloadAllAttachments");
|
||||
|
||||
async function downloadAll(attachments: MessageAttachment[]) {
|
||||
let dir: FileSystemDirectoryHandle;
|
||||
try {
|
||||
dir = await window.showDirectoryPicker({ mode: "readwrite" });
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === "AbortError") return;
|
||||
logger.error("Failed to open directory picker:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const usedNames = new Map<string, number>();
|
||||
|
||||
function uniqueName(original: string): string {
|
||||
const count = usedNames.get(original) ?? 0;
|
||||
usedNames.set(original, count + 1);
|
||||
if (count === 0) return original;
|
||||
const dot = original.lastIndexOf(".");
|
||||
return dot === -1
|
||||
? `${original}_${count}`
|
||||
: `${original.slice(0, dot)}_${count}${original.slice(dot)}`;
|
||||
}
|
||||
|
||||
const tasks = attachments.map(a => ({ attachment: a, filename: uniqueName(a.filename) }));
|
||||
|
||||
const results = await Promise.allSettled(tasks.map(async ({ attachment, filename }) => {
|
||||
if (!attachment.proxy_url) throw new Error("Missing Proxy URL");
|
||||
|
||||
const res = await fetch(attachment.proxy_url);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (!res.body) throw new Error("Response body is empty");
|
||||
|
||||
let fileHandle: FileSystemFileHandle | undefined;
|
||||
try {
|
||||
fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
try {
|
||||
await res.body.pipeTo(writable);
|
||||
} catch (e) {
|
||||
await writable.abort();
|
||||
throw e;
|
||||
}
|
||||
} catch (e) {
|
||||
if (fileHandle) {
|
||||
try { await dir.removeEntry(filename); } catch { }
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
|
||||
const failed = results.filter(r => {
|
||||
if (r.status === "rejected") {
|
||||
logger.warn("Failed to download attachment:", r.reason);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).length;
|
||||
|
||||
const succeeded = attachments.length - failed;
|
||||
|
||||
if (failed === 0)
|
||||
showToast(`Downloaded ${pluralise(succeeded, "attachment")}.`, Toasts.Type.SUCCESS);
|
||||
else
|
||||
showToast(`Downloaded ${succeeded} of ${attachments.length} attachments. ${failed} failed.`, Toasts.Type.FAILURE);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "DownloadAllAttachments",
|
||||
description: "Adds a popover button to download all attachments in a message at once.",
|
||||
tags: ["Utility", "Chat"],
|
||||
authors: [EquicordDevs.dhopcs],
|
||||
dependencies: ["MessagePopoverAPI"],
|
||||
messagePopoverButton: {
|
||||
icon: CloudDownloadIcon,
|
||||
render(message: Message) {
|
||||
if (!message.attachments.length) return null;
|
||||
return {
|
||||
label: "Download All Attachments",
|
||||
icon: CloudDownloadIcon,
|
||||
message,
|
||||
channel: ChannelStore.getChannel(message.channel_id),
|
||||
onClick: () => downloadAll(message.attachments)
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -28,7 +28,7 @@ const logger = new Logger("FileUpload", "#7cb7ff");
|
|||
function toProxyUrl(url: string): string {
|
||||
const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl);
|
||||
|
||||
if (Native || url.startsWith(`${corsProxyUrl}?url=`)) {
|
||||
if (url.startsWith(`${corsProxyUrl}?url=`)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import "./styles.css";
|
|||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Paginator, requirePaginator } from "@plugins/reviewDB/components/ReviewModal";
|
||||
import { ModalCloseButton } from "@utils/modal";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
|
||||
import { React, useRef, useState } from "@webpack/common";
|
||||
|
||||
const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
|
||||
import { MutableRefObject } from "react";
|
||||
|
||||
import { jumper } from "./index";
|
||||
|
|
@ -53,7 +54,7 @@ export default function ReplyNavigator({ replies }: { replies: Message[]; }) {
|
|||
totalCount={replies.length}
|
||||
onPageChange={processPageChange}
|
||||
/>
|
||||
<ModalCloseButton className={"vc-findreply-close"} onClick={() => setVisible(false)} />
|
||||
<CloseButton className={"vc-findreply-close"} onClick={() => setVisible(false)} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ async function searchGoogleFonts(query: string) {
|
|||
}
|
||||
|
||||
const preloadFont = (family: string) =>
|
||||
loadFontStyle(createGoogleFontUrl(family, "&text=The quick brown fox jumps over the lazy dog"));
|
||||
loadFontStyle(createGoogleFontUrl(family, ":wght@400;700"));
|
||||
|
||||
let styleElement: HTMLStyleElement | null = null;
|
||||
|
||||
|
|
@ -136,14 +136,13 @@ function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) =
|
|||
return (
|
||||
<section>
|
||||
<HeadingSecondary>{t("equicord.fontLoader.ui.searchGoogleFonts")}</HeadingSecondary>
|
||||
<Paragraph>{t("equicord.fontLoader.ui.clickToApply")}</Paragraph>
|
||||
<Paragraph className={Margins.bottom8}>{t("equicord.fontLoader.ui.clickToApply")}</Paragraph>
|
||||
|
||||
<TextInput
|
||||
value={query}
|
||||
onChange={e => handleSearch(e)}
|
||||
placeholder={t("equicord.fontLoader.ui.searchFonts")}
|
||||
disabled={loading}
|
||||
className={Margins.bottom16}
|
||||
/>
|
||||
|
||||
{results.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import "./styles.css";
|
|||
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { HeadingTertiary } from "@components/Heading";
|
||||
import { Heading, HeadingTertiary } from "@components/Heading";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { t } from "@utils/translation";
|
||||
import { findByPropsLazy, findCssClassesLazy } from "@webpack";
|
||||
import { Button, Forms, Parser, useEffect, useState } from "@webpack/common";
|
||||
import { Button, Parser, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { FriendInvite } from "./types";
|
||||
|
||||
|
|
@ -78,12 +78,12 @@ export default function FriendCodesPanel() {
|
|||
return (
|
||||
<>
|
||||
<header className={FormStyles.header}>
|
||||
<Forms.FormTitle
|
||||
<Heading
|
||||
tag="h2"
|
||||
className={FormStyles.title}
|
||||
>
|
||||
Your Friend Codes
|
||||
</Forms.FormTitle>
|
||||
</Heading>
|
||||
|
||||
<Flex
|
||||
style={{ marginBottom: "16px" }}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import { Flex } from "@components/Flex";
|
|||
import { Paragraph } from "@components/Paragraph";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, RelationshipStore } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Forms, Modal,openModal, RelationshipStore } from "@webpack/common";
|
||||
|
||||
interface rankInfo {
|
||||
title: string;
|
||||
|
|
@ -85,10 +85,12 @@ const ranks: rankInfo[] =
|
|||
];
|
||||
|
||||
function openRankModal(rank: rankInfo) {
|
||||
openModal(props => (
|
||||
openModal((props: RenderModalProps) => (
|
||||
<ErrorBoundary>
|
||||
<ModalRoot {...props} size={ModalSize.DYNAMIC}>
|
||||
<ModalHeader>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title={
|
||||
<Flex className={cl("flex")}>
|
||||
<Forms.FormTitle
|
||||
className={cl("img")}
|
||||
|
|
@ -98,15 +100,14 @@ function openRankModal(rank: rankInfo) {
|
|||
{rank.title}
|
||||
</Forms.FormTitle>
|
||||
</Flex>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div className={cl("text")}>
|
||||
<Paragraph>
|
||||
{rank.description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
}
|
||||
>
|
||||
<div className={cl("text")}>
|
||||
<Paragraph>
|
||||
{rank.description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Modal>
|
||||
</ErrorBoundary >
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { Channel } from "@vencord/discord-types";
|
||||
import { Channel, RenderModalProps } from "@vencord/discord-types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Avatar, Button, ChannelStore, MessageStore, React, Text, UserStore } from "@webpack/common";
|
||||
import { Avatar, Button, ChannelStore, MessageStore, Modal,React, Text, UserStore } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-boo-");
|
||||
|
||||
|
|
@ -37,9 +36,8 @@ function GroupDmsIcon({ channel }: { channel: Channel; }) {
|
|||
}
|
||||
|
||||
interface GhostedUsersModalProps {
|
||||
modalProps: any;
|
||||
modalProps: RenderModalProps;
|
||||
ghostedChannels: string[];
|
||||
onClose: () => void;
|
||||
onClearGhost: (channelId: string) => void;
|
||||
}
|
||||
|
||||
|
|
@ -57,14 +55,14 @@ export function getChannelDisplayName(channelId: string): string {
|
|||
return user?.username || "Unknown User";
|
||||
}
|
||||
|
||||
export function GhostedUsersModal({ modalProps, ghostedChannels: initialChannels, onClose, onClearGhost }: GhostedUsersModalProps) {
|
||||
export function GhostedUsersModal({ modalProps, ghostedChannels: initialChannels, onClearGhost }: GhostedUsersModalProps) {
|
||||
const [ghostedChannels, setGhostedChannels] = React.useState(initialChannels);
|
||||
|
||||
const handleChannelClick = (channelId: string) => {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel) {
|
||||
SelectedChannelActionCreators.selectPrivateChannel(channelId);
|
||||
onClose();
|
||||
modalProps.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -83,77 +81,67 @@ export function GhostedUsersModal({ modalProps, ghostedChannels: initialChannels
|
|||
};
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader>
|
||||
<Text
|
||||
variant="heading-lg/semibold"
|
||||
className={cl("modal-header")}
|
||||
>
|
||||
Ghosted Users ({ghostedChannels.length})
|
||||
</Text>
|
||||
{ghostedChannels.length > 0 && (
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.PRIMARY}
|
||||
onClick={handleClearAll}
|
||||
className={cl("clear")}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div className={cl("modal-content")}>
|
||||
{ghostedChannels.length === 0 ? (
|
||||
<Text variant="text-md/normal">No ghosts here!</Text>
|
||||
) : (
|
||||
ghostedChannels.map(channelId => {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (!channel) return null;
|
||||
<Modal
|
||||
{...modalProps}
|
||||
size="md"
|
||||
title={`Ghosted Users (${ghostedChannels.length})`}
|
||||
actions={ghostedChannels.length > 0 ? [
|
||||
{
|
||||
text: "Clear All",
|
||||
variant: "primary",
|
||||
onClick: handleClearAll
|
||||
}
|
||||
] : []}
|
||||
>
|
||||
<div className={cl("modal-content")}>
|
||||
{ghostedChannels.length === 0 ? (
|
||||
<Text variant="text-md/normal">No ghosts here!</Text>
|
||||
) : (
|
||||
ghostedChannels.map(channelId => {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (!channel) return null;
|
||||
|
||||
const lastMessage = MessageStore.getMessages(channelId)?.last();
|
||||
const lastMessageDate = lastMessage?.timestamp ? formatMessageDate(lastMessage.timestamp) : "";
|
||||
const lastMessage = MessageStore.getMessages(channelId)?.last();
|
||||
const lastMessageDate = lastMessage?.timestamp ? formatMessageDate(lastMessage.timestamp) : "";
|
||||
|
||||
const displayName = getChannelDisplayName(channel.id);
|
||||
const userId = channel?.recipients?.[0] ?? "";
|
||||
return (
|
||||
<div
|
||||
key={channelId}
|
||||
onClick={() => handleChannelClick(channelId)}
|
||||
className={cl("ghosted-entry")}
|
||||
>
|
||||
{channel.isGroupDM() ?
|
||||
<GroupDmsIcon
|
||||
channel={channel}
|
||||
/> : <Avatar
|
||||
src={UserStore.getUser(userId)?.getAvatarURL(undefined, 128, true)}
|
||||
size="SIZE_40"
|
||||
aria-label={displayName}
|
||||
/>}
|
||||
<div className={cl("user-info")}>
|
||||
<Text variant="text-md/normal">
|
||||
{displayName}
|
||||
const displayName = getChannelDisplayName(channel.id);
|
||||
const userId = channel?.recipients?.[0] ?? "";
|
||||
return (
|
||||
<div
|
||||
key={channelId}
|
||||
onClick={() => handleChannelClick(channelId)}
|
||||
className={cl("ghosted-entry")}
|
||||
>
|
||||
{channel.isGroupDM() ?
|
||||
<GroupDmsIcon
|
||||
channel={channel}
|
||||
/> : <Avatar
|
||||
src={UserStore.getUser(userId)?.getAvatarURL(undefined, 128, true)}
|
||||
size="SIZE_40"
|
||||
aria-label={displayName}
|
||||
/>}
|
||||
<div className={cl("user-info")}>
|
||||
<Text variant="text-md/normal">
|
||||
{displayName}
|
||||
</Text>
|
||||
{lastMessageDate && (
|
||||
<Text variant="text-xs/normal" className={cl("modal-text")}>
|
||||
{lastMessageDate}
|
||||
</Text>
|
||||
{lastMessageDate && (
|
||||
<Text variant="text-xs/normal" className={cl("modal-text")}>
|
||||
{lastMessageDate}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.PRIMARY}
|
||||
onClick={e => handleClearClick(e, channelId)}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.PRIMARY}
|
||||
onClick={e => handleClearClick(e, channelId)}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { closeModal, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Channel } from "@vencord/discord-types";
|
||||
import { Menu, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
import { Menu, openModal,Tooltip, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { Boo, clearChannelFromGhost, getBooCount, getGhostedChannels, onBooCountChange } from "./Boo";
|
||||
import { getChannelDisplayName, GhostedUsersModal } from "./GhostedUsersModal";
|
||||
|
|
@ -86,12 +85,11 @@ function BooIndicator() {
|
|||
|
||||
const handleClick = () => {
|
||||
const ghostedChannels = getGhostedChannels();
|
||||
const modalKey = openModal(modalProps => (
|
||||
openModal(modalProps => (
|
||||
<ErrorBoundary>
|
||||
<GhostedUsersModal
|
||||
modalProps={modalProps}
|
||||
ghostedChannels={ghostedChannels}
|
||||
onClose={() => closeModal(modalKey)}
|
||||
onClearGhost={clearChannelFromGhost}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { Flex } from "@components/Flex";
|
|||
import { Heading } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { Button, FluxDispatcher, TextInput, useCallback, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Button, FluxDispatcher, Modal, openModal, TextInput, useCallback, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "../settings";
|
||||
import { Collection, Gif } from "../types";
|
||||
|
|
@ -49,124 +49,116 @@ export function openRenameCollectionModal(name: string) {
|
|||
openModal(props => <RenameCollectionModal props={props} name={name} />);
|
||||
}
|
||||
|
||||
function InfoModal({ props, title, rows }: { props: ModalProps; title: string; rows: { label: string; value: string; }[]; }) {
|
||||
function InfoModal({ props, title, rows }: { props: RenderModalProps; title: string; rows: { label: string; value: string; }[]; }) {
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.SMALL} className={cl("modal")}>
|
||||
<ModalHeader separator={false} className={cl("modal-header")}>
|
||||
<Paragraph className={cl("modal-title")}>{title}</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<section>
|
||||
{rows.map(row => (
|
||||
<Flex key={row.label} className={cl("info-row")}>
|
||||
<Heading className={cl("info-title")}>{row.label}</Heading>
|
||||
<Paragraph className={cl("info-text")}>{row.value}</Paragraph>
|
||||
</Flex>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title={title}
|
||||
actions={[
|
||||
{ text: "Close", variant: "secondary", onClick: props.onClose }
|
||||
]}
|
||||
>
|
||||
<section>
|
||||
{rows.map(row => (
|
||||
<Flex key={row.label} className={cl("info-row")}>
|
||||
<Heading className={cl("info-title")}>{row.label}</Heading>
|
||||
<Paragraph className={cl("info-text")}>{row.value}</Paragraph>
|
||||
</Flex>
|
||||
))}
|
||||
</section>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function MoveToCollectionModal({ props, gifId }: { props: RenderModalProps; gifId: string; }) {
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title="Move To Collection"
|
||||
actions={[
|
||||
{ text: "Close", variant: "secondary", onClick: props.onClose }
|
||||
]}
|
||||
>
|
||||
<Heading style={{ marginBottom: "10px" }}>Select a collection to move the item to</Heading>
|
||||
<div className={cl("buttons")}>
|
||||
{cache_collections
|
||||
.filter(col => col.name !== getItemCollectionNameFromId(gifId))
|
||||
.map(col => (
|
||||
<Button
|
||||
key={col.name}
|
||||
className={cl("button")}
|
||||
onClick={async () => {
|
||||
const fromCollection = getItemCollectionNameFromId(gifId);
|
||||
if (!fromCollection) return;
|
||||
await moveGifToCollection(gifId, fromCollection, col.name);
|
||||
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
|
||||
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: fromCollection });
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
{stripPrefix(col.name)}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button onClick={props.onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function MoveToCollectionModal({ props, gifId }: { props: ModalProps; gifId: string; }) {
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.SMALL} className={cl("modal")}>
|
||||
<ModalHeader separator={false} className={cl("modal-header")}>
|
||||
<Paragraph className={cl("modal-title")}>Move To Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<Heading>Select a collection to move the item to</Heading>
|
||||
<div className={cl("buttons")}>
|
||||
{cache_collections
|
||||
.filter(col => col.name !== getItemCollectionNameFromId(gifId))
|
||||
.map(col => (
|
||||
<Button
|
||||
key={col.name}
|
||||
className={cl("button")}
|
||||
onClick={async () => {
|
||||
const fromCollection = getItemCollectionNameFromId(gifId);
|
||||
if (!fromCollection) return;
|
||||
await moveGifToCollection(gifId, fromCollection, col.name);
|
||||
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
|
||||
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: fromCollection });
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
{stripPrefix(col.name)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button onClick={props.onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateCollectionModal({ props, gif }: { props: ModalProps; gif: Gif; }) {
|
||||
function CreateCollectionModal({ props, gif }: { props: RenderModalProps; gif: Gif; }) {
|
||||
const [name, setName] = useState("");
|
||||
const onSubmit = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const onSubmit = useCallback((e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
if (!name.length) return;
|
||||
createCollection(name, [gif]);
|
||||
props.onClose();
|
||||
}, [name, gif, props]);
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title="Create Collection"
|
||||
actions={[
|
||||
{ text: "Create", onClick: onSubmit, disabled: !name.length, variant: "primary" }
|
||||
]}
|
||||
>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Paragraph>Create Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Heading className={cl("rename-text")}>Collection Name</Heading>
|
||||
<TextInput onChange={setName} />
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button type="submit" color={Button.Colors.GREEN} disabled={!name.length} onClick={onSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<Heading className={cl("rename-text")}>Collection Name</Heading>
|
||||
<TextInput onChange={setName} />
|
||||
</form>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function RenameCollectionModal({ props, name }: { props: ModalProps; name: string; }) {
|
||||
function RenameCollectionModal({ props, name }: { props: RenderModalProps; name: string; }) {
|
||||
const prefix = settings.store.collectionPrefix;
|
||||
const strippedName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
||||
const [newName, setNewName] = useState(strippedName);
|
||||
const tooLong = newName.length >= 25;
|
||||
|
||||
const onSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const onSubmit = useCallback(async (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
if (!newName.length || tooLong) return;
|
||||
await renameCollection(name, newName);
|
||||
props.onClose();
|
||||
}, [newName, name, tooLong, props]);
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title="Rename Collection"
|
||||
actions={[
|
||||
{ text: "Rename", onClick: onSubmit, disabled: !newName.length || tooLong, variant: "primary" }
|
||||
]}
|
||||
>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Paragraph>Rename Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Paragraph className={cl("rename-text")}>New Collection Name</Paragraph>
|
||||
<TextInput value={newName} className={classes(cl("rename-input"), tooLong ? cl("input-warning") : "")} onChange={setNewName} />
|
||||
{tooLong && <Paragraph className={cl("warning-text")}>Name can't be longer than 24 characters</Paragraph>}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button type="submit" color={Button.Colors.GREEN} disabled={!newName.length || tooLong} onClick={onSubmit}>
|
||||
Rename
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<Paragraph className={cl("rename-text")}>New Collection Name</Paragraph>
|
||||
<TextInput value={newName} className={classes(cl("rename-input"), tooLong ? cl("input-warning") : "")} onChange={setNewName} />
|
||||
{tooLong && <Paragraph className={cl("warning-text")}>Name can't be longer than 24 characters</Paragraph>}
|
||||
</form>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ import { Span } from "@components/Span";
|
|||
import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi";
|
||||
import { GitHubRepo } from "@equicordplugins/githubRepos/types";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
import { Clickable, React, useEffect, UserProfileStore, useState } from "@webpack/common";
|
||||
import { Clickable, openModal,React, useEffect, UserProfileStore, useState } from "@webpack/common";
|
||||
|
||||
import { ReposModal } from "./ReposModal";
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@
|
|||
import { BaseText } from "@components/BaseText";
|
||||
import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "@equicordplugins/githubRepos/githubApi";
|
||||
import { GitHubRepo } from "@equicordplugins/githubRepos/types";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { React, useEffect, UserProfileStore, useState } from "@webpack/common";
|
||||
import { openModal, React, useEffect, UserProfileStore, useState } from "@webpack/common";
|
||||
|
||||
import { cl, settings } from "..";
|
||||
import { RepoCard } from "./RepoCard";
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { HeadingPrimary } from "@components/Heading";
|
||||
import { getLanguageColor } from "@equicordplugins/githubRepos/colors";
|
||||
import { GitHubRepo } from "@equicordplugins/githubRepos/types";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalRoot } from "@utils/modal";
|
||||
import { React } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, React } from "@webpack/common";
|
||||
|
||||
import { cl } from "..";
|
||||
import { Star } from "./Star";
|
||||
|
|
@ -20,7 +18,7 @@ interface ReposModalProps {
|
|||
rootProps: any;
|
||||
}
|
||||
|
||||
export function ReposModal({ repos, username, rootProps }: ReposModalProps) {
|
||||
export function ReposModal({ repos, username, rootProps }: ReposModalProps & { rootProps: RenderModalProps; }) {
|
||||
const renderTableHeader = () => (
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -63,44 +61,38 @@ export function ReposModal({ repos, username, rootProps }: ReposModalProps) {
|
|||
);
|
||||
|
||||
return (
|
||||
<ModalRoot className={cl("modal")} size="large" {...rootProps}>
|
||||
<ModalHeader>
|
||||
<HeadingPrimary className={cl("modal-title")}>
|
||||
{username}'s GitHub Repositories
|
||||
</HeadingPrimary>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<div className={cl("table-container")}>
|
||||
<table className={cl("table")}>
|
||||
<colgroup>
|
||||
<col className={cl("header-repo")} />
|
||||
<col className={cl("header-description")} />
|
||||
<col className={cl("header-language")} />
|
||||
<col className={cl("header-stars")} />
|
||||
</colgroup>
|
||||
{renderTableHeader()}
|
||||
<tbody>
|
||||
{repos.map(renderTableRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button
|
||||
className={cl("modal-footer-github")}
|
||||
variant="link"
|
||||
onClick={() => window.open(`https://github.com/${username}?tab=repositories`, "_blank")}
|
||||
>
|
||||
View on GitHub
|
||||
</Button>
|
||||
<Button
|
||||
className={cl("modal-footer-close")}
|
||||
variant="secondary"
|
||||
onClick={rootProps.onClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
<Modal
|
||||
{...rootProps}
|
||||
className={cl("modal")}
|
||||
size="lg"
|
||||
title={`${username}'s GitHub Repositories`}
|
||||
actions={[
|
||||
{
|
||||
text: "View on GitHub",
|
||||
variant: "link",
|
||||
onClick: () => window.open(`https://github.com/${username}?tab=repositories`, "_blank")
|
||||
},
|
||||
{
|
||||
text: "Close",
|
||||
variant: "secondary",
|
||||
onClick: rootProps.onClose
|
||||
}
|
||||
]}
|
||||
>
|
||||
<div className={cl("table-container")}>
|
||||
<table className={cl("table")}>
|
||||
<colgroup>
|
||||
<col className={cl("header-repo")} />
|
||||
<col className={cl("header-description")} />
|
||||
<col className={cl("header-language")} />
|
||||
<col className={cl("header-stars")} />
|
||||
</colgroup>
|
||||
{renderTableHeader()}
|
||||
<tbody>
|
||||
{repos.map(renderTableRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,13 +65,16 @@ export default definePlugin({
|
|||
shouldShowGitHub(userId: string) {
|
||||
return UserProfileStore.getUserProfile(userId)?.connectedAccounts?.some((c: any) => c.type === "github") ?? false;
|
||||
},
|
||||
renderProfileCollection: (props: { user: User; displayProfile?: any; }) => {
|
||||
return (
|
||||
<ProfilePopoutComponent
|
||||
{...props}
|
||||
id={props.user.id}
|
||||
/>
|
||||
);
|
||||
renderProfileCollection: {
|
||||
render: (props: { user: User; displayProfile?: any; }) => {
|
||||
return (
|
||||
<ProfilePopoutComponent
|
||||
{...props}
|
||||
id={props.user.id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
priority: 0,
|
||||
},
|
||||
renderProfileRepositoriesTab: ErrorBoundary.wrap((props: { user: User; displayProfile?: any; }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { HeadingTertiary } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { React, Tooltip } from "@webpack/common";
|
||||
import { RenderModalProps, User } from "@vencord/discord-types";
|
||||
import { Modal, openModal, React, Tooltip } from "@webpack/common";
|
||||
|
||||
import { cl, GlobalBadges, INVITE_LINK } from "./utils";
|
||||
|
||||
|
|
@ -37,20 +35,32 @@ export function openBadgeModal(user: User) {
|
|||
<BadgeModalComponent key={badge.key} tooltip={badge.tooltip} badge={badge.badge} />
|
||||
));
|
||||
|
||||
openModal(modalprops => (
|
||||
<ModalRoot {...modalprops}>
|
||||
<ErrorBoundary>
|
||||
<ModalHeader>
|
||||
openModal((modalprops: RenderModalProps) => (
|
||||
<Modal
|
||||
{...modalprops}
|
||||
size="sm"
|
||||
title={
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<img
|
||||
className={cl("modal-avatar")}
|
||||
src={user.getAvatarURL(undefined, 512, true)}
|
||||
alt=""
|
||||
/>
|
||||
<HeadingTertiary className={cl("modal-name")}>
|
||||
<HeadingTertiary className={cl("modal-name")} style={{ color: "white" }}>
|
||||
{user.username}
|
||||
</HeadingTertiary>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-description")}>
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
text: "Join GlobalBadges Server",
|
||||
variant: "link",
|
||||
onClick: () => openInviteModal(INVITE_LINK)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<div className={cl("modal-description")}>
|
||||
{badgeDataElements.length ? (
|
||||
<>
|
||||
<Paragraph className={cl("modal-text")}>
|
||||
|
|
@ -66,16 +76,8 @@ export function openBadgeModal(user: User) {
|
|||
No global badges.
|
||||
</Paragraph>
|
||||
)}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => openInviteModal(INVITE_LINK)}
|
||||
>
|
||||
Join GlobalBadges Server
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,11 @@ export default definePlugin({
|
|||
dependencies: ["ChatInputButtonAPI"],
|
||||
settings: settings,
|
||||
startAt: StartAt.Init,
|
||||
chatBarButtonWrapper: (buttons: ReactNode) => {
|
||||
return <ButtonsInnerComponent buttons={buttons} />;
|
||||
chatBarButtonWrapper: {
|
||||
wrapper: (buttons: ReactNode) => {
|
||||
return <ButtonsInnerComponent buttons={buttons} />;
|
||||
},
|
||||
priority: 0,
|
||||
},
|
||||
start() {
|
||||
hidechatbuttonsopen = settings.store.open;
|
||||
|
|
|
|||
|
|
@ -5,13 +5,33 @@
|
|||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { EyeIcon } from "@components/Icons";
|
||||
import pinDms from "@plugins/pinDms";
|
||||
import { isPinned } from "@plugins/pinDms/data";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common";
|
||||
import { Channel, Message } from "@vencord/discord-types";
|
||||
import { ChannelStore, Clickable, FluxDispatcher, Menu, Tooltip } from "@webpack/common";
|
||||
|
||||
interface UserContextProps {
|
||||
channel?: Channel;
|
||||
}
|
||||
|
||||
interface PrivateChannelsListInstance {
|
||||
forceUpdate(callback?: () => void): void;
|
||||
}
|
||||
|
||||
const hiddenDmIds = new Set<string>();
|
||||
let privateChannelsListInstance: PrivateChannelsListInstance | null = null;
|
||||
let showHiddenDms = false;
|
||||
|
||||
function notifyHiddenDmsUpdate() {
|
||||
privateChannelsListInstance?.forceUpdate();
|
||||
}
|
||||
|
||||
const hideMessage = (messageId: string, channelId: string) => {
|
||||
FluxDispatcher.dispatch({
|
||||
|
|
@ -22,6 +42,17 @@ const hideMessage = (messageId: string, channelId: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
function toggleDm(channelId: string) {
|
||||
if (hiddenDmIds.has(channelId)) {
|
||||
hiddenDmIds.delete(channelId);
|
||||
if (!hiddenDmIds.size) showHiddenDms = false;
|
||||
} else {
|
||||
hiddenDmIds.add(channelId);
|
||||
}
|
||||
|
||||
notifyHiddenDmsUpdate();
|
||||
}
|
||||
|
||||
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
|
||||
const group = findGroupChildrenByChildId("copy-text", children);
|
||||
if (!group) return;
|
||||
|
|
@ -36,6 +67,25 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { m
|
|||
));
|
||||
};
|
||||
|
||||
const userCtxPatch: NavContextMenuPatchCallback = (children, { channel }: UserContextProps) => {
|
||||
if (!channel?.isDM()) return;
|
||||
if (isPluginEnabled(pinDms.name) && isPinned(channel.id)) return;
|
||||
|
||||
const group = findGroupChildrenByChildId("close-dm", children);
|
||||
if (!group) return;
|
||||
|
||||
const hidden = hiddenDmIds.has(channel.id);
|
||||
|
||||
group.splice(group.findIndex(c => c?.props?.id === "close-dm"), 0, (
|
||||
<Menu.MenuItem
|
||||
id="vc-hidemessages-dm"
|
||||
label={hidden ? "Unhide DM" : "Hide DM"}
|
||||
icon={EyeIcon}
|
||||
action={() => toggleDm(channel.id)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const settings = definePluginSettings({
|
||||
hidePopoverButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
@ -50,10 +100,71 @@ export default definePlugin({
|
|||
dependencies: ["MessagePopoverAPI"],
|
||||
tags: ["Chat", "Utility"],
|
||||
authors: [EquicordDevs.yash],
|
||||
patches: [
|
||||
{
|
||||
find: '"dm-quick-launcher"===',
|
||||
replacement: [
|
||||
{
|
||||
match: /render\(\)\{/,
|
||||
replace: "$&this.props.privateChannelIds=$self.filterPrivateChannelIds(this.props.privateChannelIds,this);"
|
||||
},
|
||||
{
|
||||
match: /renderRow=\i=>\{/,
|
||||
replace: "$&this.props.privateChannelIds=$self.filterPrivateChannelIds(this.props.privateChannelIds,this);"
|
||||
},
|
||||
{
|
||||
match: /renderDM=\(\i,\i\)=>\{/,
|
||||
replace: "$&this.props.privateChannelIds=$self.filterPrivateChannelIds(this.props.privateChannelIds,this);"
|
||||
},
|
||||
{
|
||||
match: /#{intl::DIRECT_MESSAGES}\)\}\),/,
|
||||
replace: "$&$self.renderHiddenMessagesToggle(),"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
contextMenus: {
|
||||
"message": messageCtxPatch
|
||||
"message": messageCtxPatch,
|
||||
"user-context": userCtxPatch
|
||||
},
|
||||
settings,
|
||||
stop() {
|
||||
hiddenDmIds.clear();
|
||||
showHiddenDms = false;
|
||||
notifyHiddenDmsUpdate();
|
||||
privateChannelsListInstance = null;
|
||||
},
|
||||
filterPrivateChannelIds(privateChannelIds: string[], instance?: PrivateChannelsListInstance) {
|
||||
privateChannelsListInstance = instance ?? privateChannelsListInstance;
|
||||
return showHiddenDms ? privateChannelIds : privateChannelIds.filter(id => !hiddenDmIds.has(id));
|
||||
},
|
||||
renderHiddenMessagesToggle: ErrorBoundary.wrap(() => {
|
||||
const hasHiddenDms = hiddenDmIds.size > 0;
|
||||
const label = !hasHiddenDms ? "No Hidden DMs" : showHiddenDms ? "Hide Hidden DMs" : "Show Hidden DMs";
|
||||
|
||||
return (
|
||||
<Tooltip text={label}>
|
||||
{tooltipProps => (
|
||||
<Clickable
|
||||
{...tooltipProps}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={label}
|
||||
aria-disabled={!hasHiddenDms}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!hasHiddenDms) return;
|
||||
showHiddenDms = !showHiddenDms;
|
||||
notifyHiddenDmsUpdate();
|
||||
}}
|
||||
>
|
||||
<EyeIcon width={18} height={18} />
|
||||
</Clickable>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}, { noop: true }),
|
||||
messagePopoverButton: {
|
||||
icon: EyeIcon,
|
||||
render(message: Message) {
|
||||
|
|
|
|||
|
|
@ -10,40 +10,21 @@ import { HiddenServersStore } from "@equicordplugins/hideServers/HiddenServersSt
|
|||
import { classNameFactory } from "@utils/css";
|
||||
import { getGuildAcronym } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import {
|
||||
closeModal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalProps,
|
||||
ModalRoot,
|
||||
ModalSize,
|
||||
openModal,
|
||||
} from "@utils/modal";
|
||||
import { Guild } from "@vencord/discord-types";
|
||||
import { Guild, RenderModalProps } from "@vencord/discord-types";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
import { Button, IconUtils, useStateFromStores } from "@webpack/common";
|
||||
import { Button, closeModal, IconUtils, Modal,openModal, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { SortedGuildStore } from "..";
|
||||
|
||||
const cl = classNameFactory("vc-hideservers-");
|
||||
const IconClasses = findCssClassesLazy("icon", "acronym", "childWrapper");
|
||||
|
||||
function HiddenServersModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
|
||||
function HiddenServersModal({ modalProps, close }: { modalProps: RenderModalProps; close(): void; }) {
|
||||
const guilds = useStateFromStores([HiddenServersStore], () => HiddenServersStore.hiddenGuildsDetail());
|
||||
return (
|
||||
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>
|
||||
Hidden Servers
|
||||
</BaseText>
|
||||
<ModalCloseButton onClick={close} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<HiddenServersMenu guilds={guilds} />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
<Modal {...modalProps} size="lg" title="Hidden Servers">
|
||||
<HiddenServersMenu guilds={guilds} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Button } from "@components/Button";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { HeadingSecondary } from "@components/Heading";
|
||||
|
|
@ -13,18 +12,8 @@ import { Paragraph } from "@components/Paragraph";
|
|||
import { TooltipContainer } from "@components/TooltipContainer";
|
||||
import { copyWithToast, getIntlMessage } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import {
|
||||
CloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalProps,
|
||||
ModalRoot,
|
||||
ModalSize,
|
||||
openModal
|
||||
} from "@utils/modal";
|
||||
import { saveFile } from "@utils/web";
|
||||
import { Icon } from "@vencord/discord-types";
|
||||
import { Icon, RenderModalProps } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import {
|
||||
Clickable,
|
||||
|
|
@ -32,6 +21,8 @@ import {
|
|||
createRoot,
|
||||
FluxDispatcher,
|
||||
Menu,
|
||||
Modal,
|
||||
openModal,
|
||||
ReactDOM,
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
|
@ -226,7 +217,7 @@ function OtherContextMenu({ iconName, Icon, color }: { iconName: string; Icon: I
|
|||
);
|
||||
}
|
||||
|
||||
function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: string; Icon: Icon; } & ModalProps) {
|
||||
function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: string; Icon: Icon; } & RenderModalProps) {
|
||||
const [color, setColor] = useColorNavigation(209);
|
||||
const colorData = cssColors[color];
|
||||
const colorKeys = useMemo(() => getCssColorKeys(), []);
|
||||
|
|
@ -247,62 +238,59 @@ function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: str
|
|||
});
|
||||
}, [colorKeys.length, setColor]);
|
||||
|
||||
const openOtherMenu = (e: React.MouseEvent) => {
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
const openOtherMenu = (e?: React.MouseEvent) => {
|
||||
if (e) ContextMenuApi.openContextMenu(e, () => (
|
||||
<OtherContextMenu iconName={iconName} Icon={Icon} color={color} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader separator={false} className="vc-ic-modal-header">
|
||||
<div className="vc-ic-modal-header-content">
|
||||
<BaseText size="lg" weight="semibold" className="vc-ic-modal-title">{iconName}</BaseText>
|
||||
<Modal
|
||||
transitionState={transitionState}
|
||||
onClose={onClose}
|
||||
size="md"
|
||||
title={iconName}
|
||||
actions={[
|
||||
{
|
||||
text: "Actions",
|
||||
variant: "primary",
|
||||
onClick: openOtherMenu
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Flex className="vc-ic-modal-main">
|
||||
<div
|
||||
className="vc-ic-icon-preview"
|
||||
aria-label={colorData?.name}
|
||||
onContextMenu={openColorMenu}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
<Icon className="vc-ic-icon-large" color={colorData?.css} fill={fill} />
|
||||
</div>
|
||||
<div className="vc-ic-modal-header-trailing">
|
||||
<CloseButton onClick={onClose} />
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalContent className="vc-ic-modal-content">
|
||||
<Flex className="vc-ic-modal-main">
|
||||
<div
|
||||
className="vc-ic-icon-preview"
|
||||
aria-label={colorData?.name}
|
||||
onContextMenu={openColorMenu}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
<Icon className="vc-ic-icon-large" color={colorData?.css} fill={fill} />
|
||||
</div>
|
||||
<Flex flexDirection="column" className="vc-ic-icon-info">
|
||||
<Flex className="vc-ic-icon-sizes">
|
||||
{iconSizes.map(size => (
|
||||
<TooltipContainer text={size} key={size}>
|
||||
<Icon size={size} color={colorData?.css} fill={fill} />
|
||||
</TooltipContainer>
|
||||
))}
|
||||
</Flex>
|
||||
<TooltipContainer text="Right-click icon to change">
|
||||
<BaseText size="sm" color="text-muted" className="vc-ic-color-label">
|
||||
{colorData?.name}
|
||||
</BaseText>
|
||||
</TooltipContainer>
|
||||
<Flex flexDirection="column" className="vc-ic-icon-info">
|
||||
<Flex className="vc-ic-icon-sizes">
|
||||
{iconSizes.map(size => (
|
||||
<TooltipContainer text={size} key={size}>
|
||||
<Icon size={size} color={colorData?.css} fill={fill} />
|
||||
</TooltipContainer>
|
||||
))}
|
||||
</Flex>
|
||||
<TooltipContainer text="Right-click icon to change">
|
||||
<BaseText size="sm" color="text-muted" className="vc-ic-color-label">
|
||||
{colorData?.name}
|
||||
</BaseText>
|
||||
</TooltipContainer>
|
||||
</Flex>
|
||||
<div className="vc-ic-use-as">
|
||||
<BaseText size="md" weight="semibold">Usage</BaseText>
|
||||
<BaseText size="sm" color="text-muted">Click to copy</BaseText>
|
||||
</div>
|
||||
{/* for some reason i cant make this shit codeblock full width, FF 15 */}
|
||||
<Clickable className="vc-ic-codeblock-wrapper" onClick={() => copyWithToast(findCode, "Copied!")}>
|
||||
<CodeBlock content={findCode} lang="ts" />
|
||||
</Clickable>
|
||||
</ModalContent>
|
||||
<ModalFooter className="vc-ic-modal-footer">
|
||||
<Button variant="primary" onClick={openOtherMenu}>
|
||||
Actions
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</Flex>
|
||||
<div className="vc-ic-use-as">
|
||||
<BaseText size="md" weight="semibold">Usage</BaseText>
|
||||
<BaseText size="sm" color="text-muted">Click to copy</BaseText>
|
||||
</div>
|
||||
{/* for some reason i cant make this shit codeblock full width, FF 15 */}
|
||||
<Clickable className="vc-ic-codeblock-wrapper" onClick={() => copyWithToast(findCode, "Copied!")}>
|
||||
<CodeBlock content={findCode} lang="ts" />
|
||||
</Clickable>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default definePlugin({
|
|||
description: t("equicord.iconViewer.description"),
|
||||
tags: ["Developers"],
|
||||
authors: [EquicordDevs.iamme],
|
||||
dependencies: ["Settings"],
|
||||
dependencies: ["Settings", "ConcatenatedModules"],
|
||||
startAt: StartAt.WebpackReady,
|
||||
toolboxActions: {
|
||||
[t("equicord.iconViewer.ui.openIconsTab")]() {
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ const filterOngoingRings = (currentUserId: string): CallUpdate["ongoingRings"] =
|
|||
args.ongoingRings.filter((id: string) => id !== currentUserId);
|
||||
|
||||
const ContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { channel: Channel; }) => {
|
||||
if (!channel || (!channel.isDM() && !channel.isGroupDM())) return;
|
||||
|
||||
if (!channel) return;
|
||||
const permanentlyIgnoredUsers = settings.store.permanentlyIgnoredUsers.split(",").map(s => s.trim()).filter(Boolean);
|
||||
|
||||
const [tempChecked, setTempChecked] = React.useState(ignoredChannelIds.has(channel.id));
|
||||
|
|
|
|||
|
|
@ -7,38 +7,35 @@
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { GuildMember } from "@vencord/discord-types";
|
||||
import { Parser } from "@webpack/common";
|
||||
import { GuildMember, RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal,openModal, Parser, React } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-inrole-");
|
||||
|
||||
export function showInRoleModal(members: GuildMember[], roleId: string, channelId: string) {
|
||||
openModal(props =>
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<ModalRoot {...props} size={ModalSize.DYNAMIC} fullscreenOnMobile={true} >
|
||||
<ModalHeader className={cl("header")}>
|
||||
<Paragraph style={{ fontSize: "1.2rem", fontWeight: "bold", marginRight: "7px" }}>Members of role {
|
||||
Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId })
|
||||
} ({members.length})</Paragraph>
|
||||
<ModalCloseButton onClick={props.onClose} className={cl("close")} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div style={{ padding: "13px 20px" }} className={cl("member-list")}>
|
||||
{
|
||||
members.length !== 0 ? members.map(member =>
|
||||
<>
|
||||
<Paragraph className={cl("modal-member")}>
|
||||
{Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : <Paragraph>Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users!</Paragraph>
|
||||
}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
openModal((props: RenderModalProps) =>
|
||||
<ErrorBoundary>
|
||||
<Modal
|
||||
{...props}
|
||||
size="md"
|
||||
title={
|
||||
<Paragraph style={{ fontSize: "1.2rem", fontWeight: "bold", marginRight: "7px" }}>
|
||||
Members of role {Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId })} ({members.length})
|
||||
</Paragraph>
|
||||
}
|
||||
>
|
||||
<div style={{ padding: "13px 20px" }} className={cl("member-list")}>
|
||||
{
|
||||
members.length !== 0 ? members.map(member =>
|
||||
<React.Fragment key={member.userId}>
|
||||
<Paragraph className={cl("modal-member")}>
|
||||
{Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })}
|
||||
</Paragraph>
|
||||
</React.Fragment>
|
||||
) : <Paragraph>Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users!</Paragraph>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,60 +16,50 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Heading, HeadingTertiary } from "@components/Heading";
|
||||
import {
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalRoot,
|
||||
openModal,
|
||||
} from "@utils/modal";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { t } from "@utils/translation";
|
||||
import { Button, React, TextInput } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, openModal, React, TextInput } from "@webpack/common";
|
||||
|
||||
import { buildEmbed, decrypt } from "../index";
|
||||
|
||||
export function DecModal(props: any) {
|
||||
export function DecModal(props: RenderModalProps & { message: any; }) {
|
||||
const encryptedMessage: string = props?.message?.content;
|
||||
const [password, setPassword] = React.useState("password");
|
||||
|
||||
const onDecrypt = () => {
|
||||
const toSend = decrypt(encryptedMessage, password, true);
|
||||
if (!toSend || !props?.message) return;
|
||||
buildEmbed(props?.message, toSend);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<HeadingTertiary>Decrypt Message</HeadingTertiary>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Heading style={{ marginTop: "10px" }}>Message with Encryption</Heading>
|
||||
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||
<Heading style={{ marginTop: "10px" }}>Password</Heading>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
onChange={setPassword}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={() => {
|
||||
const toSend = decrypt(encryptedMessage, password, true);
|
||||
if (!toSend || !props?.message) return;
|
||||
buildEmbed(props?.message, toSend);
|
||||
props.onClose();
|
||||
}}>
|
||||
Decrypt
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.LINK}
|
||||
style={{ left: 15, position: "absolute" }}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
{t("vencord.cancel")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title="Decrypt Message"
|
||||
actions={[
|
||||
{
|
||||
text: "Decrypt",
|
||||
variant: "primary",
|
||||
onClick: onDecrypt
|
||||
},
|
||||
{
|
||||
text: t("vencord.cancel"),
|
||||
variant: "secondary",
|
||||
onClick: props.onClose
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Heading style={{ marginTop: "10px" }}>Message with Encryption</Heading>
|
||||
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||
<Heading style={{ marginTop: "10px" }}>Password</Heading>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
onChange={setPassword}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,22 +17,15 @@
|
|||
*/
|
||||
|
||||
import { FormSwitch } from "@components/FormSwitch";
|
||||
import { Heading, HeadingTertiary } from "@components/Heading";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { insertTextIntoChatInputBox } from "@utils/discord";
|
||||
import {
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalProps,
|
||||
ModalRoot,
|
||||
openModal,
|
||||
} from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { Button, React, TextInput } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, openModal, React, TextInput } from "@webpack/common";
|
||||
|
||||
import { encrypt } from "../index";
|
||||
|
||||
function EncModal(props: ModalProps) {
|
||||
function EncModal(props: RenderModalProps) {
|
||||
const [secret, setSecret] = React.useState("");
|
||||
const [cover, setCover] = React.useState("");
|
||||
const [password, setPassword] = React.useState("password");
|
||||
|
|
@ -40,72 +33,65 @@ function EncModal(props: ModalProps) {
|
|||
|
||||
const isValid = secret && (noCover || (cover && cover.trim().split(" ").length > 1));
|
||||
|
||||
const onSend = () => {
|
||||
if (!isValid) return;
|
||||
const encrypted = encrypt(secret, password, noCover ? "d d" : cover);
|
||||
const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted;
|
||||
if (!toSend) return;
|
||||
|
||||
insertTextIntoChatInputBox(toSend);
|
||||
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<HeadingTertiary>Encrypt Message</HeadingTertiary>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Heading style={{ marginTop: "10px" }}>Secret</Heading>
|
||||
<TextInput
|
||||
onChange={(e: string) => {
|
||||
setSecret(e);
|
||||
}}
|
||||
/>
|
||||
<Heading style={{ marginTop: "10px" }}>Cover (2 or more Words!!)</Heading>
|
||||
<TextInput
|
||||
disabled={noCover}
|
||||
onChange={(e: string) => {
|
||||
setCover(e);
|
||||
}}
|
||||
/>
|
||||
<Heading style={{ marginTop: "10px" }}>Password</Heading>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
defaultValue={"password"}
|
||||
onChange={(e: string) => {
|
||||
setPassword(e);
|
||||
}}
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Don't use a Cover"
|
||||
value={noCover}
|
||||
onChange={(e: boolean) => {
|
||||
setNoCover(e);
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
disabled={!isValid}
|
||||
onClick={() => {
|
||||
if (!isValid) return;
|
||||
const encrypted = encrypt(secret, password, noCover ? "d d" : cover);
|
||||
const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted;
|
||||
if (!toSend) return;
|
||||
|
||||
insertTextIntoChatInputBox(toSend);
|
||||
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.LINK}
|
||||
style={{ left: 15, position: "absolute" }}
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
{t("vencord.cancel")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
title="Encrypt Message"
|
||||
actions={[
|
||||
{
|
||||
text: "Send",
|
||||
variant: "primary",
|
||||
disabled: !isValid,
|
||||
onClick: onSend
|
||||
},
|
||||
{
|
||||
text: t("vencord.cancel"),
|
||||
variant: "secondary",
|
||||
onClick: props.onClose
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Heading style={{ marginTop: "10px" }}>Secret</Heading>
|
||||
<TextInput
|
||||
onChange={(e: string) => {
|
||||
setSecret(e);
|
||||
}}
|
||||
/>
|
||||
<Heading style={{ marginTop: "10px" }}>Cover (2 or more Words!!)</Heading>
|
||||
<TextInput
|
||||
disabled={noCover}
|
||||
onChange={(e: string) => {
|
||||
setCover(e);
|
||||
}}
|
||||
/>
|
||||
<Heading style={{ marginTop: "10px" }}>Password</Heading>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
defaultValue={"password"}
|
||||
onChange={(e: string) => {
|
||||
setPassword(e);
|
||||
}}
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Don't use a Cover"
|
||||
value={noCover}
|
||||
onChange={(e: boolean) => {
|
||||
setNoCover(e);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ import "./style.css";
|
|||
import { actions } from "@equicordplugins/keyboardNavigation/commands";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { closeAllModals, Modal,openModal, React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "..";
|
||||
|
||||
const logger = new Logger("CommandPalette", "#e5c890");
|
||||
|
||||
export function CommandPalette({ modalProps }) {
|
||||
export function CommandPalette({ modalProps }: { modalProps: RenderModalProps; }) {
|
||||
const cl = classNameFactory("vc-command-palette-");
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
|
|
@ -100,8 +100,8 @@ export function CommandPalette({ modalProps }) {
|
|||
}, [queryEh]);
|
||||
|
||||
return (
|
||||
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}>
|
||||
<div>
|
||||
<Modal {...modalProps} size="md" title="Command Palette">
|
||||
<div className={cl("root")} onKeyDown={handleKeyDown} onWheel={handleWheel}>
|
||||
<TextInput
|
||||
value={queryEh}
|
||||
onChange={e => setQuery(e)}
|
||||
|
|
@ -123,7 +123,7 @@ export function CommandPalette({ modalProps }) {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import "./style.css";
|
|||
|
||||
import { ButtonAction } from "@equicordplugins/keyboardNavigation/commands";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { closeAllModals, Modal,openModal, React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "..";
|
||||
|
||||
interface MultipleChoiceProps {
|
||||
modalProps: ModalProps;
|
||||
modalProps: RenderModalProps;
|
||||
onSelect: (selectedValue: any) => void;
|
||||
choices: ButtonAction[];
|
||||
}
|
||||
|
|
@ -99,9 +99,8 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice
|
|||
}, [queryEh]);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}>
|
||||
<div>
|
||||
<Modal {...modalProps} size="md" title="Multiple Choice">
|
||||
<div className={cl("root")} onKeyDown={handleKeyDown} onWheel={handleWheel}>
|
||||
<TextInput
|
||||
value={queryEh}
|
||||
onChange={e => setQuery(e)}
|
||||
|
|
@ -123,7 +122,7 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import "./style.css";
|
||||
|
||||
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { closeAllModals, Modal,openModal, React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
interface SimpleTextInputProps {
|
||||
modalProps: ModalProps;
|
||||
modalProps: RenderModalProps;
|
||||
onSelect: (inputValue: string) => void;
|
||||
placeholder?: string;
|
||||
info?: string;
|
||||
|
|
@ -35,16 +35,17 @@ export function SimpleTextInput({ modalProps, onSelect, placeholder, info }: Sim
|
|||
}, []);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ModalRoot className="vc-command-palette-simple-text" {...modalProps} size={ModalSize.DYNAMIC} onKeyDown={handleKeyDown}>
|
||||
<TextInput
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e as unknown as string)}
|
||||
style={{ width: "30vw", borderRadius: "5px" }}
|
||||
placeholder={placeholder ?? "Type and press Enter"}
|
||||
/>
|
||||
{info && <div className="vc-command-palette-textinfo">{info}</div>}
|
||||
</ModalRoot>
|
||||
<Modal {...modalProps} size="sm" title="Text Input">
|
||||
<div className="vc-command-palette-simple-text" onKeyDown={handleKeyDown}>
|
||||
<TextInput
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e as unknown as string)}
|
||||
style={{ width: "30vw", borderRadius: "5px" }}
|
||||
placeholder={placeholder ?? "Type and press Enter"}
|
||||
/>
|
||||
{info && <div className="vc-command-palette-textinfo">{info}</div>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { SettingsRouter, useState } from "@webpack/common";
|
||||
import { closeAllModals,SettingsRouter, useState } from "@webpack/common";
|
||||
|
||||
import { registerAction } from "./commands";
|
||||
import { openCommandPalette } from "./components/CommandPalette";
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue