· 2 years ago · Jul 07, 2023, 09:00 PM
1/**
2* @name PlatformIndicators
3* @displayName PlatformIndicators
4* @authorId 415849376598982656
5* @invite gvA2ree
6* @version 1.4.2
7*/
8/*@cc_on
9@if (@_jscript)
10
11 // Offer to self-install for clueless users that try to run this directly.
12 var shell = WScript.CreateObject("WScript.Shell");
13 var fs = new ActiveXObject("Scripting.FileSystemObject");
14 var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\BetterDiscord\plugins");
15 var pathSelf = WScript.ScriptFullName;
16 // Put the user at ease by addressing them in the first person
17 shell.Popup("It looks like you"ve mistakenly tried to run me directly. \n(Don"t do that!)", 0, "I"m a plugin for BetterDiscord", 0x30);
18 if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
19 shell.Popup("I"m in the correct folder already.", 0, "I"m already installed", 0x40);
20 } else if (!fs.FolderExists(pathPlugins)) {
21 shell.Popup("I can"t find the BetterDiscord plugins folder.\nAre you sure it"s even installed?", 0, "Can"t install myself", 0x10);
22 } else if (shell.Popup("Should I copy myself to BetterDiscord"s plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) {
23 fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
24 // Show the user where to put plugins in the future
25 shell.Exec("explorer " + pathPlugins);
26 shell.Popup("I"m installed!", 0, "Successfully installed", 0x40);
27 }
28 WScript.Quit();
29@else@*/
30
31module.exports = (() => {
32 const config = {
33 info: {
34 name: "PlatformIndicators",
35 authors: [
36 {
37 name: "Strencher",
38 discord_id: "415849376598982656",
39 github_username: "Strencher",
40 twitter_username: "Strencher3"
41 }
42 ],
43 version: "1.4.2",
44 description: "Adds indicators for every platform that the user is using. Source code available on the repo in the 'src' folder.",
45 github: "https://github.com/Strencher/BetterDiscordStuff/blob/master/PlatformIndicators/APlatformIndicators.plugin.js",
46 github_raw: "https://raw.githubusercontent.com/Strencher/BetterDiscordStuff/master/PlatformIndicators/APlatformIndicators.plugin.js"
47 },
48 changelog: [
49 {
50 title: "v1.4.2",
51 type: "fixed",
52 items: [
53 "Fixed indicators not showing in user popout for new usernames.",
54 ]
55 },
56 ],
57 defaultConfig: [
58 {
59 type: "switch",
60 name: "Show in MemberList",
61 note: "Shows the platform indicators in the memberlist",
62 id: "showInMemberList",
63 value: true
64 },
65 {
66 type: "switch",
67 name: "Show next to username",
68 note: "Shows the platform indicators next the username in messages.",
69 id: "showInChat",
70 value: true
71 },
72 {
73 type: "switch",
74 name: "Show in DMs List",
75 note: "Shows the platform indicators in the dm list.",
76 id: "showInDmsList",
77 value: true
78 },
79 {
80 type: "switch",
81 name: "Show next to discord tags",
82 note: "Shows the platform indicators right next to the discord tag.",
83 id: "showInTags",
84 value: true
85 },
86 {
87 type: "switch",
88 name: "Ignore Bots",
89 note: "Ignores the status of bots which is always web anyways.",
90 id: "ignoreBots",
91 value: true
92 },
93 {
94 type: "category",
95 name: "icons",
96 id: "icons",
97 settings: [
98 {
99 type: "switch",
100 name: "Web Icon",
101 note: "Show the Web icon.",
102 id: "web",
103 value: true
104 },
105 {
106 type: "switch",
107 name: "Desktop Icon",
108 note: "Show the Desktop icon.",
109 id: "desktop",
110 value: true
111 },
112 {
113 type: "switch",
114 name: "Mobile Icon",
115 note: "Show the Mobile icon.",
116 id: "mobile",
117 value: true
118 },
119 {
120 type: "switch",
121 name: "Embedded Icon",
122 note: "Show the Embedded icon.",
123 id: "embedded",
124 value: true
125 }
126 ]
127 }
128 ]
129 };
130
131 return !global.ZeresPluginLibrary ? class {
132 constructor() {
133 this._config = config;
134 }
135 getName() {return config.info.name;}
136 getAuthor() {return config.info.authors.map(a => a.name).join(", ");}
137 getDescription() {return config.info.description;}
138 getVersion() {return config.info.version;}
139 load() {
140 BdApi.showConfirmationModal("Library plugin is needed", [`The library plugin needed for ${config.info.name} is missing. Please click Download Now to install it.`], {
141 confirmText: "Download",
142 cancelText: "Cancel",
143 onConfirm: () => {
144 require("request").get("https://rauenzi.github.io/BDPluginLibrary/release/0PluginLibrary.plugin.js", async (error, response, body) => {
145 if (error)
146 return require("electron").shell.openExternal("https://betterdiscord.net/ghdl?url=https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js");
147 await new Promise(r => require("fs").writeFile(require("path").join(BdApi.Plugins.folder, "0PluginLibrary.plugin.js"), body, r));
148 });
149 }
150 });
151 }
152 start() {}
153 stop() {}
154 } : (([Plugin, Api]) => {
155 const plugin = (Plugin, Api) => {
156 const {DiscordClasses, DOMTools, Utilities, WebpackModules, PluginUtilities, ReactTools, DiscordModules: {LocaleManager: {Messages}, UserStatusStore, UserStore}} = Api;
157 const Dispatcher = WebpackModules.getByProps("dispatch", "register");
158 const LocalActivityStore = WebpackModules.getByProps("getCustomStatusActivity");
159 const Flux = Object.assign({}, WebpackModules.getByProps("Store", "connectStores"), WebpackModules.getByProps("useStateFromStores"));
160 const SessionsStore = WebpackModules.getByProps("getSessions", "_dispatchToken");
161 const friendsRowClasses = WebpackModules.getByProps("hovered", "discriminator");
162
163 const {DOM, Webpack, Webpack: {Filters}} = BdApi;
164 const [ChatHeader, NameTag, MemberListItem, DirectMessage, NewUserName, {LayerClassName = ""} = {}] = Webpack.getBulk.apply(null, [
165 Filters.byProps("replyAvatar", "sizeEmoji"),
166 Filters.byProps("bot", "nameTag"),
167 Filters.byProps("wrappedName", "nameAndDecorators"),
168 Filters.combine(Filters.byProps("wrappedName", "nameAndDecorators"), m => !m.container),
169 Filters.byProps("discrimBase", "userTagUsernameBase"),
170 Filters.byProps("LayerClassName")
171 ].map(fn => ({filter: fn})));
172
173 class StringUtils {
174 static upperFirst(string) {return string.charAt(0).toUpperCase() + string.slice(1);}
175 static getStatusText(key, status) {
176 return this.upperFirst(key) + ": " + Messages[`STATUS_${(status == "mobile" ? "mobile_online" : status).toUpperCase()}`];
177 }
178 }
179
180 const Settings = new class Settings extends Flux.Store {
181 constructor() {super(Dispatcher, {});}
182 _settings = PluginUtilities.loadSettings(config.info.name, {});
183
184 get(key, def) {
185 return this._settings[key] ?? def;
186 }
187
188 set(key, value) {
189 this._settings[key] = value;
190 this.emitChange();
191 }
192 };
193
194 const StoreWatcher = {
195 _stores: [Settings, UserStatusStore, UserStore, SessionsStore],
196 _listeners: new Set,
197 onChange(callback) {
198 this._listeners.add(callback);
199 },
200 offChange(callback) {
201 this._listeners.add(callback);
202 },
203 _alertListeners() {
204 StoreWatcher._listeners.forEach(l => l());
205 },
206 _init() {
207 this._stores.forEach(store => store.addChangeListener(this._alertListeners));
208 },
209 _stop() {
210 this._stores.forEach(store => store.addChangeListener(this._alertListeners));
211 }
212 };
213
214 const StatusColors = new Proxy({
215 dnd: "#ED4245",
216 idle: "#FAA81A",
217 online: "#3BA55D",
218 streaming: "#593695",
219 offline: "#747F8D"
220 }, {
221 get(target, key) {
222 return target[key] ?? target.offline;
223 }
224 });
225
226 const isStreaming = () => LocalActivityStore.getActivities().some(e => e.type === 1);
227
228 const getReactProps = (el, filter = _ => _) => {
229 const instance = ReactTools.getReactInstance(el);
230
231 for (let current = instance.return, i = 0; i > 10000 || current !== null; current = current?.return, i++) {
232 if (current?.pendingProps && filter(current.pendingProps)) return current.pendingProps;
233 }
234
235 return null;
236 };
237
238 // Taken from SolidJS' template function.
239 function template(html, check, isSVG) {
240 const t = document.createElement("template");
241 t.innerHTML = html;
242 let node = t.content.firstChild;
243 if (isSVG)
244 node = node.firstChild;
245 return node;
246 }
247
248 const createElement = (type, props, ...children) => {
249 if (typeof type === "function") return type({...props, children: [].concat()})
250
251 const node = document.createElement(type);
252
253 for (const key of Object.keys(props)) {
254 if (key.indexOf("on") === 0) node.addEventListener(key.slice(2).toLowerCase(), props[key]);
255 else if (key === "children") {
256 node.append(...(Array.isArray(props[key]) ? props[key] : [].concat(props[key])));
257 } else {
258 node.setAttribute(key === "className" ? "class" : key, props[key]);
259 }
260 }
261
262 if (children.length) node.append(...children);
263
264 return node;
265 };
266
267 class Tooltip {
268 containerClassName = Utilities.className("PI-tooltip", ...["tooltip", "tooltipTop", "tooltipPrimary"].map(c => DiscordClasses.Tooltips?.[c]?.value));
269 pointerClassName = DiscordClasses.Tooltips?.tooltipPointer?.value;
270 contentClassName = DiscordClasses.Tooltips?.tooltipContent?.value;
271
272 constructor(target, {text, spacing}) {
273 this.target = target;
274 this.ref = null;
275 this.text = text;
276 this.spacing = spacing;
277 this.tooltip = createElement("div", {
278 className: this.containerClassName,
279 style: "visibility: hidden;",
280 children: [
281 createElement("div", {className: this.pointerClassName, style: "left: calc(50% + 0px)"}),
282 createElement("div", {className: this.contentClassName}, text)
283 ]
284 });
285
286 target.addEventListener("mouseenter", () => {
287 this.show();
288 });
289
290 target.addEventListener("mouseleave", () => {
291 this.hide();
292 });
293
294 this.tooltip._unmount = DOM.onRemoved(target, () => this.hide());
295 }
296
297 get container() {return document.querySelector(`.${LayerClassName} ~ .${LayerClassName}`);}
298
299 checkOffset(x, y) {
300 if (y < 0) {
301 y = 0;
302 } else if (y > window.innerHeight) {
303 y = window.innerHeight;
304 }
305
306 if (x > window.innerWidth) {
307 x = window.innerWidth;
308 } else if (x < 0) {
309 x = 0;
310 }
311
312 return {x, y};
313 }
314
315 show() {
316 const tooltip = this.ref = this.tooltip.cloneNode(true);
317 this.container.appendChild(tooltip);
318
319 const targetRect = this.target.getBoundingClientRect();
320 const tooltipRect = tooltip.getBoundingClientRect();
321
322 let top = (targetRect.y - tooltipRect.height) - this.spacing;
323 let left = targetRect.x + (targetRect.width / 2) - (tooltipRect.width / 2);
324
325 const position = this.checkOffset(left, top);
326
327 tooltip.style = `top: ${position.y}px; left: ${position.x}px;`;
328 }
329
330 hide() {
331 this.ref?.remove();
332 }
333 }
334
335 class StatusIndicators {
336 constructor(target, userId, type) {
337 this.userId = userId;
338 this.type = type;
339 this.ref = null;
340 this.target = target;
341 this._destroyed = false;
342
343 target._patched = true;
344
345 this.container = createElement("div", {
346 "data-id": userId,
347 className: Utilities.className("PI-indicatorContainer", "PI-type_" + type),
348 });
349
350 this._stopObserver = DOM.onRemoved(target, () => this.unmount());
351
352 StoreWatcher.onChange(this.handleChange);
353 }
354
355 unmount() {
356 this.ref?.remove();
357 this._stopObserver?.();
358 this._destroyed = true;
359 StoreWatcher.offChange(this.handleChange);
360 this.target._patched = false;
361 }
362
363 mount() {
364 if (this._destroyed) return false;
365
366 const res = this.render();
367 if (!res) this.ref?.remove();
368 else {
369 if (this.ref) {
370 this.ref.replaceWith(res);
371 } else {
372 this.target.appendChild(res);
373 }
374
375 this.ref = res;
376 }
377 }
378
379 handleChange = () => {
380 if (this._destroyed) return false;
381
382 if (this.state && _.isEqual(this.state, this.getState())) return;
383
384 this.mount();
385 }
386
387 getState() {
388 const user = UserStore.getUser(this.userId);
389 return {
390 iconStates: Settings.get("icons", {}),
391 shouldShow: (() => {
392 const shownInArea = Settings.get("showIn" + this.type, true);
393 const isBot = Settings.get("ignoreBots", true) && (user?.bot ?? false);
394
395 return shownInArea && !isBot;
396 })(),
397 clients: (() => {
398 if (user?.id === UserStore.getCurrentUser()?.id) return SessionsStore.getSession() ? {
399 [SessionsStore.getSession().clientInfo.client]: isStreaming() ? "streaming" : SessionsStore.getSession().status
400 } : {};
401
402 return UserStatusStore.getState().clientStatuses[user?.id] ?? {};
403 })(),
404 user
405 };
406 }
407
408 render() {
409 const container = this.container.cloneNode(true);
410 const state = this.state = this.getState();
411
412 if (!Object.keys(state.clients).length || !state.shouldShow) return null;
413
414 container._unmount = this.unmount.bind(this);
415
416 container.append(...Object.entries(state.clients)
417 .filter(([key]) => (state.iconStates[key] ?? true) && Icons[key] != null)
418 .map(([key, status]) => {
419 const Icon = Icons[key];
420 return Icon({
421 text: StringUtils.getStatusText(key, status),
422 style: `color: ${StatusColors[status]};`,
423 width: 18,
424 height: 18,
425 "data-status": status
426 });
427 })
428 );
429
430 return container;
431 }
432 }
433
434 const createIcon = ((Icon, defaultProps) => props => {
435 const element = Icon.cloneNode(true);
436
437 if (props.text) {
438 new Tooltip(element, {
439 text: props.text,
440 spacing: 8
441 });
442 }
443
444 for (const prop in Object.assign({}, defaultProps, props)) {
445 if (prop === "text") continue;
446 element.setAttribute(prop, props[prop]);
447 }
448
449 return element;
450 });
451
452 const Icons = {
453 mobile: createIcon(
454 template(`<svg class="PI-icon_mobile" width="24" height="24" transform="scale(0.9)" viewBox="0 -2.5 32 44"><path fill="currentColor" d="M 2.882812 0.246094 C 1.941406 0.550781 0.519531 2.007812 0.230469 2.953125 C 0.0585938 3.542969 0 7.234375 0 17.652344 L 0 31.554688 L 0.5 32.558594 C 1.117188 33.769531 2.152344 34.5625 3.519531 34.847656 C 4.210938 35 7.078125 35.058594 12.597656 35 C 20.441406 34.941406 20.691406 34.925781 21.441406 34.527344 C 22.347656 34.054688 23.078125 33.3125 23.578125 32.386719 C 23.921875 31.761719 23.941406 30.964844 24 18.085938 C 24.039062 8.503906 24 4.167969 23.847656 3.464844 C 23.558594 2.121094 22.75 1.097656 21.519531 0.492188 L 20.5 0 L 12.039062 0.0195312 C 6.402344 0.0390625 3.328125 0.113281 2.882812 0.246094 Z M 20.382812 14.582031 L 20.382812 22.917969 L 3.652344 22.917969 L 3.652344 6.25 L 20.382812 6.25 Z M 13.789062 27.539062 C 14.5 28.296875 14.597656 29.035156 14.132812 29.925781 C 13.308594 31.496094 10.671875 31.421875 9.902344 29.8125 C 9.539062 29.054688 9.539062 28.730469 9.902344 28.011719 C 10.691406 26.535156 12.632812 26.308594 13.789062 27.539062 Z M 13.789062 27.539062 "></path></svg>`)
455 ),
456 web: createIcon(
457 template(`<svg class="PI-icon_web" width="24" height="24" viewBox="0 -2.5 28 28"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM11 19.93C7.05 19.44 4 16.08 4 12C4 11.38 4.08 10.79 4.21 10.21L9 15V16C9 17.1 9.9 18 11 18V19.93ZM17.9 17.39C17.64 16.58 16.9 16 16 16H15V13C15 12.45 14.55 12 14 12H8V10H10C10.55 10 11 9.55 11 9V7H13C14.1 7 15 6.1 15 5V4.59C17.93 5.78 20 8.65 20 12C20 14.08 19.2 15.97 17.9 17.39Z"></path></svg>`)
458 ),
459 desktop: createIcon(
460 template(`<svg class="PI-icon_desktop" width="24" height="24" viewBox="0 -2.5 28 28"><path fill="currentColor" d="M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z"></path></svg>`)
461 ),
462 embedded: createIcon(
463 template(`<svg class="PI-icon_embedded" width="24" height="24" viewBox="0 -2.5 28 28"><path fill="currentColor" d="M5.79335761,5 L18.2066424,5 C19.7805584,5 21.0868816,6.21634264 21.1990185,7.78625885 L21.8575059,17.0050826 C21.9307825,18.0309548 21.1585512,18.9219909 20.132679,18.9952675 C20.088523,18.9984215 20.0442685,19 20,19 C18.8245863,19 17.8000084,18.2000338 17.5149287,17.059715 L17,15 L7,15 L6.48507125,17.059715 C6.19999155,18.2000338 5.1754137,19 4,19 C2.97151413,19 2.13776159,18.1662475 2.13776159,17.1377616 C2.13776159,17.0934931 2.1393401,17.0492386 2.1424941,17.0050826 L2.80098151,7.78625885 C2.91311838,6.21634264 4.21944161,5 5.79335761,5 Z M14.5,10 C15.3284271,10 16,9.32842712 16,8.5 C16,7.67157288 15.3284271,7 14.5,7 C13.6715729,7 13,7.67157288 13,8.5 C13,9.32842712 13.6715729,10 14.5,10 Z M18.5,13 C19.3284271,13 20,12.3284271 20,11.5 C20,10.6715729 19.3284271,10 18.5,10 C17.6715729,10 17,10.6715729 17,11.5 C17,12.3284271 17.6715729,13 18.5,13 Z M6,9 L4,9 L4,11 L6,11 L6,13 L8,13 L8,11 L10,11 L10,9 L8,9 L8,7 L6,7 L6,9 Z"></path></svg>`)
464 )
465 };
466
467 const ElementInjections = {
468 [ChatHeader?.headerText ?? "unknown"]: elements => {
469 for (const el of elements) {
470 if (el.getElementsByClassName("PI-indicatorContainer").length || el._patched) continue;
471
472 const user = getReactProps(el.parentElement, e => e?.message)?.message?.author;
473
474 if (user) {
475 new StatusIndicators(el, user.id, "Chat").mount();
476 }
477 }
478 },
479 ...Object.fromEntries([NameTag?.nameTag, NewUserName?.userTagWithNickname]
480 .filter(Boolean)
481 .map(className => [
482 className,
483 elements => {
484 for (const el of elements) {
485 if (el.getElementsByClassName("PI-indicatorContainer").length || el._patched) continue;
486
487 const user = getReactProps(el, e => e?.user)?.user;
488 if (user) {
489 new StatusIndicators(el, user.id, "Tags").mount();
490 }
491 }
492 }
493 ])
494 ),
495 ...Object.fromEntries([MemberListItem?.nameAndDecorators, DirectMessage?.nameAndDecorators]
496 .filter(Boolean)
497 .map(className => [
498 className,
499 elements => {
500 for (const el of elements) {
501 if (el.getElementsByClassName("PI-indicatorContainer").length || el._patched) continue;
502
503 const user = getReactProps(el, e => e?.user)?.user;
504
505 if (user) {
506 new StatusIndicators(el, user.id, "MemberList").mount();
507 }
508 }
509 }
510 ])
511 )
512 };
513
514 return class PlatformIndicators extends Plugin {
515 getSettingsPanel() {
516 const panel = this.buildSettingsPanel();
517
518 // Very dirty
519 panel.addListener(() => {
520 Settings._settings = {...this.settings};
521 Settings.emitChange();
522 });
523
524 return panel.getElement();
525 }
526
527 css = /*css*/`
528 .PI-tooltip {
529 position: fixed;
530 }
531
532 .PI-indicatorContainer {
533 display: inline-flex;
534 vertical-align: bottom;
535 margin-bottom: 2px;
536 margin-left: 5px;
537 }
538
539 .PI-indicatorContainer svg {
540 margin-left: -2px;
541 }
542
543 .PI-indicatorContainer div:first-child svg {
544 margin-left: 2px;
545 }
546
547 .PI-container {
548 display: flex;
549 }
550
551 .PI-icon_mobile {
552 position: relative;
553 top: 1px;
554 }
555
556 .PI-indicatorContainer.PI-type_Chat {
557 margin-right: -6px;
558 vertical-align: top;
559 }
560
561 .${friendsRowClasses.userInfo} .PI-indicatorContainer > div {display: inline-flex;}
562
563 .${friendsRowClasses.userInfo} .${friendsRowClasses.discriminator} {
564 display: none;
565 visibility: visible;
566 }
567
568 .${friendsRowClasses.hovered} .${friendsRowClasses.discriminator} {display: block;}
569 `;
570
571 onStart() {
572 PluginUtilities.addStyle(config.info.name, this.css);
573 StoreWatcher._init();
574
575 for (const className in ElementInjections) {
576 const elements = Array.from(document.body.getElementsByClassName(className));
577
578 if (elements.length) {
579 ElementInjections[className](elements);
580 }
581 }
582 }
583
584 observer({addedNodes}) {
585 for (const added of addedNodes) {
586 if (added.nodeType === Node.TEXT_NODE) continue;
587
588 for (const className in ElementInjections) {
589 const elements = Array.from(added.getElementsByClassName(className));
590
591 if (elements.length) {
592 ElementInjections[className](elements);
593 }
594 }
595 }
596 }
597
598 onStop() {
599 StoreWatcher._stop();
600 StoreWatcher._listeners.clear();
601 PluginUtilities.removeStyle(config.info.name);
602 document.querySelectorAll(".PI-indicatorContainer").forEach(el => el._unmount?.());
603 document.querySelectorAll(".PI-tooltip").forEach(n => (n?._unmount?.(), n.remove()));
604 }
605 };
606 };
607 return plugin(Plugin, Api);
608 //@ts-ignore
609 })(global.ZeresPluginLibrary.buildPlugin(config));
610})();
611/*@end@*/
612