diff --git a/home-manager/sajenim/features/desktop/common/discord/default.nix b/home-manager/sajenim/features/desktop/common/discord/default.nix index 7f58107..30254eb 100644 --- a/home-manager/sajenim/features/desktop/common/discord/default.nix +++ b/home-manager/sajenim/features/desktop/common/discord/default.nix @@ -17,6 +17,11 @@ source = ./config/custom.css; }; + home.file.".config/BetterDiscord/plugins/HideChannels.plugin.js" = { + enable = true; + source = ./plugins/HideChannels.plugin.js; + }; + home.persistence."/persist/home/sajenim".directories = [ ".config/discord" ".config/BetterDiscord" diff --git a/home-manager/sajenim/features/desktop/common/discord/plugins/HideChannels.plugin.js b/home-manager/sajenim/features/desktop/common/discord/plugins/HideChannels.plugin.js new file mode 100644 index 0000000..399d929 --- /dev/null +++ b/home-manager/sajenim/features/desktop/common/discord/plugins/HideChannels.plugin.js @@ -0,0 +1,351 @@ +/** + * @name Hide Channels + * @author Farcrada + * @version 2.2.11 + * @description Hide channel list from view. + * + * @invite qH6UWCwfTu + * @website https://github.com/Farcrada/DiscordPlugins + * @source https://github.com/Farcrada/DiscordPlugins/edit/master/Hide-Channels/HideChannels.plugin.js + * @updateUrl https://raw.githubusercontent.com/Farcrada/DiscordPlugins/master/Hide-Channels/HideChannels.plugin.js + */ + +/** @type {typeof import("react")} */ +const React = BdApi.React; + +const { Webpack, Webpack: { Filters }, Data, DOM, Patcher } = BdApi, + + config = { + info: { + name: "Hide Channels", + id: "HideChannels", + description: "Hide channel list from view.", + version: "2.2.11", + author: "Farcrada", + updateUrl: "https://raw.githubusercontent.com/Farcrada/DiscordPlugins/master/Hide-Channels/HideChannels.plugin.js" + }, + constants: { + //The names we need for CSS + cssStyle: "HideChannelsStyle", + hideElementsName: "hideChannelElement", + buttonID: "toggleChannels", + buttonHidden: "channelsHidden", + buttonVisible: "channelsVisible" + } + } + + +module.exports = class HideChannels { + + + load() { + try { global.ZeresPluginLibrary.PluginUpdater.checkForUpdate(config.info.name, config.info.version, config.info.updateUrl); } + catch (err) { console.error(config.info.name, "Failed to reach the ZeresPluginLibrary for Plugin Updater.", err); } + } + + start() { + try { + //React components for settings + this.WindowInfoStore = Webpack.getModule(Filters.byKeys("isFocused", "isElementFullScreen")); + + this.KeybindToCombo = Webpack.getModule(Filters.byStrings("numpad plus"), { searchExports: true }); + this.KeybindToString = Webpack.getModule(Filters.byStrings(".join(\"+\")"), { searchExports: true }); + + //The sidebar to "minimize"/hide + this.sidebarClass = Webpack.getModule(Filters.byKeys("container", "base")).sidebar; + this.headerBarClass = Webpack.getModule(Filters.byKeys("chat", "title")).title; + + //And the keybind + this.animation = this.checkKeybindLoad(Data.load(config.info.id, "animation")) ?? true; + this.keybindSetting = this.checkKeybindLoad(Data.load(config.info.id, "keybind")); + this.keybind = this.keybindSetting.split('+'); + + //Predefine for the eventlistener + this.currentlyPressed = {}; + + this.generateCSS(); + + //Render the button and we're off to the races! + const filter = f => f?.Icon && f.Title, + modules = Webpack.getModule(m => Object.values(m).some(filter), { first: false }); + for (const module of modules) { + const HeaderBar = [module, Object.keys(module).find(k => filter(module[k]))]; + this.patchTitleBar(HeaderBar); + } + } + catch (err) { + try { + console.error("Attempting to stop after starting error...", err) + this.stop(); + } + catch (err) { + console.error(config.info.name + ".stop()", err); + } + } + } + + getSettingsPanel() { + //Settings window is lazy loaded so we need to cache this after it's been loaded (i.e. open settings). + //This also allows for a (delayed) call to retrieve a way to prompt a Form + if (!this.KeybindRecorder) { + this.KeybindRecorder = Webpack.getModule(m => m.prototype?.cleanUp); + this.FormItem = Webpack.getModule(Filters.byRegex(/(case .\.LEGEND:)/), { searchExports: true }); + this.SwitchItem = Webpack.getModule(Filters.byStrings("=e.note", "checked:"), { searchExports: true }); + } + + //Return our keybind settings wrapped in a form item + return () => { + const [animation, setanimation] = React.useState(this.animation); + + return [ + React.createElement(this.SwitchItem, { + value: animation, + note: "Enable the hide animation. Useful if the animation is \"unstatisfactory\".", + onChange: (newState) => { + //Save new state + this.animation = newState; + Data.save(config.info.id, "animation", newState); + setanimation(newState); + + //Update CSS to reflect new settings. + this.generateCSS() + } + }, "Enable Hide Animation"), + React.createElement(this.FormItem, { + tag: "h5" + }, "Toggle by keybind:", + //Containing a keybind recorder. + React.createElement(this.KeybindRecorder, { + //The `keyup` and `keydown` events register the Ctrl key different + //We need to accomodate for that + defaultValue: this.KeybindToCombo(this.keybindSetting.replace("control", "ctrl")), + onChange: (e) => { + //Convert the keybind to current locale + //Once again accomodate for event differences + const keybindString = this.KeybindToString(e).toLowerCase().replace("ctrl", "control"); + + //Set the keybind and save it. + Data.save(config.info.id, "keybind", keybindString); + //And the keybindSetting + this.keybindSetting = keybindString; + this.keybind = keybindString.split('+'); + } + }))]; + } + } + + stop() { + Patcher.unpatchAll(config.info.id); + + //Our CSS + DOM.removeStyle(config.constants.cssStyle); + + //And if there are remnants of css left, + //make sure we remove the class from the sidebar to ensure visual confirmation. + let sidebar = document.querySelector(`.${this.sidebarClass}`); + if (sidebar?.classList.contains(config.constants.hideElementsName)) + sidebar.classList.remove(config.constants.hideElementsName); + } + + /** + * @param {object[]} headerBar The module and the export's name (as a string) that contains it + */ + patchTitleBar(headerBar) { + Patcher.before(config.info.id, ...headerBar, (thisObject, methodArguments, returnValue) => { + //When elements are being re-rendered we need to check if there actually is a place for us. + //Along with that we need to check if what we're adding to is an array. + if (Array.isArray(methodArguments[0]?.children)) + if (methodArguments[0].children.some?.(child => + //Make sure we're on the "original" headerbar and not that of a Voice channel's chat, or thread. + child?.props?.channel || + //The friends page + child?.type?.Header || + //The Nitro page + child?.props?.children === "Nitro" || + //Home page of certain servers. This is gonna be broken next update, calling it. + child?.props?.children?.some?.(grandChild => typeof grandChild === 'string'))) + + //Make sure our component isn't already present. + if (!methodArguments[0].children.some?.(child => child?.key === config.info.id)) + //And since we want to be on the most left of the header bar for style we unshift into the array. + methodArguments[0].children.unshift?.(React.createElement(this.hideChannelComponent, { key: config.info.id })); + + + }); + } + + /** + * React component for our button. + * @returns React element + */ + hideChannelComponent = () => { + //Only fetch the sidebar on a rerender. + const sidebarNode = document.querySelector(`.${this.sidebarClass}`), + //When a state updates, it rerenders. + [hidden, setHidden] = React.useState( + //Check on a rerender where our side bar is so we can correctly reflect this. + sidebarNode?.classList.contains(config.constants.hideElementsName)); + + /** + * Use this to make a despensable easy to use listener with React. + * @param {string} eventName The name of the event to listen for. + * @param {callback} callback Function to call when said event is triggered. + * @param {boolean} bubbling Handle bubbling or not + * @param {object} [target] The object to attach our listener to. + */ + function useListener(eventName, callback, bubbling, target = window) { + React.useEffect(() => { + //ComponentDidMount + target.addEventListener(eventName, callback, bubbling); + //ComponentWillUnmount + return () => target.removeEventListener(eventName, callback, bubbling); + }); + } + + function useWindowChangeListener(windowStore, callback) { + React.useEffect(() => { + windowStore.addChangeListener(callback); + return () => windowStore.removeChangeListener(callback); + }); + } + + /** + * @param {Node} sidebar Sidebar node we want to toggle. + * @returns The passed state in reverse. + */ + function toggleSidebar(sidebar) { + + /** + * Adds and removes our CSS to make our sidebar appear and disappear. + * @param {boolean} state State that determines the toggle. + * @returns The passed state in reverse. + */ + return state => { + //If it is showing, we need to hide it. + if (!state) + //We hide it through CSS by adding a class. + sidebar?.classList.add(config.constants.hideElementsName); + //If it is hidden, we need to show it. + else + sidebar?.classList.remove(config.constants.hideElementsName); + + return !state; + }; + } + + //Keydown event + useListener("keydown", e => { + //Since we made this an object, + //we can make new properties with `[]` + if (e?.key?.toLowerCase) + this.currentlyPressed[e.key.toLowerCase()] = true; + + //Account for bubbling + }, true); + + //Keyup event + useListener("keyup", e => { + //Check if every currentlyPessed is in our saved keybind. + if (this.keybind.every(key => this.currentlyPressed[key.toLowerCase()] === true)) + //Toggle the sidebar and rerender on toggle; change the state + setHidden(toggleSidebar(sidebarNode)); + + //Current key goes up, so... + this.currentlyPressed[e.key.toLowerCase()] = false; + + //Account for bubbling + }, true); + + //Lose focus event + useWindowChangeListener(this.WindowInfoStore, () => { + //Clear when it gets back into focus + if (this.WindowInfoStore.isFocused()) + this.currentlyPressed = {}; + }); + + //Return our element. + return React.createElement("div", { + //Styling + id: config.constants.buttonID, + //The icon + className: hidden ? config.constants.buttonHidden : config.constants.buttonVisible, + //Toggle the sidebar and rerender on toggle; change the state. + onClick: () => setHidden(toggleSidebar(sidebarNode)) + }); + } + + /** + * Checks the given keybind for validity. If not valid returns a default keybind. + * @param {String|Array.|Array.>} keybindToLoad The keybind to filter and load in. + * @param {String} [defaultKeybind] A default keybind to fall back on in case of invalidity. + * @returns Will return the keybind or return a default keybind. + */ + checkKeybindLoad(keybindToLoad, defaultKeybind = "control+h") { + defaultKeybind = defaultKeybind.toLowerCase().replace("ctrl", "control"); + + //If no keybind + if (!keybindToLoad) + return defaultKeybind; + + //Error sensitive, so just plump it into a try-catch + try { + //If it's already a string, double check it + if (typeof (keybindToLoad) === typeof (defaultKeybind)) { + keybindToLoad = keybindToLoad.toLowerCase().replace("control", "ctrl"); + //Does it go into a combo? (i.e.: is it the correct format?) + if (this.KeybindToCombo(keybindToLoad)) + return keybindToLoad.replace("ctrl", "control"); + else + return defaultKeybind; + } + else + //If it's not a string, check if it's a combo. + if (this.KeybindToString(keybindToLoad)) + return this.KeybindToString(keybindToLoad).toLowerCase().replace("ctrl", "control"); + } + catch (e) { return defaultKeybind; } + } + + generateCSS() { + //Check if there is any CSS we have already, and remove it. + DOM.removeStyle(config.constants.cssStyle); + + //Now inject our (new) CSS + DOM.addStyle(config.constants.cssStyle, ` +/* Button CSS */ +#${config.constants.buttonID} { + min-width: 24px; + height: 24px; + background-position: center !important; + background-size: 100% !important; + opacity: 0.8; + cursor: pointer; +} + +/* How the button looks */ +.theme-dark #${config.constants.buttonID}.${config.constants.buttonVisible} { + background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiIgd2lkdGg9IjE4cHgiIGhlaWdodD0iMThweCI+PHBhdGggZD0iTTE4LjQxIDE2LjU5TDEzLjgyIDEybDQuNTktNC41OUwxNyA2bC02IDYgNiA2ek02IDZoMnYxMkg2eiIvPjxwYXRoIGQ9Ik0yNCAyNEgwVjBoMjR2MjR6IiBmaWxsPSJub25lIi8+PC9zdmc+) no-repeat; +} +.theme-dark #${config.constants.buttonID}.${config.constants.buttonHidden} { + background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiIgd2lkdGg9IjE4cHgiIGhlaWdodD0iMThweCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTUuNTkgNy40MUwxMC4xOCAxMmwtNC41OSA0LjU5TDcgMThsNi02LTYtNnpNMTYgNmgydjEyaC0yeiIvPjwvc3ZnPg==) no-repeat; +} +/* In light theme */ +.theme-light #${config.constants.buttonID}.${config.constants.buttonVisible} { + background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzRmNTY2MCIgd2lkdGg9IjE4cHgiIGhlaWdodD0iMThweCI+PHBhdGggZD0iTTE4LjQxIDE2LjU5TDEzLjgyIDEybDQuNTktNC41OUwxNyA2bC02IDYgNiA2ek02IDZoMnYxMkg2eiIvPjxwYXRoIGQ9Ik0yNCAyNEgwVjBoMjR2MjR6IiBmaWxsPSJub25lIi8+PC9zdmc+) no-repeat; +} +.theme-light #${config.constants.buttonID}.${config.constants.buttonHidden} { + background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzRmNTY2MCIgd2lkdGg9IjE4cHgiIGhlaWdodD0iMThweCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTUuNTkgNy40MUwxMC4xOCAxMmwtNC41OSA0LjU5TDcgMThsNi02LTYtNnpNMTYgNmgydjEyaC0yeiIvPjwvc3ZnPg==) no-repeat; +} + +/* Attached CSS to sidebar */ +.${config.constants.hideElementsName} { + width: 0 !important; +} + +/* Set animations */ +.${this.sidebarClass} { + ${this.animation ? "transition: width 400ms ease;" : ""} + overflow: hidden; +}`); + } +}