ChannelDeck working prototype
This commit is contained in:
commit
54fe9f17bf
13 changed files with 241 additions and 0 deletions
66
ChannelDeckStore.tsx
Normal file
66
ChannelDeckStore.tsx
Normal 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;
|
||||
});;
|
5
components/DeckColumn.css
Normal file
5
components/DeckColumn.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.vc-channelDeck-column {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
23
components/DeckColumn.tsx
Normal file
23
components/DeckColumn.tsx
Normal 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>;
|
||||
};
|
4
components/DeckContent.css
Normal file
4
components/DeckContent.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.vc-channelDeck-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
11
components/DeckContent.tsx
Normal file
11
components/DeckContent.tsx
Normal 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
23
components/DeckPopout.tsx
Normal 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>;
|
||||
};
|
16
components/DeckSidebar.css
Normal file
16
components/DeckSidebar.css
Normal 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%;
|
||||
}
|
20
components/DeckSidebar.tsx
Normal file
20
components/DeckSidebar.tsx
Normal 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
5
components/DeckView.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.vc-channelDeck-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
12
components/DeckView.tsx
Normal file
12
components/DeckView.tsx
Normal 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
10
components/util.ts
Normal 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
16
index.tsx
Normal 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
30
unsupportedMessage.tsx
Normal 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");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Reference in a new issue