commit 54fe9f17bf84728cf301f171ce378a9a15e81f96 Author: Sqaaakoi Date: Wed Feb 26 03:11:08 2025 +1300 ChannelDeck working prototype diff --git a/ChannelDeckStore.tsx b/ChannelDeckStore.tsx new file mode 100644 index 0000000..b4b10f1 --- /dev/null +++ b/ChannelDeckStore.tsx @@ -0,0 +1,66 @@ +import { proxyLazyWebpack, findByProps, findByPropsLazy } from "@webpack"; +import { Flux, FluxDispatcher, PopoutActions, PopoutWindowStore, SnowflakeUtils } from "@webpack/common"; +import DeckPopout from "./components/DeckPopout"; + +export interface ChannelDeck { + id: string; + name: string; + color?: number; + columns: DeckColumn[]; + open: boolean; +} + +export interface DeckColumn { + channelId: string, + width: `${number}px` | `${number}%`; +} + +// Don't wanna run before Flux and Dispatcher are ready! +export const ChannelDeckStore = proxyLazyWebpack(() => { + class ChannelDeckStore extends Flux.Store { + + public _decks = new Map(); + + public windowKeyPrefix = "DISCORD_CHANNELDECK_DECK_"; + + public createDeck(deckState: Partial>) { + const deck = { + id: SnowflakeUtils.fromTimestamp(Date.now()), + name: "", + columns: [], + open: false, + ...deckState + }; + this._decks.set(deck.id, deck); + this.updateDeck(deck.id); + return deck; + } + + public getDeck(id: string) { + return this._decks.get(id); + } + + public updateDeck(id: string) { + const deck = this.getDeck(id); + if (deck?.open && !PopoutWindowStore.getWindowKeys().includes(this.windowKeyPrefix + id)) + PopoutActions.open( + this.windowKeyPrefix + id, + (key) => , { + defaultWidth: 1200, + defaultHeight: 960 + }); + if (!deck?.open && PopoutWindowStore.getWindowKeys().includes(this.windowKeyPrefix + id)) + this.getDeckWindow(id).close(); + this.emitChange(); + }; + + public getDeckWindow(id: string) { + return PopoutWindowStore.getWindow(this.windowKeyPrefix + id); + } + } + + const store = new ChannelDeckStore(FluxDispatcher, { + }); + + return store; +});; diff --git a/components/DeckColumn.css b/components/DeckColumn.css new file mode 100644 index 0000000..9b74f0c --- /dev/null +++ b/components/DeckColumn.css @@ -0,0 +1,5 @@ +.vc-channelDeck-column { + height: 100%; + display: flex; + flex-direction: column; +} diff --git a/components/DeckColumn.tsx b/components/DeckColumn.tsx new file mode 100644 index 0000000..322bf7b --- /dev/null +++ b/components/DeckColumn.tsx @@ -0,0 +1,23 @@ +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import "./DeckColumn.css"; + +import { cl } from "./util"; +import { DeckColumn } from "../ChannelDeckStore"; +import { ChannelStore, GuildStore } from "@webpack/common"; + +const Chat = findComponentByCodeLazy("filterAfterTimestamp:", "chatInputType"); +const ChatInputTypes = findByPropsLazy("FORM", "NORMAL"); + +export default function DeckColumn({ column }: { column: DeckColumn; }) { + const channel = ChannelStore.getChannel(column.channelId); + const guild = GuildStore.getGuild(channel.guild_id); + return
+ +
; +}; diff --git a/components/DeckContent.css b/components/DeckContent.css new file mode 100644 index 0000000..2c5ba65 --- /dev/null +++ b/components/DeckContent.css @@ -0,0 +1,4 @@ +.vc-channelDeck-content { + display: flex; + flex-grow: 1; +} diff --git a/components/DeckContent.tsx b/components/DeckContent.tsx new file mode 100644 index 0000000..c362305 --- /dev/null +++ b/components/DeckContent.tsx @@ -0,0 +1,11 @@ +import "./DeckContent.css"; + +import { cl, useDeck } from "./util"; +import DeckColumn from "./DeckColumn"; + +export default function DeckContent() { + const deck = useDeck(); + return
+ {deck?.columns.map(column => )} +
; +}; diff --git a/components/DeckPopout.tsx b/components/DeckPopout.tsx new file mode 100644 index 0000000..35dd5f1 --- /dev/null +++ b/components/DeckPopout.tsx @@ -0,0 +1,23 @@ +import { findComponentByCodeLazy } from "@webpack"; +import { useStateFromStores } from "@webpack/common"; +import { ChannelDeckStore } from "../ChannelDeckStore"; +import DeckView from "./DeckView"; +import { DeckContext } from "./util"; + +const PopoutWindow = findComponentByCodeLazy("Missing guestWindow reference"); + +export default function DeckPopout({ deckId, windowKey }: { deckId: string; windowKey?: string; }) { + // Copy from an unexported function of the one they use in the experiment + // right click a channel and search withTitleBar:!0,windowKey + const deck = useStateFromStores([ChannelDeckStore], () => ChannelDeckStore.getDeck(deckId)); + + return + + + + ; +}; diff --git a/components/DeckSidebar.css b/components/DeckSidebar.css new file mode 100644 index 0000000..a5c0657 --- /dev/null +++ b/components/DeckSidebar.css @@ -0,0 +1,16 @@ +.vc-channelDeck-sidebar { + display: flex; + width: 40px; + height: 100%; + background: var(--bg-overlay-5, var(--background-secondary)); +} + +.vc-channelDeck-sidebar[data-tab] { + width: 320px; +} + +.vc-channelDeck-sidebar-items { + display: flex; + width: 40px; + height: 100%; +} diff --git a/components/DeckSidebar.tsx b/components/DeckSidebar.tsx new file mode 100644 index 0000000..6a22960 --- /dev/null +++ b/components/DeckSidebar.tsx @@ -0,0 +1,20 @@ +import { useState } from "@webpack/common"; +import "./DeckSidebar.css"; + +import { cl } from "./util"; + +enum SidebarTabs { + ADD_CHANNEL = "add_channel", + DECKS = "decks", + CONFIGURE = "configure" +} + +export default function DeckSidebar() { + const [tab, setTab] = useState(null); + const toggleTab = (value) => setTab((current) => current === value ? null : value); + return
+
+ +
+
; +} diff --git a/components/DeckView.css b/components/DeckView.css new file mode 100644 index 0000000..3d1569c --- /dev/null +++ b/components/DeckView.css @@ -0,0 +1,5 @@ +.vc-channelDeck-view { + display: flex; + width: 100%; + height: 100%; +} diff --git a/components/DeckView.tsx b/components/DeckView.tsx new file mode 100644 index 0000000..ae4a683 --- /dev/null +++ b/components/DeckView.tsx @@ -0,0 +1,12 @@ +import "./DeckView.css"; + +import DeckSidebar from "./DeckSidebar"; +import { cl } from "./util"; +import DeckContent from "./DeckContent"; + +export default function DeckView() { + return
+ + +
; +}; diff --git a/components/util.ts b/components/util.ts new file mode 100644 index 0000000..55314d5 --- /dev/null +++ b/components/util.ts @@ -0,0 +1,10 @@ +import { classNameFactory } from "@api/Styles"; +import { ChannelDeck } from "../ChannelDeckStore"; +import { React } from "@webpack/common"; +import { proxyLazy } from "@utils/index"; + +export const cl = classNameFactory("vc-channelDeck-"); + +export const DeckContext = proxyLazy(() => React.createContext(undefined)); + +export const useDeck = () => React.useContext(DeckContext); diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..ead16ac --- /dev/null +++ b/index.tsx @@ -0,0 +1,16 @@ +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import showUnsupportedMessage from "./unsupportedMessage"; +import { ChannelDeckStore } from "./ChannelDeckStore"; + +export default definePlugin({ + name: "ChannelDeck", + description: 'Multi channel "deck" popout windows', + authors: [Devs.Sqaaakoi], + + start() { + if (!Vencord?.Api?.Styles?.createStyle) return showUnsupportedMessage(); + }, + + ChannelDeckStore +}); diff --git a/unsupportedMessage.tsx b/unsupportedMessage.tsx new file mode 100644 index 0000000..a7e9d02 --- /dev/null +++ b/unsupportedMessage.tsx @@ -0,0 +1,30 @@ +import { showNotice } from "@api/Notices"; +import { Alerts, Forms } from "@webpack/common"; + +export default function showUnsupportedMessage() { + showNotice("ChannelDeck requires Unified Styles API", "Details", () => { + Alerts.show({ + title: "ChannelDeck", + body:
+ + Your version of Vencord does not support injecting styles into popout windows. + ChannelDeck opens in popout windows, and will look broken. +

+ You can resolve this issue by adding the Unified Styles API patch to your Vencord installation. + You will need to manually merge the PR into your current Vencord branch. + Detailed instructions are not documented here. + If you cannot do this, wait for the API to be officially supported in Vencord. +
+
+ + Disable the plugin to stop this notice from appearing again. + +
, + confirmText: "View PR", + cancelText: "Dismiss", + onConfirm() { + open("https://github.com/Vendicated/Vencord/pull/3153", "_blank"); + } + }); + }); +}