Files
arindOS/dotfiles/.config/BetterDiscord/plugins/WiderUserArea.plugin.js
T

289 lines
12 KiB
JavaScript

/**
* @name WiderUserArea
* @version 0.2.5
* @description A BetterDiscord plugin that expands your user area into the server list, compatible with most themes
* @author TheLazySquid
* @authorId 619261917352951815
* @website https://github.com/TheLazySquid/DiscordWiderUserArea
* @source https://github.com/TheLazySquid/DiscordWiderUserArea/blob/main/build/WiderUserArea.plugin.js
*/
module.exports = class {
constructor() {
'use strict';
var styles = ":root {\r\n --user-area-bottom: 0;\r\n --user-area-left: 0;\r\n}\r\n\r\nnav[class*=\"guilds_\"] {\r\n height: var(--sidebar-height);\r\n}\r\n\r\n[class*=\"sidebar_\"] {\r\n height: calc(var(--sidebar-height) - var(--notices-height));\r\n}\r\n\r\n/* user area */\r\nsection[class*=\"panels_\"] {\r\n position: fixed;\r\n overflow: hidden;\r\n bottom: var(--user-area-bottom);\r\n left: var(--user-area-left);\r\n width: var(--user-area-width);\r\n}\r\n\r\nsection[class*=\"panels_\"] [class*=\"avatarWrapper_\"] {\r\n flex-grow: 1;\r\n}\r\n\r\n/* prevent the user info popout from clipping */\r\n[class*=\"accountProfilePopoutWrapper\"] {\r\n left: 0;\r\n}";
const userAreaSelector = 'section[class*="panels_"]';
const serverListSelector = 'nav[class*="guilds_"]';
const containerSelector = `div:has(> ${serverListSelector})`;
const layerSelector = `[class*="baseLayer"]`;
const channelsSelector = '[class*="sidebar_"] > nav';
const scaleRegex = /scale\((.*)\)/;
const UAButtonsSelector = 'div[class*="avatarWrapper_"] + div';
const firstServerListSelector = `${serverListSelector}:first-child`;
const secondServerListSelector = `${serverListSelector}:nth-child(2)`;
function scaleDOMRect(rect, scale, scaleCenterX, scaleCenterY) {
// Calculate the distance of the rect from the scale center
const distX = rect.x - scaleCenterX;
const distY = rect.y - scaleCenterY;
// Scale the distances
const newDistX = distX * scale;
const newDistY = distY * scale;
// Calculate the new position of the rect
const newX = scaleCenterX + newDistX;
const newY = scaleCenterY + newDistY;
// Scale the dimensions of the rect
const newWidth = rect.width * scale;
const newHeight = rect.height * scale;
// Create a new DOMRect with the new dimensions and position
const newRect = new DOMRect(newX, newY, newWidth, newHeight);
return newRect;
}
const createCallbackHandler = (callbackName) => {
const fullName = callbackName + "Callbacks";
this[fullName] = [];
this[callbackName] = () => {
for (let i = 0; i < this[fullName].length; i++) {
this[fullName][i].callback();
}
};
return (callback, once, id) => {
let object = { callback };
const delCallback = () => {
this[fullName].splice(this[fullName].indexOf(object), 1);
};
// if once is true delete it after use
if (once === true) {
object.callback = () => {
callback();
delCallback();
};
}
if (id) {
object.id = id;
for (let i = 0; i < this[fullName].length; i++) {
if (this[fullName][i].id === id) {
this[fullName][i] = object;
return delCallback;
}
}
}
this[fullName].push(object);
return delCallback;
};
};
/**
* Takes a callback and fires it when the plugin is started
* @param callback - The callback to be fired
* @param once - If true, the callback will be deleted after use
* @param id - The id of the callback - if it already exists, it will be replaced
* @returns A function to delete the callback
*/
const onStart = createCallbackHandler("start");
/**
* Takes a callback and fires it when the plugin is stopped
* @param callback - The callback to be fired
* @param once - If true, the callback will be deleted after use
* @param id - The id of the callback - if it already exists, it will be replaced
* @returns A function to delete the callback
*/
const onStop = createCallbackHandler("stop");
/**
* Takes a callback and fires it when the user navigates
* @param callback - The callback to be fired
* @param once - If true, the callback will be deleted after use
* @param id - The id of the callback - if it already exists, it will be replaced
* @returns A function to delete the callback
*/
const onSwitch = createCallbackHandler("onSwitch");
/**
* Watches for an element with a given selector to be added to the DOM
* @param selector The CSS selector to watch
* @param callback The callback to run whenever the matching element is added to the DOM
* @returns A function to stop watching
*/
function watchElement(selector, callback) {
let observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.addedNodes.length) {
for (let node of mutation.addedNodes) {
if (!(node instanceof HTMLElement))
continue;
if (node.matches && node.matches(selector)) {
callback(node);
}
if (node.querySelectorAll) {
for (let element of node.querySelectorAll(selector)) {
callback(element);
}
}
}
}
}
});
let startDispose = onStart(() => {
observer.observe(document.body, { childList: true, subtree: true });
for (let element of document.querySelectorAll(selector)) {
callback(element);
}
});
let stopDispose = onStop(() => {
observer.disconnect();
});
return () => {
observer.disconnect();
startDispose();
stopDispose();
};
}
// @ts-ignore
let baseChannelHeight = 0;
let baseChannelWidth = 0;
let varsSet = new Set();
const recalcDebounce = BdApi.Utils.debounce(() => {
const userArea = document.querySelector(userAreaSelector);
if (userArea)
userAreaFound(userArea);
}, 150);
watchElement(userAreaSelector, userAreaFound);
let userAreaObserver = new ResizeObserver(entries => {
for (let entry of entries) {
updateVar('--sidebar-height', `${baseChannelHeight - entry.contentRect.height}px`);
}
});
let channelsRect;
let secondChannelsRect;
let channelsObserver = new ResizeObserver(entries => {
for (let entry of entries) {
channelsRect = entry.contentRect;
// hide everything but the profile picture if the channel list is hidden
let btns = document.querySelector(UAButtonsSelector);
if (entry.contentRect.width === 0) {
if (btns)
btns.style.display = 'none';
}
else {
if (btns)
btns.style.display = '';
}
let newWidth = entry.contentRect.right - baseChannelWidth;
if (secondChannelsRect)
newWidth += secondChannelsRect.width;
updateVar('--user-area-width', `${newWidth}px`);
}
});
watchElement(channelsSelector, (element) => {
channelsObserver.disconnect();
channelsObserver.observe(element);
});
let secondServerListObserver = new ResizeObserver((entries) => {
if (!channelsRect)
return;
for (let entry of entries) {
secondChannelsRect = entry.contentRect;
let width = entry.contentRect.width;
updateVar('--user-area-width', `${channelsRect.right - baseChannelWidth + width}px`);
}
});
watchElement(secondServerListSelector, (element) => {
secondServerListObserver.disconnect();
secondServerListObserver.observe(element);
});
let themesObserver = new MutationObserver(async () => {
// wait a bit for it to apply
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('theme changed');
// recalculate the user area
const userArea = document.querySelector(userAreaSelector);
if (userArea)
userAreaFound(userArea);
});
let noticesHeight = 0;
let noticesObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
noticesHeight = entry.contentRect.height;
updateVar('--notices-height', `${entry.contentRect.height}px`);
}
});
watchElement("#bd-notices", (el) => {
noticesObserver.disconnect();
noticesObserver.observe(el);
});
function updateVar(name, value) {
BdApi.DOM.removeStyle(`wua-${name}`);
BdApi.DOM.addStyle(`wua-${name}`, `:root { ${name}: ${value} !important; }`);
varsSet.add(name);
}
onSwitch(() => {
// hacky fix for betteranimations sometimes giving a parent the "perspective" style breaking the fixed positioning
setTimeout(() => {
const userArea = document.querySelector(userAreaSelector);
let parent = userArea?.parentElement;
while (parent) {
if (parent.style.perspective) {
parent.style.perspective = '';
break;
}
parent = parent.parentElement;
}
userAreaFound(userArea);
}, 5100);
});
function userAreaFound(element) {
// remove the old style if it exists
BdApi.DOM.removeStyle('wua-styles');
const layer = document.querySelector(layerSelector);
const container = document.querySelector(containerSelector);
const serverList = document.querySelector(firstServerListSelector);
const channels = document.querySelector(channelsSelector);
const secondServerList = document.querySelector(secondServerListSelector);
const layerScale = 1 / parseFloat(scaleRegex.exec(layer.style.transform)?.[1] ?? '1');
const layerRect = layer.getBoundingClientRect();
const centerX = layerRect.left + layerRect.width / 2;
const centerY = layerRect.top + layerRect.height / 2;
// this scaling is neccesary for compatibility with betterAnimations,
// which sometimes scales the layer when in a settings menu, which is where theme switches would be.
const rect = scaleDOMRect(element.getBoundingClientRect(), layerScale, centerX, centerY);
const containerRect = scaleDOMRect(container.getBoundingClientRect(), layerScale, centerX, centerY);
const serverListRect = scaleDOMRect(serverList.getBoundingClientRect(), layerScale, centerX, centerY);
const channelsRect = scaleDOMRect(channels.getBoundingClientRect(), layerScale, centerX, centerY);
// figure out how far from the bottom of the screen the user area is
const bottom = containerRect.bottom - rect.bottom;
baseChannelHeight = channelsRect.height + rect.height;
baseChannelWidth = serverListRect.left - channelsRect.left;
if (secondServerList) {
const secondServerListRect = scaleDOMRect(secondServerList.getBoundingClientRect(), layerScale, centerX, centerY);
baseChannelWidth += secondServerListRect.width;
}
// add the new style
BdApi.DOM.addStyle('wua-styles', styles);
updateVar('--sidebar-height', `${channelsRect.height + noticesHeight}px`);
updateVar('--user-area-width', `${channelsRect.right - serverListRect.left}px`);
updateVar('--user-area-left', `${serverListRect.left}px`);
updateVar('--user-area-bottom', `${bottom}px`);
userAreaObserver.observe(element);
}
onStart(() => {
window.addEventListener('resize', recalcDebounce);
updateVar('--notices-height', '0px');
themesObserver.observe(document.querySelector("bd-themes"), { childList: true, subtree: true });
});
onStop(() => {
userAreaObserver.disconnect();
channelsObserver.disconnect();
themesObserver.disconnect();
secondServerListObserver.disconnect();
noticesObserver.disconnect();
BdApi.DOM.removeStyle('wua-styles');
for (let varName of varsSet) {
BdApi.DOM.removeStyle(`wua-${varName}`);
}
window.removeEventListener('resize', recalcDebounce);
});
}
}