feat(plugin): Dragify (#684)
* first and last commit * fixed. * reuse invite setting toggle * complete rewrite * sushi slide * woah new setting alert * polish * Optimizations + fix merge conflict lol * more optimizations * my code editor did NOT tell me about this error * t * Update index.tsx * fixes * fixes * patch fixes * Dragify's finally done * drop accidental changes * patch fixes * feat(MessageClickActions): add REPLY to own messages (#677) - also adds a click timeout to double click (for MS) * Apply suggestion from @Vendicated * improve ExpandableList find * make plain folder icon not cursed + fix incorrect patch it was missing the ! before we insert our class * limit ml patch length * boom 5 * Apply suggestion from @sadan4 Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> * fix the boom I broke * Apply suggestion from @Vendicated * limit length * Apply suggestion from @Vendicated * fix find for friends since * limit length * fix lint * so good * limit length * fix plainfoldericon * improve IgnoreActitivies * Update src/plugins/messageLogger/index.tsx Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> * Update src/plugins/messageLogger/index.tsx Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> * I'm so smart * limit length * stupid x2 * fix IgnoreActivities * limit length * fix * fix showHiddenThings * sssss * make backref lazy * fix: vcNarratorCustom user leave voice event filtering bug (#671) * fix(vcNarratorCustom): voice state event filtering is stricter now and has a guard against false positive userleave events * refactor(vcNarratorCustom): remove unused audio toggle functions for self-mute and self-deaf, they're not wired into the menu anyways. I forgot about them cause i just use discords built in mute/deafen sound for my own audio * fix(vcNarratorCustom): ensure voice state snapshot updates only occur if myChanId is defined as an extra guard * migrate to new switch style (#669) * fix everything (#3925) Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com> Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Co-authored-by: V <vendicated@riseup.net> Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: prism <snawalt420@proton.me> Co-authored-by: wearrrrr <contact@wearr.dev> Co-authored-by: Elvyra <88881326+EepyElvyra@users.noreply.github.com> * bump to v1.14.1 * WebScreenshareFixes: remove codec changes This causes too many issues unfortunately * ExpressionCloner: fix cloning animated emojis * Maybe bug fix. (#678) * Add support for silent pings in notifications (#679) * fix * Decor: Fix change decoration modal being unable to be opened * Questify Bug Fix (Real) (#680) * Maybe bug fix. * Actually fixed this time (real). * Remove log. * PlatformSpoofer: Do a terrible patch * RoleColorEverywhere: Fix not working on voice channel and chat (#3934) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> * bunp discord-types * apply patch fix to rolecoloreverywhere * remove duplicate EquicordDevs.Prism * fix(mediaPlaybackSpeed): update color vars, fix alignment (#681) * fix(mediaPlaybackSpeed): update color vars to fix on voice message * fix(mediaPlaybackSpeed): fix alignment * fix prism * MessageClickActions: Click actions no longer trigger on message links (#683) * fix(MessageClickActions): stuff fixes click actions triggering on message links, also use the double click timeout for single click * fix(MessageClickActions): only delay actions if current action is valid * fix(MessageClickActions): proactive modifier checking * add fileUpload plugin (#685) * add fileUpload plugin * nest * types * css * hey man * nest -> destkop only, per service tokens, hide only service per options * Update upload.ts --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> * feat(FileUpload): add E-Z to hosts (#687) * Questify: Feature and bugfix. (#689) * Feature and bugfix. * Improve comment. * cool changes (#682) * BetterUserArea: fix some stuff * bleh * add better update notification * wau * HeaderBarApi: fix new style * Fix clips (kinda) (#688) * add ignorePlatformRestriction toggle * Clean up types * fix intl --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> * update css * chore: KeywordNotify cleanup (#692) * remove commas from highlights in inbox * use Button Component for Clear All button * fix other buttons * cleanup try catches * keywordnotify update * feat: prevent middle-click link open (option) (#690) * feat: add NoMiddleOpen plugin * feat: extend LimitMiddleClickPaste * ReviewDB: Fix viewing reviews with new CSS mangling * NoBlockedMessages: Fix extra space between messages * WebContextMenus: Fix restoring Copy & Save Image * Fix Most Broken Patches * Add FavoriteAnything * LimitMiddleClickPaste: Description * fix patch * Fix patches for latest Discord update (#3943) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> * fix(SupportHelper): make update button check case-insensitive (#3939) * revert toastnotif edits * update and add components (#691) * update: switch * add: notice * update: link * add: badge * update heading * update: basetext * update: card * fix: heading * components-dev: fix * lint * update: margins * KeywordNotify: Revert some changes * Make Messageclickactions Great Again! (#695) forgive me father for i have sinned, and thank you prism for fixing my awful code * add equicord tray menu for desktop discord (#694) * fix pnpm inject on local builds (#696) * bypass cors to fix e-z uploads (#697) * channelTabs macos tweaks (#701) * fix: show info icon instead of cog for plugins with only hidden settings (#699) * Questify: Improve panel override patch and add ctx option to resume Auto Complete. (#698) * Improve panel override, add ctx option. * Improve panel text. * holyNotes: fix - Modal Fixes, Don't Update Global UserStore (#693) * Fix: holyNotes - Make Modal Actually Bearable * Fix: holyNotes - Fix Double Declaration * Fix: holyNotes - Don't Change Global UserStore Co-Authored-By: prism <69634294+imjustprism@users.noreply.github.com> * QOL: holyNotes - Change Bot Tag to "NOTE" * Fix: holyNotes - Fuck Your Comments GitHub Copilot, I Thought I Turned You Off * Feat: holyNotes - UI Overhaul, Fixed Spaghetti CSS * Fix: holyNotes - Fix CSS Classes * Fix: holyNotes - Fix Fetching User Profiles for Notes --------- Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com> * This better not break again for at least a year. (#702) * Questify: CSS Fix. (#703) * This better not break again for at least a year. * CSS. * fix VoiceMessages plugin * Settings: add textarea options * misc Settings ui improvements * VcNarrator: only narrate events of current session * CrashHandler: log component stack on crash (#3933) * Translate: update DeepL language list (#3919) * StartupTimings: Fix duplicate find * CopyFileContents: Fix broken patch * ChatInputButtonAPI: Fix broken patch * ShowConnections: Fix broken patch * Fix Patches * remove styles * Questify: 2025-12-quest-cta-refactor-rollout support. (#704) * 2025-12-quest-cta-refactor-rollout support. * Fix. * BetterActivities: Update * BetterActivities: Remove logs * MessageClickActions: Update (#706) * ThemeLibrary: Fix Key * ThemeLibrary: Fix Key * LoginWithQR: Fixes * fixes (#705) * HeaderBarApi: fix minor style issue * HolyNotes: replace icon * Components.dev: remove popout * HeaderBarApi: fix tooltip * HolyNotes: replace icon again * HolyNotes: refactor * Switch: fix thumb color for whtie theme * BetterUserArea: fix patch * ModalCloseButton: remove dups and export properly * bleh * Update NotebookCreateModal.tsx * bleh --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> * BetterActivities: Fix Spacing * KeywordNotify: Update * bump * Equicord Tray: Update Repair * Fix Dummy Filled Authors Co-Authored-By: Etorix <92535668+etorixdev@users.noreply.github.com> * DevCompanion: Update * Tray: Rename IPC Events * Tray: About page * About: Redesign to match Equicord website (#709) * Tray: Hyperlink Version & Commit * Message click actions (yes, again) (#710) * fix(MessageClickActions): stop sticky modifiers caused by not cleaning up * fix(MessageClickActions): double clicks now double click double as consistently * she make on my range till i marker * chore: patch improvements * chore: fix remaining merge conflicts and resolve duplicates * chore: sync with upstream dev and remove broken vencondToolbox * fixes * greatest commit of all time * patch improvements and code quality improvements * fucking hell * bleh * Update index.tsx * hopefully the last commit * code quality improvements * can this shit end * lint * error misfire fix * lint * cook dragify back * Update index.tsx * LET IT END --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: Key <keircn@proton.me> Co-authored-by: V <vendicated@riseup.net> Co-authored-by: sadan <117494111+sadan4@users.noreply.github.com> Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Co-authored-by: Sam <82487541+example-git@users.noreply.github.com> Co-authored-by: prism <snawalt420@proton.me> Co-authored-by: prism <69634294+imjustprism@users.noreply.github.com> Co-authored-by: wearrrrr <contact@wearr.dev> Co-authored-by: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Co-authored-by: Etorix <92535668+EtorixDev@users.noreply.github.com> Co-authored-by: DjJewl <47831173+wolflord212312@users.noreply.github.com> Co-authored-by: verticalsync <60797172+verticalsync@users.noreply.github.com> Co-authored-by: Nico <nico@d3sox.me> Co-authored-by: creations <creations@creations.works> Co-authored-by: x3rt <git@x3rt.dev> Co-authored-by: korzi <106588537+notkorzi@users.noreply.github.com> Co-authored-by: Dog <104234930+dgxo@users.noreply.github.com> Co-authored-by: Z ❤'s Life <zcomer4dthesecond@gmail.com> Co-authored-by: Vũ Minh Nguyên <PiscesKaze@gmail.com>
This commit is contained in:
parent
d6ede6fa3c
commit
484cf5d67b
6 changed files with 1797 additions and 4 deletions
|
|
@ -22,7 +22,7 @@ interface IMessageCreate {
|
|||
message: Message;
|
||||
}
|
||||
|
||||
const SILENT_PING_FLAG = 1 << 12;
|
||||
const SILENT_PING_FLAG = 1 << 12;
|
||||
|
||||
function Icon(enabled?: boolean): JSX.Element {
|
||||
return <svg
|
||||
|
|
@ -168,7 +168,7 @@ export default definePlugin({
|
|||
if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === currentChannelId && WindowStore.isFocused()) || userStatus !== settings.store.statusToUse) {
|
||||
return;
|
||||
}
|
||||
if (settings.store.respectSilentPings && (message.flags & SILENT_PING_FLAG)) {return;}
|
||||
if (settings.store.respectSilentPings && (message.flags & SILENT_PING_FLAG)) { return; }
|
||||
const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned;
|
||||
if ((settings.store.guilds.split(", ").includes(guildId) || settings.store.channels.split(", ").includes(channelId)) && mentioned) {
|
||||
await showNotification(message, guildId);
|
||||
|
|
|
|||
237
src/equicordplugins/dragify/ghost.tsx
Normal file
237
src/equicordplugins/dragify/ghost.tsx
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { createRoot, React, useEffect, useState, useStateFromStores, VoiceStateStore } from "@webpack/common";
|
||||
|
||||
export type GhostState = {
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
kind: "user" | "channel" | "guild";
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
iconUrl?: string;
|
||||
symbol?: string;
|
||||
badge?: string;
|
||||
entityId?: string;
|
||||
exiting: boolean;
|
||||
};
|
||||
|
||||
const ScreenArrowIcon = findComponentByCodeLazy("3V5Zm16") as React.ComponentType<{
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
size?: number;
|
||||
color?: string;
|
||||
colorClass?: string;
|
||||
}>;
|
||||
|
||||
const cl = classNameFactory("vc-dragify-");
|
||||
|
||||
let ghostRoot: ReturnType<typeof createRoot> | null = null;
|
||||
let ghostMountNode: HTMLDivElement | null = null;
|
||||
let ghostRaf: number | null = null;
|
||||
let ghostPendingPos: { x: number; y: number; } | null = null;
|
||||
let ghostHideTimer: number | null = null;
|
||||
|
||||
let ghostState: GhostState = {
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
kind: "channel",
|
||||
title: "",
|
||||
exiting: false
|
||||
};
|
||||
|
||||
const ghostListeners = new Set<() => void>();
|
||||
|
||||
function notifyGhost() {
|
||||
ghostListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
function setGhostState(next: Partial<GhostState>) {
|
||||
ghostState = { ...ghostState, ...next };
|
||||
notifyGhost();
|
||||
}
|
||||
|
||||
export function isGhostVisible(): boolean {
|
||||
return ghostState.visible;
|
||||
}
|
||||
|
||||
export function scheduleGhostPosition(x: number, y: number) {
|
||||
ghostPendingPos = { x, y };
|
||||
if (ghostRaf !== null) return;
|
||||
setGhostState({ x, y });
|
||||
ghostRaf = requestAnimationFrame(() => {
|
||||
if (ghostPendingPos) setGhostState({ x: ghostPendingPos.x, y: ghostPendingPos.y });
|
||||
ghostPendingPos = null;
|
||||
ghostRaf = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function hideGhost() {
|
||||
if (!ghostState.visible) return;
|
||||
if (ghostHideTimer !== null) {
|
||||
clearTimeout(ghostHideTimer);
|
||||
ghostHideTimer = null;
|
||||
}
|
||||
setGhostState({ exiting: true });
|
||||
ghostHideTimer = window.setTimeout(() => {
|
||||
ghostHideTimer = null;
|
||||
setGhostState({ visible: false, exiting: false });
|
||||
}, 200);
|
||||
}
|
||||
|
||||
export function showGhost(next: Omit<GhostState, "visible" | "x" | "y">, position?: { x: number; y: number; }) {
|
||||
if (ghostHideTimer !== null) {
|
||||
clearTimeout(ghostHideTimer);
|
||||
ghostHideTimer = null;
|
||||
}
|
||||
if (position) scheduleGhostPosition(position.x, position.y);
|
||||
setGhostState({ ...next, visible: true, exiting: false });
|
||||
}
|
||||
|
||||
export function mountGhost() {
|
||||
if (typeof document === "undefined") return;
|
||||
const { body } = document;
|
||||
if (!body) return;
|
||||
|
||||
if (ghostRoot) {
|
||||
if (ghostMountNode && !ghostMountNode.isConnected) body.appendChild(ghostMountNode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ghostMountNode) ghostMountNode = document.createElement("div");
|
||||
if (!ghostMountNode.isConnected) body.appendChild(ghostMountNode);
|
||||
|
||||
ghostRoot = createRoot(ghostMountNode);
|
||||
ghostRoot.render(
|
||||
<div className={cl("ghost-container")}>
|
||||
<ErrorBoundary noop>
|
||||
<DragGhost />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function unmountGhost() {
|
||||
ghostRoot?.unmount();
|
||||
ghostRoot = null;
|
||||
|
||||
ghostMountNode?.remove();
|
||||
ghostMountNode = null;
|
||||
|
||||
if (ghostRaf !== null) {
|
||||
cancelAnimationFrame(ghostRaf);
|
||||
ghostRaf = null;
|
||||
}
|
||||
ghostPendingPos = null;
|
||||
|
||||
if (ghostHideTimer !== null) {
|
||||
clearTimeout(ghostHideTimer);
|
||||
ghostHideTimer = null;
|
||||
}
|
||||
setGhostState({ visible: false, exiting: false });
|
||||
}
|
||||
|
||||
const DragGhost = () => {
|
||||
const [state, setState] = useState(ghostState);
|
||||
useEffect(() => {
|
||||
const listener = () => setState({ ...ghostState });
|
||||
ghostListeners.add(listener);
|
||||
return () => {
|
||||
ghostListeners.delete(listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const voiceState = useStateFromStores(
|
||||
[VoiceStateStore],
|
||||
() => (state.kind === "user" && state.entityId
|
||||
? VoiceStateStore.getVoiceStateForUser(state.entityId)
|
||||
: null)
|
||||
);
|
||||
const inVoice = voiceState?.channelId;
|
||||
const isMuted = voiceState && (voiceState.selfMute || voiceState.mute || voiceState.selfDeaf || voiceState.deaf);
|
||||
const isStreaming = voiceState?.selfStream;
|
||||
|
||||
if (!state.visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes(cl("ghost"), state.exiting ? cl("ghost-exit") : "")}
|
||||
style={{ transform: `translate3d(${state.x}px, ${state.y}px, 0)` }}
|
||||
>
|
||||
<div className={cl("card")}>
|
||||
<div className={cl("icon")}>
|
||||
{state.iconUrl
|
||||
? <img className={cl("icon-image")} src={state.iconUrl} alt="" />
|
||||
: <span className={cl("icon-text")}>{state.symbol ?? "#"}</span>
|
||||
}
|
||||
</div>
|
||||
<div className={cl("body")}>
|
||||
<div className={cl("title-row")}>
|
||||
<div className={cl("title")}>{state.title}</div>
|
||||
{state.kind === "user" && inVoice
|
||||
? (isMuted
|
||||
? <VoiceMutedIcon className={classes(cl("voice-icon"), cl("voice-icon-muted"))} />
|
||||
: <VoiceStateIcon className={cl("voice-icon")} />)
|
||||
: null}
|
||||
{state.kind === "user" && inVoice && isStreaming
|
||||
? <ScreenArrowIcon
|
||||
className={classes(cl("voice-icon"), cl("voice-icon-stream"))}
|
||||
size={14}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
{state.subtitle && <div className={cl("subtitle")}>{state.subtitle}</div>}
|
||||
</div>
|
||||
<div className={cl("badge")}>{state.badge ?? state.kind}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function VoiceStateIcon({ className, size = 14 }: { className?: string; size?: number; }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 2a1 1 0 0 0-1 1v18a1 1 0 1 0 2 0V3a1 1 0 0 0-1-1ZM11 6a1 1 0 1 1 2 0v12a1 1 0 1 1-2 0V6ZM1 8a1 1 0 0 1 2 0v8a1 1 0 1 1-2 0V8ZM16 5a1 1 0 1 1 2 0v14a1 1 0 1 1-2 0V5ZM22 8a1 1 0 0 0-1 1v6a1 1 0 1 0 2 0V9a1 1 0 0 0-1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function VoiceMutedIcon({ className, size = 14 }: { className?: string; size?: number; }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4l20-20ZM6.85 13.15a.5.5 0 0 1-.85-.36V3a1 1 0 0 1 2 0v8.8a.5.5 0 0 1-.15.35l-1 1ZM11 17.2v.8a1 1 0 1 0 2 0v-1.8a.5.5 0 0 0-.85-.35l-1 1a.5.5 0 0 0-.15.36ZM11 7.8V6a1 1 0 1 1 2 0v.8a.5.5 0 0 1-.15.35l-1 1a.5.5 0 0 1-.85-.36ZM17.15 10.85a.5.5 0 0 1 .85.36V19a1 1 0 1 1-2 0v-6.8a.5.5 0 0 1 .15-.35l1-1ZM2 7a1 1 0 0 0-1 1v8a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1ZM21 9a1 1 0 1 1 2 0v6a1 1 0 1 1-2 0V9Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
1225
src/equicordplugins/dragify/index.tsx
Normal file
1225
src/equicordplugins/dragify/index.tsx
Normal file
File diff suppressed because it is too large
Load diff
149
src/equicordplugins/dragify/style.css
Normal file
149
src/equicordplugins/dragify/style.css
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
.vc-dragify-ghost-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.vc-dragify-ghost {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
color: var(--text-normal, #fff);
|
||||
opacity: 1;
|
||||
animation: vc-dragify-fade-in 200ms ease-out forwards;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.vc-dragify-ghost-exit {
|
||||
animation: vc-dragify-fade-out 200ms ease-in forwards;
|
||||
}
|
||||
|
||||
.vc-dragify-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 10px;
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
border-radius: 6px;
|
||||
background: rgb(0 0 0 / 70%);
|
||||
border: 1px solid rgb(255 255 255 / 10%);
|
||||
box-shadow: 0 12px 40px rgb(0 0 0 / 35%);
|
||||
backdrop-filter: blur(12px);
|
||||
transform: translateZ(0);
|
||||
color: var(--text-default, #fff);
|
||||
font-family: var(--font-code);
|
||||
line-height: 1.4;
|
||||
animation: none;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.vc-dragify-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: rgb(255 255 255 / 10%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-default, #fff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vc-dragify-icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.vc-dragify-icon-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-default, #fff);
|
||||
}
|
||||
|
||||
.vc-dragify-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vc-dragify-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vc-dragify-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-default, #fff);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vc-dragify-voice-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--interactive-normal, #b9bbbe);
|
||||
}
|
||||
|
||||
.vc-dragify-voice-icon-muted {
|
||||
color: var(--status-danger, #ed4245);
|
||||
}
|
||||
|
||||
.vc-dragify-voice-icon-stream {
|
||||
color: var(--status-positive, #3ba55d);
|
||||
}
|
||||
|
||||
.vc-dragify-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vc-dragify-badge {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@keyframes vc-dragify-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vc-dragify-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vc-dragify-ghost {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.vc-dragify-ghost-exit {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
182
src/equicordplugins/dragify/utils.ts
Normal file
182
src/equicordplugins/dragify/utils.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Channel, Guild, User } from "@vencord/discord-types";
|
||||
|
||||
const userMentionRegex = /<@!?(\d{17,20})>/;
|
||||
const userProfileUrlRegex = /discord(?:(?:app)?\.com|:\/\/-?)\/users\/(\d{17,20})/;
|
||||
const userAvatarRegex = /cdn\.discordapp\.com\/(?:avatars|users)\/(\d{17,20})\//;
|
||||
const guildUserAvatarRegex = /cdn\.discordapp\.com\/guilds\/\d{17,20}\/users\/(\d{17,20})\/avatars\//;
|
||||
const channelMentionRegex = /<#(\d{17,20})>/;
|
||||
const channelUrlRegex = /discord(?:(?:app)?\.com|:\/\/-?)\/channels\/(?:(@me)|(\d{17,20}))\/(\d{17,20})/;
|
||||
const channelPathRegex = /\/channels\/(@me|\d{17,20})\/(\d{17,20})/;
|
||||
const guildIconRegex = /cdn\.discordapp\.com\/icons\/(\d{17,20})\//;
|
||||
|
||||
export type DropEntity =
|
||||
| { kind: "user"; id: string; }
|
||||
| { kind: "channel"; id: string; guildId?: string; }
|
||||
| { kind: "guild"; id: string; };
|
||||
|
||||
interface DragifyPayload {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
type?: string;
|
||||
kind?: string;
|
||||
itemType?: string;
|
||||
}
|
||||
|
||||
type StoreSet = {
|
||||
ChannelStore: { getChannel(id: string): Channel | null | undefined; };
|
||||
GuildStore: { getGuild(id: string): Guild | null | undefined; };
|
||||
UserStore: { getUser(id: string): User | null | undefined; };
|
||||
};
|
||||
|
||||
export function tryParseJson<T = Record<string, unknown>>(value: string): T | null {
|
||||
if (!value || value.length < 2 || (value[0] !== "{" && value[0] !== "[")) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return typeof parsed === "object" && parsed ? parsed as T : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractSnowflakeFromString(value: string): string | null {
|
||||
const match = value.match(/\d{17,20}/);
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
export function extractSnowflakes(values: string[]): string[] {
|
||||
const ids = new Set<string>();
|
||||
const regex = /\d{17,20}/g;
|
||||
for (const value of values) {
|
||||
const matches = value.match(regex);
|
||||
if (matches) matches.forEach(id => ids.add(id));
|
||||
}
|
||||
return Array.from(ids);
|
||||
}
|
||||
|
||||
export function extractStrings(dataTransfer: DataTransfer): string[] {
|
||||
const collected = new Set<string>();
|
||||
const add = (v?: string) => v && collected.add(v);
|
||||
add(dataTransfer.getData("text/plain"));
|
||||
add(dataTransfer.getData("text/uri-list"));
|
||||
add(dataTransfer.getData("text/html"));
|
||||
add(dataTransfer.getData("text/x-moz-url"));
|
||||
add(dataTransfer.getData("application/json"));
|
||||
for (const type of dataTransfer.types ?? []) add(dataTransfer.getData(type));
|
||||
|
||||
const split: string[] = [];
|
||||
for (const v of collected) {
|
||||
split.push(v);
|
||||
if (v.includes("\n")) split.push(...v.split(/\s+/).filter(Boolean));
|
||||
}
|
||||
return Array.from(new Set(split));
|
||||
}
|
||||
|
||||
export async function collectPayloadStrings(dataTransfer: DataTransfer): Promise<string[]> {
|
||||
const sync = extractStrings(dataTransfer);
|
||||
const asyncValues: string[] = [];
|
||||
const itemPromises = Array.from(dataTransfer.items ?? [])
|
||||
.filter(item => item.kind === "string")
|
||||
.map(item => new Promise<void>(resolve => item.getAsString(val => {
|
||||
if (val) asyncValues.push(val);
|
||||
resolve();
|
||||
})));
|
||||
await Promise.all(itemPromises);
|
||||
return Array.from(new Set([...sync, ...asyncValues]));
|
||||
}
|
||||
|
||||
export function parseFromStrings(payloads: string[], stores: StoreSet): DropEntity | null {
|
||||
if (payloads.length === 0) return null;
|
||||
|
||||
for (const value of payloads) {
|
||||
const parsed = tryParseJson<DragifyPayload>(value);
|
||||
if (parsed) {
|
||||
const id = parsed.id ?? parsed.userId ?? parsed.channelId ?? parsed.guildId;
|
||||
const type = (parsed.type ?? parsed.kind ?? parsed.itemType ?? "").toLowerCase();
|
||||
if (id) {
|
||||
if (type.includes("user")) return { kind: "user", id };
|
||||
if (type.includes("channel")) return { kind: "channel", id, guildId: parsed.guildId };
|
||||
if (type.includes("guild") || type.includes("server")) return { kind: "guild", id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of payloads) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) continue;
|
||||
const userFromMention = userMentionRegex.exec(trimmed);
|
||||
if (userFromMention) return { kind: "user", id: userFromMention[1] };
|
||||
const userFromProfile = userProfileUrlRegex.exec(trimmed);
|
||||
if (userFromProfile) return { kind: "user", id: userFromProfile[1] };
|
||||
const guildUserAvatar = guildUserAvatarRegex.exec(trimmed);
|
||||
if (guildUserAvatar) return { kind: "user", id: guildUserAvatar[1] };
|
||||
const userFromAvatar = userAvatarRegex.exec(trimmed);
|
||||
if (userFromAvatar) return { kind: "user", id: userFromAvatar[1] };
|
||||
}
|
||||
|
||||
for (const value of payloads) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) continue;
|
||||
const channelFromMention = channelMentionRegex.exec(trimmed);
|
||||
if (channelFromMention) return { kind: "channel", id: channelFromMention[1] };
|
||||
const channelFromUrl = channelUrlRegex.exec(trimmed);
|
||||
if (channelFromUrl) {
|
||||
const guildId = channelFromUrl[1] === "@me" ? "@me" : channelFromUrl[2];
|
||||
return { kind: "channel", id: channelFromUrl[3], guildId: guildId ?? undefined };
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of payloads) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) continue;
|
||||
const guildFromIcon = guildIconRegex.exec(trimmed);
|
||||
if (guildFromIcon) return { kind: "guild", id: guildFromIcon[1] };
|
||||
}
|
||||
|
||||
const candidates = extractSnowflakes(payloads);
|
||||
for (const candidate of candidates) {
|
||||
if (stores.ChannelStore.getChannel(candidate)) return { kind: "channel", id: candidate };
|
||||
if (stores.GuildStore.getGuild(candidate)) return { kind: "guild", id: candidate };
|
||||
if (stores.UserStore.getUser(candidate)) return { kind: "user", id: candidate };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractChannelPath(value: string): { guildId?: string; channelId?: string; } | null {
|
||||
const channelPath = channelPathRegex.exec(value);
|
||||
if (!channelPath) return null;
|
||||
return {
|
||||
guildId: channelPath[1] === "@me" ? "@me" : channelPath[1],
|
||||
channelId: channelPath[2]
|
||||
};
|
||||
}
|
||||
|
||||
export function extractChannelFromUrl(value: string): { guildId?: string; channelId?: string; } | null {
|
||||
const channelFromUrl = channelUrlRegex.exec(value);
|
||||
if (!channelFromUrl) return null;
|
||||
const guildId = channelFromUrl[1] ?? channelFromUrl[2];
|
||||
return {
|
||||
guildId: guildId === "@me" ? "@me" : guildId,
|
||||
channelId: channelFromUrl[3]
|
||||
};
|
||||
}
|
||||
|
||||
export function extractUserFromAvatar(value: string): string | null {
|
||||
const guildUserAvatar = guildUserAvatarRegex.exec(value);
|
||||
if (guildUserAvatar?.[1]) return guildUserAvatar[1];
|
||||
const userAvatar = userAvatarRegex.exec(value);
|
||||
if (userAvatar?.[1]) return userAvatar[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractUserFromProfile(value: string): string | null {
|
||||
const match = userProfileUrlRegex.exec(value);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
@ -94,7 +94,7 @@ const handleMessage = ((channelId, message) => {
|
|||
const wordMatch: WordMatch | null = chooseRandomWord(msg);
|
||||
if (wordMatch === null) return;
|
||||
|
||||
const word = wordMatch.word;
|
||||
const { word } = wordMatch;
|
||||
const wordUpper = word.toUpperCase();
|
||||
const isUpper = word === wordUpper;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue