From 9dd8a65eb2be1d405d4ce22a9feceb2012be177b Mon Sep 17 00:00:00 2001 From: Creation's Date: Mon, 14 Oct 2024 19:36:52 -0400 Subject: [PATCH] feat(plugin): ChannelBadges (#51) * start of channelbadges * update badges * give in and add colors to settings page, refactor * change from styles.css to style.css * update for linter * forgot to remove dm css no need for them * Fixes --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/channelBadges/index.ts | 140 +++++++ .../channelBadges/settings.tsx | 389 ++++++++++++++++++ src/equicordplugins/channelBadges/style.css | 69 ++++ 3 files changed, 598 insertions(+) create mode 100644 src/equicordplugins/channelBadges/index.ts create mode 100644 src/equicordplugins/channelBadges/settings.tsx create mode 100644 src/equicordplugins/channelBadges/style.css diff --git a/src/equicordplugins/channelBadges/index.ts b/src/equicordplugins/channelBadges/index.ts new file mode 100644 index 00000000..99238438 --- /dev/null +++ b/src/equicordplugins/channelBadges/index.ts @@ -0,0 +1,140 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import { EquicordDevs } from "@utils/constants"; +import { getCurrentGuild } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { ChannelStore, SelectedGuildStore } from "@webpack/common"; +import { Channel, Guild } from "discord-types/general"; + +import { isEnabled, returnChannelBadge, settings } from "./settings"; + +let observer: MutationObserver | null = null; +let currentGuild: Guild | undefined | null = null; + +function addBadgesToChannel(element: HTMLElement, channelId: string) { + const parentContainer: HTMLElement | null = element.querySelector('[class*="linkTop"]'); + if (!parentContainer) return; + + const channel: Channel | undefined = ChannelStore.getChannel(channelId); + if (!channel || !isEnabled(channel.type)) return; + + const { type, nsfw, threadMetadata } = channel; + const isPrivate = channel.isPrivate() || threadMetadata?.locked || channel.isArchivedThread(); + const isNSFW = nsfw || channel.isNSFW(); + + let badgeContainer: HTMLElement | null = parentContainer.querySelector(".badge-container"); + if (!badgeContainer) { + badgeContainer = document.createElement("div"); + badgeContainer.classList.add("badge-container"); + parentContainer.appendChild(badgeContainer); + } + + const badgeConditions = [ + { id: 6101, condition: isPrivate, title: "This channel is locked." }, + { id: 6100, condition: isNSFW, title: "This channel is marked as NSFW." }, + { id: 6102, condition: currentGuild?.rulesChannelId === channel.id, title: "This channel is the server rules channel." }, + ]; + + badgeConditions.forEach(({ id, condition, title }) => { + if (condition && isEnabled(id)) addBadge(badgeContainer!, id, title); + }); + + addBadge(badgeContainer!, type, returnChannelBadge(type).label); +} + +function addBadge(container: HTMLElement, id: number, title: string) { + const { css, label, color } = returnChannelBadge(id); + const badge = document.createElement("div"); + badge.classList.add("channel-badge", `channel-badge-${css}`); + badge.textContent = label; + badge.title = title; + + if (color && color !== "") { + badge.style.backgroundColor = color; + } + + container.appendChild(badge); +} + +function deleteAllBadges() { + document.querySelectorAll(".channel-badge").forEach(badge => badge.remove()); + document.querySelectorAll('[data-list-item-id^="channels___"][data-scanned]').forEach(element => { + element.removeAttribute("data-scanned"); + }); +} + +export function reloadBadges() { + deleteAllBadges(); + document.querySelectorAll('[data-list-item-id^="channels___"]').forEach(element => { + const channelId = element.getAttribute("data-list-item-id")?.split("___")[1]; + if (channelId && /^\d+$/.test(channelId)) { + addBadgesToChannel(element as HTMLElement, channelId); + element.setAttribute("data-scanned", "true"); + } + }); +} + +function observeDomChanges() { + if (observer) observer.disconnect(); + + const handleMutations = (mutations: MutationRecord[]) => { + const addedElements = new Set(); + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + element.querySelectorAll('[data-list-item-id^="channels___"]:not([data-scanned])').forEach(child => { + addedElements.add(child); + }); + } + }); + }); + + addedElements.forEach(element => { + const channelId = element.getAttribute("data-list-item-id")?.split("___")[1]; + if (channelId && /^\d+$/.test(channelId)) { + addBadgesToChannel(element as HTMLElement, channelId); + element.setAttribute("data-scanned", "true"); + } + }); + }; + + observer = new MutationObserver(handleMutations); + observer.observe(document.body, { childList: true, subtree: true }); +} + +function onGuildChange() { + const newGuild: Guild | undefined | null = getCurrentGuild(); + if (newGuild !== currentGuild) { + currentGuild = newGuild; + } +} + +export default definePlugin({ + name: "ChannelBadges", + description: "Adds badges to channels based on their type", + authors: [EquicordDevs.creations], + settings, + + async start() { + currentGuild = getCurrentGuild(); + observeDomChanges(); + reloadBadges(); + SelectedGuildStore.addChangeListener(onGuildChange); + }, + + stop() { + if (observer) { + observer.disconnect(); + observer = null; + } + deleteAllBadges(); + SelectedGuildStore.removeChangeListener(onGuildChange); + } +}); diff --git a/src/equicordplugins/channelBadges/settings.tsx b/src/equicordplugins/channelBadges/settings.tsx new file mode 100644 index 00000000..c0283bd5 --- /dev/null +++ b/src/equicordplugins/channelBadges/settings.tsx @@ -0,0 +1,389 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +import { reloadBadges } from "./index"; + +const settings = definePluginSettings({ + oneBadgePerChannel: { + type: OptionType.BOOLEAN, + default: false, + description: "Show only one badge per channel", + onChange: reloadBadges, + }, + showTextBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Text badge", + onChange: reloadBadges, + }, + showVoiceBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Voice badge", + onChange: reloadBadges, + }, + showCategoryBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Category badge", + onChange: reloadBadges, + }, + showDirectoryBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Directory badge", + onChange: reloadBadges, + }, + showAnnouncementThreadBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Announcement Thread badge", + onChange: reloadBadges, + }, + showPublicThreadBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Public Thread badge", + onChange: reloadBadges, + }, + showPrivateThreadBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Private Thread badge", + onChange: reloadBadges, + }, + showStageBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Stage badge", + onChange: reloadBadges, + }, + showAnnouncementBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Announcement badge", + onChange: reloadBadges, + }, + showForumBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Forum badge", + onChange: reloadBadges, + }, + showMediaBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Media badge", + onChange: reloadBadges, + }, + showNSFWBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show NSFW badge", + onChange: reloadBadges, + }, + showLockedBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Locked badge", + onChange: reloadBadges, + }, + showRulesBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Rules badge", + onChange: reloadBadges, + }, + showUnknownBadge: { + type: OptionType.BOOLEAN, + default: true, + description: "Show Unknown badge", + onChange: reloadBadges, + }, + + textBadgeLabel: { + type: OptionType.STRING, + default: "Text", + description: "Text badge label", + onChange: reloadBadges, + }, + voiceBadgeLabel: { + type: OptionType.STRING, + default: "Voice", + description: "Voice badge label", + onChange: reloadBadges, + }, + categoryBadgeLabel: { + type: OptionType.STRING, + default: "Category", + description: "Category badge label", + onChange: reloadBadges, + }, + announcementBadgeLabel: { + type: OptionType.STRING, + default: "News", + description: "Announcement badge label", + onChange: reloadBadges, + }, + announcementThreadBadgeLabel: { + type: OptionType.STRING, + default: "News Thread", + description: "Announcement Thread badge label", + onChange: reloadBadges, + }, + publicThreadBadgeLabel: { + type: OptionType.STRING, + default: "Thread", + description: "Public Thread badge label", + onChange: reloadBadges, + }, + privateThreadBadgeLabel: { + type: OptionType.STRING, + default: "Private Thread", + description: "Private Thread badge label", + onChange: reloadBadges, + }, + stageBadgeLabel: { + type: OptionType.STRING, + default: "Stage", + description: "Stage badge label", + onChange: reloadBadges, + }, + directoryBadgeLabel: { + type: OptionType.STRING, + default: "Directory", + description: "Directory badge label", + onChange: reloadBadges, + }, + forumBadgeLabel: { + type: OptionType.STRING, + default: "Forum", + description: "Forum badge label", + onChange: reloadBadges, + }, + mediaBadgeLabel: { + type: OptionType.STRING, + default: "Media", + description: "Media badge label", + onChange: reloadBadges, + }, + nsfwBadgeLabel: { + type: OptionType.STRING, + default: "NSFW", + description: "NSFW badge label", + onChange: reloadBadges, + }, + lockedBadgeLabel: { + type: OptionType.STRING, + default: "Locked", + description: "Locked badge label", + onChange: reloadBadges, + }, + rulesBadgeLabel: { + type: OptionType.STRING, + default: "Rules", + description: "Rules badge label", + onChange: reloadBadges, + }, + unknownBadgeLabel: { + type: OptionType.STRING, + default: "Unknown", + description: "Unknown badge label", + onChange: reloadBadges, + }, + + + textBadgeColor: { + type: OptionType.STRING, + description: "Text badge color", + onChange: reloadBadges, + }, + voiceBadgeColor: { + type: OptionType.STRING, + description: "Voice badge color", + onChange: reloadBadges, + }, + categoryBadgeColor: { + type: OptionType.STRING, + description: "Category badge color", + onChange: reloadBadges, + }, + announcementBadgeColor: { + type: OptionType.STRING, + description: "Announcement badge color", + onChange: reloadBadges, + }, + announcementThreadBadgeColor: { + type: OptionType.STRING, + description: "Announcement Thread badge color", + onChange: reloadBadges, + }, + publicThreadBadgeColor: { + type: OptionType.STRING, + description: "Public Thread badge color", + onChange: reloadBadges, + }, + privateThreadBadgeColor: { + type: OptionType.STRING, + description: "Private Thread badge color", + onChange: reloadBadges, + }, + stageBadgeColor: { + type: OptionType.STRING, + description: "Stage badge color", + onChange: reloadBadges, + }, + directoryBadgeColor: { + type: OptionType.STRING, + description: "Directory badge color", + onChange: reloadBadges, + }, + forumBadgeColor: { + type: OptionType.STRING, + description: "Forum badge color", + onChange: reloadBadges, + }, + mediaBadgeColor: { + type: OptionType.STRING, + description: "Media badge color", + onChange: reloadBadges, + }, + nsfwBadgeColor: { + type: OptionType.STRING, + description: "NSFW badge color", + onChange: reloadBadges, + }, + lockedBadgeColor: { + type: OptionType.STRING, + description: "Locked badge color", + onChange: reloadBadges, + }, + rulesBadgeColor: { + type: OptionType.STRING, + description: "Rules badge color", + onChange: reloadBadges, + }, + unknownBadgeColor: { + type: OptionType.STRING, + description: "Unknown badge color", + onChange: reloadBadges, + }, +}); + +const defaultValues = { + showTextBadge: true, + showVoiceBadge: true, + showCategoryBadge: true, + showAnnouncementBadge: true, + showAnnouncementThreadBadge: true, + showPublicThreadBadge: true, + showPrivateThreadBadge: true, + showStageBadge: true, + showDirectoryBadge: true, + showForumBadge: true, + showMediaBadge: true, + showNSFWBadge: true, + showLockedBadge: true, + showRulesBadge: true, + showUnknownBadge: true, + + channelBadges: { + text: "Text", + voice: "Voice", + category: "Category", + announcement: "News", + announcement_thread: "News Thread", + public_thread: "Thread", + private_thread: "Private Thread", + stage: "Stage", + directory: "Directory", + forum: "Forum", + media: "Media", + nsfw: "NSFW", + locked: "Locked", + rules: "Rules", + unknown: "Unknown" + }, + lockedBadgeTooltip: "This channel is locked.", + nsfwBadgeTooltip: "This channel is marked as NSFW.", +}; + +function isEnabled(type: number) { + const fromValues = settings.store; + + switch (type) { + case 0: + return fromValues.showTextBadge; + case 2: + return fromValues.showVoiceBadge; + case 4: + return fromValues.showCategoryBadge; + case 5: + return fromValues.showAnnouncementBadge; + case 10: + return fromValues.showAnnouncementThreadBadge; + case 11: + return fromValues.showPublicThreadBadge; + case 12: + return fromValues.showPrivateThreadBadge; + case 13: + return fromValues.showStageBadge; + case 14: + return fromValues.showDirectoryBadge; + case 15: + return fromValues.showForumBadge; + case 16: + return fromValues.showMediaBadge; + case 6100: + return fromValues.showNSFWBadge; + case 6101: + return fromValues.showLockedBadge; + case 6102: + return fromValues.showRulesBadge; + default: + return fromValues.showUnknownBadge; + } +} + +function returnChannelBadge(type: number) { + switch (type) { + case 0: + return { css: "text", label: settings.store.textBadgeLabel, color: settings.store.textBadgeColor }; + case 2: + return { css: "voice", label: settings.store.voiceBadgeLabel, color: settings.store.voiceBadgeColor }; + case 4: + return { css: "category", label: settings.store.categoryBadgeLabel, color: settings.store.categoryBadgeColor }; + case 5: + return { css: "announcement", label: settings.store.announcementBadgeLabel, color: settings.store.announcementBadgeColor }; + case 10: + return { css: "announcement-thread", label: settings.store.announcementThreadBadgeLabel, color: settings.store.announcementThreadBadgeColor }; + case 11: + return { css: "thread", label: settings.store.publicThreadBadgeLabel, color: settings.store.publicThreadBadgeColor }; + case 12: + return { css: "private-thread", label: settings.store.privateThreadBadgeLabel, color: settings.store.privateThreadBadgeColor }; + case 13: + return { css: "stage", label: settings.store.stageBadgeLabel, color: settings.store.stageBadgeColor }; + case 14: + return { css: "directory", label: settings.store.directoryBadgeLabel, color: settings.store.directoryBadgeColor }; + case 15: + return { css: "forum", label: settings.store.forumBadgeLabel, color: settings.store.forumBadgeColor }; + case 16: + return { css: "media", label: settings.store.mediaBadgeLabel, color: settings.store.mediaBadgeColor }; + case 6100: + return { css: "nsfw", label: settings.store.nsfwBadgeLabel, color: settings.store.nsfwBadgeColor }; + case 6101: + return { css: "locked", label: settings.store.lockedBadgeLabel, color: settings.store.lockedBadgeColor }; + case 6102: + return { css: "rules", label: settings.store.rulesBadgeLabel, color: settings.store.rulesBadgeColor }; + default: + return { css: "unknown", label: settings.store.unknownBadgeLabel, color: settings.store.unknownBadgeColor }; + } +} + +export { defaultValues, isEnabled, returnChannelBadge, settings }; diff --git a/src/equicordplugins/channelBadges/style.css b/src/equicordplugins/channelBadges/style.css new file mode 100644 index 00000000..372b328e --- /dev/null +++ b/src/equicordplugins/channelBadges/style.css @@ -0,0 +1,69 @@ +.channel-badge { + margin-left: 0.2rem; + padding: 2px 8px; + border-radius: 20px; + font-size: 12px; + font-weight: bold; + display: inline-block; + color: var(--white); + order: 2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + +.badge-container { + display: flex; + gap: 0.1rem; + align-items: center; + margin-left: 0.2rem; + order: 2; +} + +.channel-badge-text { + background-color: #5865F2; +} + +.channel-badge-voice { + background-color: #3BA55D; +} + +.channel-badge-category { + background-color: #4F545C; +} + +.channel-badge-directory { + background-color: #FAA61A; +} + +.channel-badge-thread, +.channel-badge-public-thread, +.channel-badge-announcement-thread, +.channel-badge-private-thread { + background-color: #7289DA; +} + +.channel-badge-announcement, +.channel-badge-announcement-channel { + background-color: #FAA61A; +} + +.channel-badge-rules, +.channel-badge-stage, +.channel-badge-media, +.channel-badge-forum { + background-color: #5865F2; +} + +.channel-badge-nsfw { + background-color: #E91E63; +} + +.channel-badge-locked { + background-color: #B9BBBE; +} + +.channel-badge-unknown { + background-color: #747F8D; +}