- Floating window going below min on low resolution
- Floating window editor not usable in settings
- Confirmation modal component gone
- Fix addon toast strings
- Fix emote render
- Add folder in AddonAPIs
- Restructure FloatingWindows
This commit is contained in:
Zack Rauen 2020-07-17 22:24:20 -04:00
parent c648d37ab3
commit e5d099ebce
18 changed files with 1991 additions and 2091 deletions

View File

@ -7,7 +7,7 @@ Note: The items listed here are not in any sort of priority order.
### To Do (Remote Side)
- Dependency loading (jquery, css, config file)
- Stop depending on injector giving config
- Fix emote render
- Abstract out more UI strings
### To Do (Injector)
- Update to new windowprefs location

File diff suppressed because one or more lines are too long

2
js/main.min.js vendored

File diff suppressed because one or more lines are too long

3697
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,16 +23,16 @@
},
"homepage": "https://github.com/rauenzi/BetterDiscordApp#readme",
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.0.0",
"babel-loader": "^8.0.6",
"circular-dependency-plugin": "^5.0.2",
"gulp": "^4.0.0",
"gulp-csso": "^3.0.1",
"gulp-rename": "^1.4.0",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/register": "^7.10.5",
"babel-loader": "^8.1.0",
"circular-dependency-plugin": "^5.2.0",
"gulp": "^4.0.2",
"gulp-csso": "^4.0.1",
"gulp-rename": "^2.0.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12"
}
}

View File

@ -1,7 +1,7 @@
import Builtin from "../structs/builtin";
import {Settings, DataStore, React, WebpackModules, Events, DOMManager, Strings} from "modules";
import CSSEditor from "../ui/customcss/csseditor";
import FloatingWindowContainer from "../ui/floating/container";
import FloatingWindows from "../ui/floatingwindows";
import SettingsTitle from "../ui/settings/title";
import Utilities from "../modules/utilities";
@ -131,7 +131,7 @@ export default new class CustomCSS extends Builtin {
onChange: this.onChange.bind(this)
});
FloatingWindowContainer.open({
FloatingWindows.open({
onClose: () => {
this.isDetached = false;
},

View File

@ -37,7 +37,7 @@ export default new class EmoteModule extends Builtin {
get(id) {return super.get("emotes", "general", id);}
get MessageContentComponent() {return WebpackModules.getModule(m => m.defaultProps && m.defaultProps.hasOwnProperty("disableButtons"));}
get MessageComponent() {return WebpackModules.find(m => m.default && m.default.displayName && m.default.displayName == "Message");}
get Emotes() {return Emotes;}
get TwitchGlobal() {return Emotes.TwitchGlobal;}
@ -69,6 +69,7 @@ export default new class EmoteModule extends Builtin {
Events.on("emotes-favorite-added", this.addFavorite);
Events.on("emotes-favorite-removed", this.removeFavorite);
Events.on("setting-updated", this.onCategoryToggle);
this.patchMessageContent();
}
disabled() {
@ -113,71 +114,66 @@ export default new class EmoteModule extends Builtin {
patchMessageContent() {
if (this.cancelEmoteRender) return;
this.cancelEmoteRender = this.after(this.MessageContentComponent.prototype, "render", (thisObj, args, retVal) => {
this.after(retVal.props, "children", (t, a, returnValue) => {
if (this.categories.length == 0) return;
const markup = returnValue.props.children[1];
if (!markup.props.children) return;
const nodes = markup.props.children[1];
if (!nodes || !nodes.length) return;
for (let n = 0; n < nodes.length; n++) {
const node = nodes[n];
if (typeof(node) !== "string") continue;
const words = node.split(/([^\s]+)([\s]|$)/g);
for (let c = 0, clen = this.categories.length; c < clen; c++) {
for (let w = 0, wlen = words.length; w < wlen; w++) {
const emote = words[w];
const emoteSplit = emote.split(":");
const emoteName = emoteSplit[0];
let emoteModifier = emoteSplit[1] ? emoteSplit[1] : "";
let emoteOverride = emoteModifier.slice(0);
this.cancelEmoteRender = this.before(this.MessageComponent, "default", (thisObj, args) => {
const nodes = args[0].childrenMessageContent.props.content;
if (!nodes || !nodes.length) return;
for (let n = 0; n < nodes.length; n++) {
const node = nodes[n];
if (typeof(node) !== "string") continue;
const words = node.split(/([^\s]+)([\s]|$)/g);
for (let c = 0, clen = this.categories.length; c < clen; c++) {
for (let w = 0, wlen = words.length; w < wlen; w++) {
const emote = words[w];
const emoteSplit = emote.split(":");
const emoteName = emoteSplit[0];
let emoteModifier = emoteSplit[1] ? emoteSplit[1] : "";
let emoteOverride = emoteModifier.slice(0);
if (emoteName.length < 4 || blacklist.includes(emoteName)) continue;
if (!modifiers.includes(emoteModifier) || !Settings.get("emotes", "general", "modifiers")) emoteModifier = "";
if (!overrides.includes(emoteOverride)) emoteOverride = "";
else emoteModifier = emoteOverride;
if (emoteName.length < 4 || blacklist.includes(emoteName)) continue;
if (!modifiers.includes(emoteModifier) || !Settings.get("emotes", "general", "modifiers")) emoteModifier = "";
if (!overrides.includes(emoteOverride)) emoteOverride = "";
else emoteModifier = emoteOverride;
let current = this.categories[c];
if (emoteOverride === "twitch") {
if (Emotes.TwitchGlobal[emoteName]) current = "TwitchGlobal";
else if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "subscriber") {
if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "bttv") {
if (Emotes.BTTV[emoteName]) current = "BTTV";
}
else if (emoteOverride === "ffz") {
if (Emotes.FrankerFaceZ[emoteName]) current = "FrankerFaceZ";
}
if (!Emotes[current][emoteName]) continue;
const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
if (!results) continue;
const pre = nodes[n].substring(0, results.index + results[1].length);
const post = nodes[n].substring(results.index + results[0].length - results[2].length);
nodes[n] = pre;
const emoteComponent = DiscordModules.React.createElement(BDEmote, {name: emoteName, url: EmoteURLs[current].format({id: Emotes[current][emoteName]}), modifier: emoteModifier, isFavorite: this.isFavorite(emoteName)});
nodes.splice(n + 1, 0, post);
nodes.splice(n + 1, 0, emoteComponent);
let current = this.categories[c];
if (emoteOverride === "twitch") {
if (Emotes.TwitchGlobal[emoteName]) current = "TwitchGlobal";
else if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "subscriber") {
if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "bttv") {
if (Emotes.BTTV[emoteName]) current = "BTTV";
}
else if (emoteOverride === "ffz") {
if (Emotes.FrankerFaceZ[emoteName]) current = "FrankerFaceZ";
}
if (!Emotes[current][emoteName]) continue;
const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
if (!results) continue;
const pre = nodes[n].substring(0, results.index + results[1].length);
const post = nodes[n].substring(results.index + results[0].length - results[2].length);
nodes[n] = pre;
const emoteComponent = DiscordModules.React.createElement(BDEmote, {name: emoteName, url: EmoteURLs[current].format({id: Emotes[current][emoteName]}), modifier: emoteModifier, isFavorite: this.isFavorite(emoteName)});
nodes.splice(n + 1, 0, post);
nodes.splice(n + 1, 0, emoteComponent);
}
}
const onlyEmotes = nodes.every(r => {
if (typeof(r) == "string" && r.replace(/\s*/, "") == "") return true;
else if (r.type && r.type.name == "BDEmote") return true;
else if (r.props && r.props.children && r.props.children.props && r.props.children.props.emojiName) return true;
return false;
});
if (!onlyEmotes) return;
for (const node of nodes) {
if (typeof(node) != "object") continue;
if (node.type.name == "BDEmote") node.props.jumboable = true;
else if (node.props && node.props.children && node.props.children.props && node.props.children.props.emojiName) node.props.children.props.jumboable = true;
}
}
const onlyEmotes = nodes.every(r => {
if (typeof(r) == "string" && r.replace(/\s*/, "") == "") return true;
else if (r.type && r.type.name == "BDEmote") return true;
else if (r.props && r.props.children && r.props.children.props && r.props.children.props.emojiName) return true;
return false;
});
if (!onlyEmotes) return;
for (const node of nodes) {
if (typeof(node) != "object") continue;
if (node.type.name == "BDEmote") node.props.jumboable = true;
else if (node.props && node.props.children && node.props.children.props && node.props.children.props.emojiName) node.props.children.props.jumboable = true;
}
});
}

View File

@ -209,6 +209,10 @@ export default {
deleteAddon: "Delete",
confirmDelete: "Are you sure you want to delete {{name}}?",
confirmationText: "You have unsaved changes to {{name}}. Closing this window will lose all those changes.",
enabled: "{{name}} has been enabled.",
disabled: "{{name}} has been disabled.",
unknownAuthor: "Unknown Author",
noDescription: "Description not provided."
},
Emotes: {
loading: "Loading emotes in the background do not reload.",

View File

@ -10,7 +10,7 @@ import DiscordModules from "./discordmodules";
import Strings from "./strings";
import AddonEditor from "../ui/misc/addoneditor";
import FloatingWindowContainer from "../ui/floating/container";
import FloatingWindows from "../ui/floatingwindows";
const React = DiscordModules.React;
@ -82,21 +82,22 @@ export default class AddonManager {
Logger.log(this.name, `Starting to watch ${this.prefix} addons.`);
this.watcher = fs.watch(this.addonFolder, {persistent: false}, async (eventType, filename) => {
if (!eventType || !filename || !filename.endsWith(this.extension)) return;
await new Promise(r => setTimeout(r, 50));
try {fs.statSync(path.resolve(this.addonFolder, filename));}
await new Promise(r => setTimeout(r, 100));
try {
const stats = fs.statSync(path.resolve(this.addonFolder, filename));
if (!stats.isFile()) return;
if (!stats || !stats.mtime || !stats.mtime.getTime()) return;
if (typeof(stats.mtime.getTime()) !== "number") return;
if (this.timeCache[filename] == stats.mtime.getTime()) return;
this.timeCache[filename] = stats.mtime.getTime();
if (eventType == "rename") this.loadAddon(filename, true);
if (eventType == "change") this.reloadAddon(filename, true);
}
catch (err) {
if (err.code !== "ENOENT") return;
delete this.timeCache[filename];
this.unloadAddon(filename, true);
}
if (!fs.statSync(path.resolve(this.addonFolder, filename)).isFile()) return;
const stats = fs.statSync(path.resolve(this.addonFolder, filename));
if (!stats || !stats.mtime || !stats.mtime.getTime()) return;
if (typeof(stats.mtime.getTime()) !== "number") return;
if (this.timeCache[filename] == stats.mtime.getTime()) return;
this.timeCache[filename] = stats.mtime.getTime();
if (eventType == "rename") this.loadAddon(filename, true);
if (eventType == "change") this.reloadAddon(filename, true);
});
}
@ -160,6 +161,9 @@ export default class AddonManager {
fileContent = stripBOM(fileContent);
const stats = fs.statSync(filename);
const meta = self.extractMeta(fileContent);
if (!meta.author) meta.author = Strings.Addons.unknownAuthor;
if (!meta.version) meta.version = "???";
if (!meta.description) meta.description = Strings.Addons.noDescription;
meta.id = meta.name;
meta.filename = path.basename(filename);
meta.added = stats.atimeMs;
@ -301,7 +305,7 @@ export default class AddonManager {
language: this.language
});
FloatingWindowContainer.open({
FloatingWindows.open({
onClose: () => {
this.isDetached = false;
},

View File

@ -249,7 +249,7 @@ BdApi.setBDData = function(key, data) {
};
const makeAddonAPI = (manager) => new class AddonAPI {
get folder() {return manager.folder;}
get folder() {return manager.addonFolder;}
isEnabled(idOrFile) {return manager.isEnabled(idOrFile);}
enable(idOrAddon) {return manager.enableAddon(idOrAddon);}
disable(idOrAddon) {return manager.disableAddon(idOrAddon);}

View File

@ -112,7 +112,7 @@ export default new class PluginManager extends AddonManager {
try {
plugin.start();
this.emit("started", addon.id);
Toasts.show(`${addon.name} v${addon.version} has started.`);
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
}
catch (err) {
this.state[addon.id] = false;
@ -129,7 +129,7 @@ export default new class PluginManager extends AddonManager {
try {
plugin.stop();
this.emit("stopped", addon.id);
Toasts.show(`${addon.name} v${addon.version} has stopped.`);
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
}
catch (err) {
this.state[addon.id] = false;

View File

@ -66,13 +66,13 @@ export default new class ThemeManager extends AddonManager {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
if (!addon) return;
DOMManager.injectTheme(addon.id, addon.css);
Toasts.show(`${addon.name} v${addon.version} has been applied.`);
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
}
removeTheme(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
if (!addon) return;
DOMManager.removeTheme(addon.id);
Toasts.show(`${addon.name} v${addon.version} has been removed.`);
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
}
};

View File

@ -84,6 +84,10 @@ export default class BuiltinModule {
Logger.stacktrace(this.name, message, error);
}
before(object, func, callback) {
return Patcher.before(this.name, object, func, callback);
}
after(object, func, callback) {
return Patcher.after(this.name, object, func, callback);
}

View File

@ -1,4 +1,4 @@
import {React, ReactDOM, DOM, WebpackModules} from "modules";
import {React, DOM} from "modules";
import FloatingWindow from "./window";
@ -10,7 +10,7 @@ class FloatingWindowContainer extends React.Component {
}
get minY() {
const appContainer = DOM.query(`#app-mount > div[class*="app-"`);
const appContainer = DOM.query(`#app-mount > div[class*="app-"]`);
if (appContainer) return appContainer.offsetTop;
return 0;
}
@ -48,10 +48,4 @@ class FloatingWindowContainer extends React.Component {
}
}
const containerRef = React.createRef();
const container = <FloatingWindowContainer ref={containerRef} />;
const wrapped = React.createElement(WebpackModules.getByProps("AppReferencePositionLayer").AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-yqaFcK")]}, container);
const div = DOM.createElement(`<div id="floating-windows-layer">`);
DOM.query("#app-mount").append(div);
ReactDOM.render(wrapped, div);
export default containerRef.current;
export default FloatingWindowContainer;

View File

@ -121,17 +121,6 @@ export default class FloatingWindow extends React.Component {
</div>;
}
async close() {
let shouldClose = true;
const confirmClose = typeof(this.props.confirmClose) == "function" ? this.props.confirmClose() : this.props.confirmClose;
if (confirmClose) {
this.setState({modalOpen: true});
shouldClose = await this.confirmClose();
this.setState({modalOpen: false});
}
if (this.props.close && shouldClose) this.props.close();
}
maximize() {
this.window.current.style.width = "100%";
this.window.current.style.height = "100%";
@ -145,8 +134,35 @@ export default class FloatingWindow extends React.Component {
const right = left + width;
const bottom = top + height;
// Prevent expanding off the bottom and right and readjust position
if (bottom > this.maxY) this.window.current.style.top = (this.maxY - height) + "px";
if (right > this.maxX) this.window.current.style.left = (this.maxX - width) + "px";
const newLeft = parseInt(this.window.current.style.left);
const newTop = parseInt(this.window.current.style.top);
// For small screens it's possible this pushes us off the other direction... we need to readjust size
if (newTop < this.minY) {
const difference = this.minY - newTop;
this.window.current.style.top = this.minY + "px";
this.window.current.style.height = (height - difference) + "px";
}
if (newLeft < this.minX) {
const difference = this.minX - newLeft;
this.window.current.style.left = this.minX + "px";
this.window.current.style.height = (width - difference) + "px";
}
}
async close() {
let shouldClose = true;
const confirmClose = typeof(this.props.confirmClose) == "function" ? this.props.confirmClose() : this.props.confirmClose;
if (confirmClose) {
this.setState({modalOpen: true});
shouldClose = await this.confirmClose();
this.setState({modalOpen: false});
}
if (this.props.close && shouldClose) this.props.close();
}
confirmClose() {

19
src/ui/floatingwindows.js Normal file
View File

@ -0,0 +1,19 @@
import {WebpackModules, React, ReactDOM, DOM, DOMManager} from "modules";
import FloatingWindowContainer from "./floating/container";
export default class FloatingWindows {
static initialize() {
const containerRef = React.createRef();
const container = <FloatingWindowContainer ref={containerRef} />;
const wrapped = React.createElement(WebpackModules.getByProps("AppReferencePositionLayer").AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-yqaFcK")]}, container);
const div = DOM.createElement(`<div id="floating-windows-layer">`);
DOMManager.bdBody.append(div);
ReactDOM.render(wrapped, div);
this.ref = containerRef;
}
static open(window) {
if (!this.ref) this.initialize();
return this.ref.current.open(window);
}
}

View File

@ -6,10 +6,11 @@ export default class Modals {
static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");}
static get ModalActions() {return WebpackModules.getByProps("openModal", "updateModal");}
static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get AlertModal() {return WebpackModules.getByPrototypes("handleCancel", "handleSubmit", "handleMinorConfirm");}
static get TextElement() {return WebpackModules.getByProps("Sizes", "Weights");}
static get ConfirmationModal() {return WebpackModules.getModule(m => m.defaultProps && m.key && m.key() == "confirm-modal");}
static get ConfirmationModal() {return WebpackModules.findByDisplayName("ConfirmModal");}
static get Markdown() {return WebpackModules.findByDisplayName("Markdown");}
static default(title, content) {
@ -64,9 +65,9 @@ export default class Modals {
static showConfirmationModal(title, content, options = {}) {
const Markdown = this.Markdown;
const ConfirmationModal = this.ConfirmationModal;
const ModalStack = this.ModalStack;
const ModalActions = this.ModalActions;
if (content instanceof FormattableString) content = content.toString();
if (!this.ModalStack || !this.ConfirmationModal || !this.Markdown) return this.default(title, content);
if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) return this.default(title, content);
const emptyFunction = () => {};
const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options;
@ -74,15 +75,16 @@ export default class Modals {
if (!Array.isArray(content)) content = [content];
content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c);
return ModalStack.push(ConfirmationModal, {
header: title,
children: content,
red: danger,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel
}, key);
return ModalActions.openModal(props => {
return React.createElement(ConfirmationModal, Object.assign({
header: title,
red: danger,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel
}, props), content);
}, {modalKey: key});
}
static showAddonErrors({plugins: pluginErrors = [], themes: themeErrors = []}) {

View File

@ -1,4 +1,4 @@
import {WebpackModules, Settings} from "modules";
import {WebpackModules, Settings, DOMManager} from "modules";
const channelsClass = WebpackModules.getByProps("sidebar", "hasNotice").sidebar.split(" ")[0];
const membersWrapClass = WebpackModules.getByProps("membersWrap").membersWrap.split(" ")[0];
@ -65,6 +65,6 @@ export default class Toasts {
toastWrapper.style.setProperty("left", left + "px");
toastWrapper.style.setProperty("width", width + "px");
toastWrapper.style.setProperty("bottom", bottom + "px");
document.querySelector("#app-mount").appendChild(toastWrapper);
DOMManager.bdBody.appendChild(toastWrapper);
}
}