ChannelDeck working prototype

This commit is contained in:
Sqaaakoi 2025-02-26 03:11:08 +13:00
commit 54fe9f17bf
No known key found for this signature in database
13 changed files with 241 additions and 0 deletions

66
ChannelDeckStore.tsx Normal file
View file

@ -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<string, ChannelDeck>();
public windowKeyPrefix = "DISCORD_CHANNELDECK_DECK_";
public createDeck(deckState: Partial<Omit<ChannelDeck, "id">>) {
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) => <DeckPopout deckId={id} windowKey={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;
});;

View file

@ -0,0 +1,5 @@
.vc-channelDeck-column {
height: 100%;
display: flex;
flex-direction: column;
}

23
components/DeckColumn.tsx Normal file
View file

@ -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 <div className={cl("column")} style={{
width: column.width
}}>
<Chat
channel={channel}
guild={guild}
chatInputType={ChatInputTypes.SIDEBAR}
/>
</div>;
};

View file

@ -0,0 +1,4 @@
.vc-channelDeck-content {
display: flex;
flex-grow: 1;
}

View file

@ -0,0 +1,11 @@
import "./DeckContent.css";
import { cl, useDeck } from "./util";
import DeckColumn from "./DeckColumn";
export default function DeckContent() {
const deck = useDeck();
return <div className={cl("content")}>
{deck?.columns.map(column => <DeckColumn key={column.channelId} column={column} />)}
</div>;
};

23
components/DeckPopout.tsx Normal file
View file

@ -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 <PopoutWindow
withTitleBar
windowKey={windowKey}
title={deck?.name}
>
<DeckContext.Provider value={deck}>
<DeckView />
</DeckContext.Provider>
</PopoutWindow>;
};

View file

@ -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%;
}

View file

@ -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<SidebarTabs | null>(null);
const toggleTab = (value) => setTab((current) => current === value ? null : value);
return <div className={cl("sidebar")} data-tab={tab}>
<div className={cl("sidebar-items")}>
</div>
</div>;
}

5
components/DeckView.css Normal file
View file

@ -0,0 +1,5 @@
.vc-channelDeck-view {
display: flex;
width: 100%;
height: 100%;
}

12
components/DeckView.tsx Normal file
View file

@ -0,0 +1,12 @@
import "./DeckView.css";
import DeckSidebar from "./DeckSidebar";
import { cl } from "./util";
import DeckContent from "./DeckContent";
export default function DeckView() {
return <div className={cl("view")}>
<DeckSidebar />
<DeckContent />
</div>;
};

10
components/util.ts Normal file
View file

@ -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<ChannelDeck | undefined>(undefined));
export const useDeck = () => React.useContext(DeckContext);

16
index.tsx Normal file
View file

@ -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
});

30
unsupportedMessage.tsx Normal file
View file

@ -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: <div>
<Forms.FormText>
Your version of Vencord does not support injecting styles into popout windows.
ChannelDeck opens in popout windows, and will look broken.
<br /><br />
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.
</Forms.FormText>
<br />
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
Disable the plugin to stop this notice from appearing again.
</Forms.FormText>
</div>,
confirmText: "View PR",
cancelText: "Dismiss",
onConfirm() {
open("https://github.com/Vendicated/Vencord/pull/3153", "_blank");
}
});
});
}