cleanup + real collections

This commit is contained in:
Zack Rauen 2019-06-09 16:30:33 -04:00
parent 6d0d3ddd28
commit 869c4c5b71
28 changed files with 393 additions and 591 deletions

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,7 @@ export default new class ClassNormalizer extends Builtin {
this.patchClassModules(WebpackModules.getModules(this.moduleFilter.bind(this))); this.patchClassModules(WebpackModules.getModules(this.moduleFilter.bind(this)));
this.normalizeElement(document.querySelector("#app-mount")); this.normalizeElement(document.querySelector("#app-mount"));
this.hasPatched = true; this.hasPatched = true;
this.patchDOMMethods();
} }
disabled() { disabled() {
@ -110,5 +111,13 @@ export default new class ClassNormalizer extends Builtin {
} }
element.classList.remove(...toRemove); element.classList.remove(...toRemove);
} }
patchDOMMethods() {
const contains = DOMTokenList.prototype.contains;
DOMTokenList.prototype.contains = function(token) {
const tokens = token.split(" ");
return tokens.every(t => contains.call(this, t));
};
}
}; };

View File

@ -1,8 +1,9 @@
import Builtin from "../structs/builtin"; import Builtin from "../structs/builtin";
import {Emotes} from "data";
import {Utilities} from "modules"; import {Utilities} from "modules";
import EmoteModule from "./emotes";
export default new class EmoteAutocaps extends Builtin { export default new class EmoteAutocaps extends Builtin {
get name() {return "EmoteAutocapitalize";} get name() {return "EmoteAutocapitalize";}
get collection() {return "emotes";} get collection() {return "emotes";}
@ -31,7 +32,7 @@ export default new class EmoteAutocaps extends Builtin {
} }
capitalize(value) { capitalize(value) {
const res = Emotes.TwitchGlobal; const res = EmoteModule.getCategory("TwitchGlobal");
for (const p in res) { for (const p in res) {
if (res.hasOwnProperty(p) && value == (p + "").toLowerCase()) { if (res.hasOwnProperty(p) && value == (p + "").toLowerCase()) {
return p; return p;

View File

@ -1,7 +1,9 @@
import Builtin from "../structs/builtin"; import Builtin from "../structs/builtin";
import {Emotes, State} from "data"; import {State} from "data";
import {DataStore, Utilities, Events} from "modules"; import {DataStore, Utilities, Events} from "modules";
import EmoteModule from "./emotes";
const headerHTML = `<div id="bda-qem"> const headerHTML = `<div id="bda-qem">
<button class="active" id="bda-qem-twitch">Twitch</button> <button class="active" id="bda-qem-twitch">Twitch</button>
<button id="bda-qem-favourite">Favourite</button> <button id="bda-qem-favourite">Favourite</button>
@ -193,9 +195,9 @@ export default new class EmoteMenu extends Builtin {
updateTwitchEmotes() { updateTwitchEmotes() {
while (this.teContainerInner.firstChild) this.teContainerInner.firstChild.remove(); while (this.teContainerInner.firstChild) this.teContainerInner.firstChild.remove();
for (const emote in Emotes.TwitchGlobal) { for (const emote in EmoteModule.getCategory("TwitchGlobal")) {
if (!Emotes.TwitchGlobal.hasOwnProperty(emote)) continue; if (!EmoteModule.getCategory("TwitchGlobal").hasOwnProperty(emote)) continue;
const url = Emotes.TwitchGlobal[emote]; const url = EmoteModule.getCategory("TwitchGlobal")[emote];
const emoteElement = makeEmote(emote, url, {onClick: this.insertEmote.bind(this, emote)}); const emoteElement = makeEmote(emote, url, {onClick: this.insertEmote.bind(this, emote)});
this.teContainerInner.append(emoteElement); this.teContainerInner.append(emoteElement);
} }

View File

@ -1,10 +1,18 @@
import Builtin from "../structs/builtin"; import Builtin from "../structs/builtin";
import {Config, Emotes, EmoteBlacklist, EmoteInfo, EmoteModifiers, EmoteOverrides, State} from "data"; import {Config, EmoteInfo, State, EmoteConfig} from "data";
import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings} from "modules"; import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings} from "modules";
import BDEmote from "../ui/emote"; import BDEmote from "../ui/emote";
import {Toasts} from "ui"; import {Toasts} from "ui";
const Emotes = {
TwitchGlobal: {},
TwitchSubscriber: {},
BTTV: {},
FrankerFaceZ: {},
BTTV2: {}
};
const bdEmoteSettingIDs = { const bdEmoteSettingIDs = {
TwitchGlobal: "twitch", TwitchGlobal: "twitch",
TwitchSubscriber: "twitch", TwitchSubscriber: "twitch",
@ -13,6 +21,10 @@ const bdEmoteSettingIDs = {
BTTV2: "bttv" BTTV2: "bttv"
}; };
const blacklist = [];
const overrides = ["twitch", "bttv", "ffz"];
const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];
export default new class EmoteModule extends Builtin { export default new class EmoteModule extends Builtin {
get name() {return "Emotes";} get name() {return "Emotes";}
get collection() {return "settings";} get collection() {return "settings";}
@ -20,8 +32,6 @@ export default new class EmoteModule extends Builtin {
get id() {return "emotes";} get id() {return "emotes";}
get categories() { return Object.keys(bdEmoteSettingIDs).filter(k => this.isCategoryEnabled(bdEmoteSettingIDs[k])); } get categories() { return Object.keys(bdEmoteSettingIDs).filter(k => this.isCategoryEnabled(bdEmoteSettingIDs[k])); }
get MessageContentComponent() {return WebpackModules.getModule(m => m.defaultProps && m.defaultProps.hasOwnProperty("disableButtons"));}
isCategoryEnabled(id) { isCategoryEnabled(id) {
return super.get("emotes", "categories", id); return super.get("emotes", "categories", id);
} }
@ -30,7 +40,28 @@ export default new class EmoteModule extends Builtin {
return super.get("emotes", "general", id); return super.get("emotes", "general", id);
} }
get MessageContentComponent() {return WebpackModules.getModule(m => m.defaultProps && m.defaultProps.hasOwnProperty("disableButtons"));}
get Emotes() {return Emotes;}
get TwitchGlobal() {return Emotes.TwitchGlobal;}
get TwitchSubscriber() {return Emotes.TwitchSubscriber;}
get BTTV() {return Emotes.BTTV;}
get FrankerFaceZ() {return Emotes.FrankerFaceZ;}
get BTTV2() {return Emotes.BTTV2;}
get blacklist() {return blacklist;}
getCategory(category) {
return Emotes[category];
}
initialize() {
super.initialize();
// EmoteConfig;
// emoteCollection.button = {title: "Clear Emote Cache", onClick: () => { this.clearEmoteData(); this.loadEmoteData(EmoteInfo); }};
}
async enabled() { async enabled() {
Settings.registerCollection("emotes", "Emotes", EmoteConfig, {title: "Clear Emote Cache", onClick: () => { this.clearEmoteData(); this.loadEmoteData(EmoteInfo); }});
// Disable emote module for now because it's annoying and slow // Disable emote module for now because it's annoying and slow
// await this.getBlacklist(); // await this.getBlacklist();
// await this.loadEmoteData(EmoteInfo); // await this.loadEmoteData(EmoteInfo);
@ -40,8 +71,9 @@ export default new class EmoteModule extends Builtin {
} }
disabled() { disabled() {
Settings.removeCollection("emotes");
this.emptyEmotes(); this.emptyEmotes();
if (this.cancelEmoteRender) return; if (!this.cancelEmoteRender) return;
this.cancelEmoteRender(); this.cancelEmoteRender();
delete this.cancelEmoteRender; delete this.cancelEmoteRender;
} }
@ -71,9 +103,9 @@ export default new class EmoteModule extends Builtin {
let emoteModifier = emoteSplit[1] ? emoteSplit[1] : ""; let emoteModifier = emoteSplit[1] ? emoteSplit[1] : "";
let emoteOverride = emoteModifier.slice(0); let emoteOverride = emoteModifier.slice(0);
if (emoteName.length < 4 || EmoteBlacklist.includes(emoteName)) continue; if (emoteName.length < 4 || blacklist.includes(emoteName)) continue;
if (!EmoteModifiers.includes(emoteModifier) || !Settings.get(this.category, "general", "modifiers")) emoteModifier = ""; if (!modifiers.includes(emoteModifier) || !Settings.get(this.category, "general", "modifiers")) emoteModifier = "";
if (!EmoteOverrides.includes(emoteOverride)) emoteOverride = ""; if (!overrides.includes(emoteOverride)) emoteOverride = "";
else emoteModifier = emoteOverride; else emoteModifier = emoteOverride;
let current = this.categories[c]; let current = this.categories[c];
@ -211,7 +243,7 @@ export default new class EmoteModule extends Builtin {
if (typeof(emoteMeta.parser) === "function") parsedData = emoteMeta.parser(parsedData); if (typeof(emoteMeta.parser) === "function") parsedData = emoteMeta.parser(parsedData);
for (const emote in parsedData) { for (const emote in parsedData) {
if (emote.length < 4 || EmoteBlacklist.includes(emote)) { if (emote.length < 4 || blacklist.includes(emote)) {
delete parsedData[emote]; delete parsedData[emote];
continue; continue;
} }
@ -226,7 +258,7 @@ export default new class EmoteModule extends Builtin {
getBlacklist() { getBlacklist() {
return new Promise(resolve => { return new Promise(resolve => {
$.getJSON(`https://rauenzi.github.io/BetterDiscordApp/data/emotefilter.json`, function (data) { $.getJSON(`https://rauenzi.github.io/BetterDiscordApp/data/emotefilter.json`, function (data) {
resolve(EmoteBlacklist.push(...data.blacklist)); resolve(blacklist.push(...data.blacklist));
}); });
}); });
} }

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,32 +0,0 @@
export default {
"bda-gs-1": true,
"bda-gs-2": false,
"bda-gs-3": false,
"bda-gs-4": false,
"bda-gs-5": true,
"bda-gs-6": false,
"bda-gs-7": false,
"bda-gs-8": false,
"bda-es-0": true,
"bda-es-1": true,
"bda-es-2": true,
"bda-es-4": false,
"bda-es-6": true,
"bda-es-7": true,
"bda-gs-b": false,
"bda-es-8": true,
"bda-dc-0": false,
"bda-css-0": false,
"bda-css-1": false,
"bda-es-9": true,
"fork-dm-1": false,
"fork-ps-1": true,
"fork-ps-2": true,
"fork-ps-3": true,
"fork-ps-4": true,
"fork-ps-5": true,
"fork-es-2": false,
"fork-es-3": true,
"fork-wp-1": false,
"fork-wp-2": false
};

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,18 +1,9 @@
import State from "./state"; import State from "./state";
// import SettingsInfo from "./settings";
// import SettingsCookie from "./cookies/settingscookie";
import Config from "./config"; import Config from "./config";
// import PluginCookie from "./cookies/plugincookie";
// import ThemeCookie from "./cookies/themecookie";
// import Themes from "./themes";
// import Plugins from "./plugins";
import Emotes from "./emotes/emotes";
import EmoteBlacklist from "./emotes/blacklist";
import EmoteInfo from "./emotes/info"; import EmoteInfo from "./emotes/info";
import EmoteModifiers from "./emotes/modifiers"; import EmoteConfig from "./emotes/config";
import EmoteOverrides from "./emotes/overrides";
import SettingsConfig from "./settings/config"; import SettingsConfig from "./settings/config";
import SettingsState from "./settings/state"; import SettingsState from "./settings/state";
export {State, Config, /*SettingsInfo, SettingsCookie, PluginCookie, ThemeCookie, Themes, Plugins,*/ Emotes, EmoteBlacklist, EmoteInfo, EmoteModifiers, EmoteOverrides, SettingsConfig, SettingsState}; export {State, Config, EmoteInfo, EmoteConfig, SettingsConfig, SettingsState};

View File

@ -1 +0,0 @@
export default [];

89
src/data/emotes/config.js Normal file
View File

@ -0,0 +1,89 @@
export default [
{
type: "category",
id: "general",
name: "General",
collapsible: true,
settings: [
{
type: "switch",
id: "download",
name: "Download Emotes",
note: "Download emotes once a week to stay up to date",
value: true
},
{
type: "switch",
id: "emoteMenu",
name: "Emote Menu",
note: "Show Twitch/Favourite emotes in emote menu",
value: true
},
{
type: "switch",
id: "hideEmojiMenu",
name: "Hide Emoji Menu",
note: "Hides Discord's emoji menu when using emote menu",
value: false,
enableWith: "emoteMenu"
},
{
type: "switch",
id: "autoCaps",
name: "Emote Autocapitalization",
note: "Autocapitalize emote commands",
value: false
},
{
type: "switch",
id: "showNames",
name: "Show Names",
note: "Show emote names on hover",
value: true
},
{
type: "switch",
id: "modifiers",
name: "Show Emote Modifiers",
note: "Enable emote mods (flip, spin, pulse, spin2, spin3, 1spin, 2spin, 3spin, tr, bl, br, shake, shake2, shake3, flap)",
value: true
},
{
type: "switch",
id: "animateOnHover",
name: "Animate On Hover",
note: "Only animate the emote modifiers on hover",
value: false
}
]
},
{
type: "category",
id: "categories",
name: "Categories",
collapsible: true,
settings: [
{
type: "switch",
id: "twitch",
name: "Twitch",
note: "Show Twitch global & subscriber emotes",
value: true
},
{
type: "switch",
id: "ffz",
name: "FrankerFaceZ",
note: "Show emotes from FFZ",
value: true
},
{
type: "switch",
id: "bttv",
name: "BetterTTV",
note: "Show emotes from BTTV",
value: true
}
]
}
];

View File

@ -1,7 +0,0 @@
export default {
TwitchGlobal: {},
TwitchSubscriber: {},
BTTV: {},
FrankerFaceZ: {},
BTTV2: {}
};

View File

@ -1 +0,0 @@
export default ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];

View File

@ -1 +0,0 @@
export default ["twitch", "bttv", "ffz"];

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,37 +0,0 @@
{
"core": {
"PublicServers": true,
"MinimalMode": false,
"VoiceMode": false,
"HideChannels": false,
"DarkMode": true,
"VoiceDisconnect": false,
"Timestamps": false,
"ColoredText": false,
"BDBlue": false,
"DeveloperMode": false
},
"fork": {
"ContentErrors": true,
"Toasts": true,
"Scroll": true,
"AnimateOnHover": false,
"CopySelector": false,
"DownloadEmotes": true,
"NormalizeClasses": true,
"AutomaticLoading": true,
"Transparency": false
},
"emote": {
"Twitch": true,
"FFZ": true,
"BTTV": true,
"EmoteMenu": true,
"EmojiMenu": true,
"AutoCaps": false,
"ShowNames": true,
"Modifiers": true
}
}

View File

@ -1,274 +1,172 @@
export default [ export default [
{ {
type: "collection", type: "category",
id: "settings", id: "general",
name: "Settings", name: "General",
collapsible: true,
settings: [ settings: [
{ {
type: "category", type: "switch",
id: "general", id: "emotes",
name: "General", name: "Emote System",
collapsible: true, note: "Enables BD's emote system",
settings: [ value: true
{
type: "switch",
id: "emotes",
name: "Emote System",
note: "Enables BD's emote system",
value: true
},
{
type: "switch",
id: "publicServers",
name: "Public Servers",
note: "Display public servers button",
value: true
},
{
type: "switch",
id: "voiceDisconnect",
name: "Voice Disconnect",
note: "Disconnect from voice server when closing Discord",
value: false
},
{
type: "switch",
id: "twentyFourHour",
name: "24 Hour Timestamps",
note: "Hides channels when in minimal mode",
value: false,
},
{
type: "switch",
id: "classNormalizer",
name: "Normalize Classes",
note: "Adds stable classes to elements to help themes. (e.g. adds .da-channels to .channels-Ie2l6A)",
value: true
},
{
type: "switch",
id: "showToasts",
name: "Show Toasts",
note: "Shows a small notification for important information",
value: true
}
]
}, },
{ {
type: "category", type: "switch",
id: "appearance", id: "publicServers",
name: "Appearance", name: "Public Servers",
collapsible: true, note: "Display public servers button",
settings: [ value: true
{
type: "switch",
id: "voiceMode",
name: "Voice Mode",
note: "Hides everything that isn't voice chat",
value: false
},
{
type: "switch",
id: "minimalMode",
name: "Minimal Mode",
note: "Hide elements and reduce the size of elements",
value: false
},
{
type: "switch",
id: "hideChannels",
name: "Hide Channels",
note: "Hides channels when in minimal mode",
value: false,
enableWith: "minimalMode"
},
{
type: "switch",
id: "darkMode",
name: "Dark Mode",
note: "Make certain elements dark by default",
value: true
},
{
type: "switch",
id: "coloredText",
name: "Colored Text",
note: "Make text colour the same as role color",
value: false
}
]
}, },
{ {
type: "category", type: "switch",
id: "content", id: "voiceDisconnect",
name: "Content Manager", name: "Voice Disconnect",
collapsible: true, note: "Disconnect from voice server when closing Discord",
settings: [ value: false
{
type: "switch",
id: "contentErrors",
name: "Show Content Errors",
note: "Shows a modal with plugin/theme errors",
value: true
},
{
type: "switch",
id: "autoScroll",
name: "Scroll To Settings",
note: "Auto-scrolls to a plugin's settings when the button is clicked (only if out of view)",
value: true
},
{
type: "switch",
id: "autoReload",
name: "Automatic Loading",
note: "Automatically loads, reloads, and unloads plugins and themes",
value: true
}
]
}, },
{ {
type: "category", type: "switch",
id: "developer", id: "twentyFourHour",
name: "Developer Settings", name: "24 Hour Timestamps",
collapsible: true, note: "Hides channels when in minimal mode",
shown: false, value: false,
settings: [
{
type: "switch",
id: "developerMode",
name: "Developer Mode",
note: "Allows activating debugger when pressing F8",
value: false
},
{
type: "switch",
id: "copySelector",
name: "Copy Selector",
note: "Adds a \"Copy Selector\" option to context menus when developer mode is active",
value: false,
enableWith: "developerMode"
}
]
}, },
{ {
type: "category", type: "switch",
id: "window", id: "classNormalizer",
name: "Window Preferences", name: "Normalize Classes",
collapsible: true, note: "Adds stable classes to elements to help themes. (e.g. adds .da-channels to .channels-Ie2l6A)",
shown: false, value: true
settings: [ },
{ {
type: "switch", type: "switch",
id: "transparency", id: "showToasts",
name: "Enable Transparency", name: "Show Toasts",
note: "Enables the main window to be see-through (requires restart)", note: "Shows a small notification for important information",
value: false value: true
},
{
type: "switch",
id: "frame",
name: "Window Frame",
note: "Adds the native os window frame to the main window",
value: false,
hidden: true
}
]
} }
] ]
}, },
{ {
type: "collection", type: "category",
id: "emotes", id: "appearance",
name: "Emotes", name: "Appearance",
enableWith: "settings.general.emotes", collapsible: true,
settings: [ settings: [
{ {
type: "category", type: "switch",
id: "general", id: "voiceMode",
name: "General", name: "Voice Mode",
collapsible: true, note: "Hides everything that isn't voice chat",
settings: [ value: false
{
type: "switch",
id: "download",
name: "Download Emotes",
note: "Download emotes once a week to stay up to date",
value: true
},
{
type: "switch",
id: "emoteMenu",
name: "Emote Menu",
note: "Show Twitch/Favourite emotes in emote menu",
value: true
},
{
type: "switch",
id: "hideEmojiMenu",
name: "Hide Emoji Menu",
note: "Hides Discord's emoji menu when using emote menu",
value: false,
enableWith: "emoteMenu"
},
{
type: "switch",
id: "autoCaps",
name: "Emote Autocapitalization",
note: "Autocapitalize emote commands",
value: false
},
{
type: "switch",
id: "showNames",
name: "Show Names",
note: "Show emote names on hover",
value: true
},
{
type: "switch",
id: "modifiers",
name: "Show Emote Modifiers",
note: "Enable emote mods (flip, spin, pulse, spin2, spin3, 1spin, 2spin, 3spin, tr, bl, br, shake, shake2, shake3, flap)",
value: true
},
{
type: "switch",
id: "animateOnHover",
name: "Animate On Hover",
note: "Only animate the emote modifiers on hover",
value: false
}
]
}, },
{ {
type: "category", type: "switch",
id: "categories", id: "minimalMode",
name: "Categories", name: "Minimal Mode",
collapsible: true, note: "Hide elements and reduce the size of elements",
settings: [ value: false
{ },
type: "switch", {
id: "twitch", type: "switch",
name: "Twitch", id: "hideChannels",
note: "Show Twitch global & subscriber emotes", name: "Hide Channels",
value: true note: "Hides channels when in minimal mode",
}, value: false,
{ enableWith: "minimalMode"
type: "switch", },
id: "ffz", {
name: "FrankerFaceZ", type: "switch",
note: "Show emotes from FFZ", id: "darkMode",
value: true name: "Dark Mode",
}, note: "Make certain elements dark by default",
{ value: true
type: "switch", },
id: "bttv", {
name: "BetterTTV", type: "switch",
note: "Show emotes from BTTV", id: "coloredText",
value: true name: "Colored Text",
} note: "Make text colour the same as role color",
] value: false
}
]
},
{
type: "category",
id: "content",
name: "Content Manager",
collapsible: true,
settings: [
{
type: "switch",
id: "contentErrors",
name: "Show Content Errors",
note: "Shows a modal with plugin/theme errors",
value: true
},
{
type: "switch",
id: "autoScroll",
name: "Scroll To Settings",
note: "Auto-scrolls to a plugin's settings when the button is clicked (only if out of view)",
value: true
},
{
type: "switch",
id: "autoReload",
name: "Automatic Loading",
note: "Automatically loads, reloads, and unloads plugins and themes",
value: true
}
]
},
{
type: "category",
id: "developer",
name: "Developer Settings",
collapsible: true,
shown: false,
settings: [
{
type: "switch",
id: "developerMode",
name: "Developer Mode",
note: "Allows activating debugger when pressing F8",
value: false
},
{
type: "switch",
id: "copySelector",
name: "Copy Selector",
note: "Adds a \"Copy Selector\" option to context menus when developer mode is active",
value: false,
enableWith: "developerMode"
}
]
},
{
type: "category",
id: "window",
name: "Window Preferences",
collapsible: true,
shown: false,
settings: [
{
type: "switch",
id: "transparency",
name: "Enable Transparency",
note: "Enables the main window to be see-through (requires restart)",
value: false
},
{
type: "switch",
id: "frame",
name: "Window Frame",
note: "Adds the native os window frame to the main window",
value: false,
hidden: true
} }
] ]
} }

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,4 +1,4 @@
import {Config, /*SettingsCookie, SettingsInfo, PluginCookie, ThemeCookie, Plugins, Themes,*/ Emotes, EmoteBlacklist} from "data"; import {Config} from "data";
import proxyLocalStorage from "./localstorage"; import proxyLocalStorage from "./localstorage";
import Core from "./modules/core"; import Core from "./modules/core";
import BdApi from "./modules/pluginapi"; import BdApi from "./modules/pluginapi";
@ -6,6 +6,8 @@ import PluginManager from "./modules/pluginmanager";
import ThemeManager from "./modules/thememanager"; import ThemeManager from "./modules/thememanager";
import {bdPluginStorage} from "./modules/oldstorage"; import {bdPluginStorage} from "./modules/oldstorage";
import Events from "./modules/emitter"; import Events from "./modules/emitter";
import Settings from "./modules/settingsmanager";
import EmoteModule from "./builtins/emotes";
// Perform some setup // Perform some setup
proxyLocalStorage(); proxyLocalStorage();
@ -24,9 +26,10 @@ window.pluginModule = PluginManager;
window.themeModule = ThemeManager; window.themeModule = ThemeManager;
// window.bdthemes = Themes; // window.bdthemes = Themes;
// window.bdplugins = Plugins; // window.bdplugins = Plugins;
window.bdEmotes = Emotes; window.bdEmotes = EmoteModule.Emotes;
window.bemotes = EmoteBlacklist; window.bemotes = EmoteModule.blacklist;
window.bdPluginStorage = bdPluginStorage; window.bdPluginStorage = bdPluginStorage;
window.settingsModule = Settings;
window.BDEvents = Events; window.BDEvents = Events;

View File

@ -49,7 +49,6 @@ export default class ContentManager {
loadState() { loadState() {
const saved = DataStore.getData(`${this.prefix}s`); const saved = DataStore.getData(`${this.prefix}s`);
console.log(saved);
if (!saved) return; if (!saved) return;
Object.assign(this.state, saved); Object.assign(this.state, saved);
} }
@ -152,7 +151,6 @@ export default class ContentManager {
try {__non_webpack_require__(path.resolve(this.contentFolder, filename));} try {__non_webpack_require__(path.resolve(this.contentFolder, filename));}
catch (error) {return new ContentError(filename, filename, "Could not be compiled.", {message: error.message, stack: error.stack});} catch (error) {return new ContentError(filename, filename, "Could not be compiled.", {message: error.message, stack: error.stack});}
const content = __non_webpack_require__(path.resolve(this.contentFolder, filename)); const content = __non_webpack_require__(path.resolve(this.contentFolder, filename));
console.log(content);
if (this.contentList.find(c => c.id == content.id)) return new ContentError(content.name, filename, `There is already a plugin with name ${content.name}`); if (this.contentList.find(c => c.id == content.id)) return new ContentError(content.name, filename, `There is already a plugin with name ${content.name}`);
const error = this.initializeContent(content); const error = this.initializeContent(content);
if (error) return error; if (error) return error;

View File

@ -48,7 +48,7 @@ export default new class PluginManager extends ContentManager {
loadAllPlugins() { loadAllPlugins() {
const errors = this.loadAllContent(); const errors = this.loadAllContent();
this.setupFunctions(); this.setupFunctions();
Settings.registerPanel("Plugins", {element: () => SettingsRenderer.getPluginsPanel(this.contentList)}); Settings.registerPanel("Plugins", {element: () => SettingsRenderer.getPluginsPanel(this.contentList, this.contentFolder)});
return errors; return errors;
} }
@ -58,7 +58,7 @@ export default new class PluginManager extends ContentManager {
try { try {
const thePlugin = new content.type(); const thePlugin = new content.type();
content.plugin = thePlugin; content.plugin = thePlugin;
content.name = content.name || thePlugin.getName(); content.name = thePlugin.getName() || content.name;
content.author = content.author || thePlugin.getAuthor() || "No author"; content.author = content.author || thePlugin.getAuthor() || "No author";
content.description = content.description || thePlugin.getDescription() || "No description"; content.description = content.description || thePlugin.getDescription() || "No description";
content.version = content.version || thePlugin.getVersion() || "No version"; content.version = content.version || thePlugin.getVersion() || "No version";

View File

@ -1,6 +1,5 @@
import {SettingsConfig, SettingsState} from "data"; import {SettingsConfig, SettingsState} from "data";
import DataStore from "./datastore"; import DataStore from "./datastore";
// import PluginManager from "./pluginmanager";
import BdApi from "./pluginapi"; import BdApi from "./pluginapi";
import Events from "./emitter"; import Events from "./emitter";
import WebpackModules, {DiscordModules} from "./webpackmodules"; import WebpackModules, {DiscordModules} from "./webpackmodules";
@ -14,15 +13,33 @@ export default new class SettingsManager {
constructor() { constructor() {
this.config = SettingsConfig; this.config = SettingsConfig;
this.state = SettingsState; this.state = SettingsState;
this.collections = [];
this.panels = []; this.panels = [];
this.setup(SettingsConfig, SettingsState); this.registerCollection("settings", "Settings", SettingsConfig);
} }
initialize() { initialize() {
DataStore.initialize(); DataStore.initialize();
this.loadSettings(); this.loadSettings();
this.patchSections(); this.patchSections();
// this.registerPanel("Plugins", {element: () => SettingsRenderer.getPluginsPanel(PluginManager.contentList)}); }
registerCollection(id, name, settings, button = null) {
if (this.collections.find(c => c.id == id)) Utilities.err("Settings", "Already have a collection with id " + id);
this.collections.push({
type: "collection",
id: id,
name: name,
settings: settings,
button: button
});
this.setup();
}
removeCollection(id) {
const location = this.collections.findIndex(c => c.id == id);
if (!location < 0) Utilities.err("Settings", "No collection with id " + id);
this.collections.splice(location, 1);
} }
registerPanel(name, options) { registerPanel(name, options) {
@ -40,25 +57,27 @@ export default new class SettingsManager {
return {collection, category, setting}; return {collection, category, setting};
} }
setup(collections, state) { setup() {
const config = {}; console.log("before state");
for (let c = 0; c < collections.length; c++) { console.log(this.state);
const collection = collections[c]; for (let c = 0; c < this.collections.length; c++) {
const categories = collections[c].settings; const collection = this.collections[c];
config[collection.id] = {}; const categories = this.collections[c].settings;
if (!this.state[collection.id]) this.state[collection.id] = {};
for (let s = 0; s < categories.length; s++) { for (let s = 0; s < categories.length; s++) {
const category = categories[s]; const category = categories[s];
if (category.type != "category") {config[collection.id][category.id] = category.value;} if (category.type != "category") {if (!this.state[collection.id].hasOwnProperty(category.id)) this.state[collection.id][category.id] = category.value;}
else { else {
config[collection.id][category.id] = {}; if (!this.state[collection.id].hasOwnProperty(category.id)) this.state[collection.id][category.id] = {};
for (let s = 0; s < category.settings.length; s++) { for (let s = 0; s < category.settings.length; s++) {
const setting = category.settings[s]; const setting = category.settings[s];
config[collection.id][category.id][setting.id] = setting.value; if (!this.state[collection.id][category.id].hasOwnProperty(setting.id)) this.state[collection.id][category.id][setting.id] = setting.value;
if (setting.enableWith) { if (setting.enableWith) {
const path = this.getPath(setting.enableWith.split("."), collection.id, category.id); const path = this.getPath(setting.enableWith.split("."), collection.id, category.id);
if (setting.hasOwnProperty("disabled")) continue;
Object.defineProperty(setting, "disabled", { Object.defineProperty(setting, "disabled", {
get: () => { get: () => {
return !state[path.collection][path.category][path.setting]; return !this.state[path.collection][path.category][path.setting];
} }
}); });
} }
@ -69,14 +88,13 @@ export default new class SettingsManager {
const path = this.getPath(collection.enableWith.split(".")); const path = this.getPath(collection.enableWith.split("."));
Object.defineProperty(collection, "disabled", { Object.defineProperty(collection, "disabled", {
get: () => { get: () => {
return !state[path.collection][path.category][path.setting]; return !this.state[path.collection][path.category][path.setting];
} }
}); });
} }
} }
console.log("after state");
this.defaultState = config; console.log(this.state);
Object.assign(this.state, this.defaultState);
} }
async patchSections() { async patchSections() {
@ -90,12 +108,12 @@ export default new class SettingsManager {
console.log(data); /* eslint-disable-line no-console */ console.log(data); /* eslint-disable-line no-console */
insert({section: "DIVIDER"}); insert({section: "DIVIDER"});
insert({section: "HEADER", label: "BandagedBD"}); insert({section: "HEADER", label: "BandagedBD"});
for (const collection of this.config) { for (const collection of this.collections) {
if (collection.disabled) continue; if (collection.disabled) continue;
insert({ insert({
section: collection.name, section: collection.name,
label: collection.name, label: collection.name,
element: () => SettingsRenderer.buildSettingsPanel(collection.name, collection.settings, SettingsState[collection.id], this.onSettingChange.bind(this, collection.id)) element: () => SettingsRenderer.buildSettingsPanel(collection.name, collection.settings, SettingsState[collection.id], this.onSettingChange.bind(this, collection.id), collection.button ? collection.button : null)
}); });
} }
for (const panel of this.panels) insert(panel); for (const panel of this.panels) insert(panel);
@ -143,21 +161,26 @@ export default new class SettingsManager {
} }
onSettingChange(collection, category, id, value) { onSettingChange(collection, category, id, value) {
const before = this.config.filter(c => c.disabled).length; const before = this.collections.length;
this.state[collection][category][id] = value; this.state[collection][category][id] = value;
Events.dispatch("setting-updated", collection, category, id, value); Events.dispatch("setting-updated", collection, category, id, value);
const after = this.config.filter(c => c.disabled).length; const after = this.collections.length;
this.saveSettings(); this.saveSettings();
if (before != after) this.forceUpdate(); if (before != after) this.forceUpdate();
} }
getSetting(collection, category, id) { getSetting(collection, category, id) {
if (arguments.length == 2) return this.config[0].find(c => c.id == arguments[0]).settings.find(s => s.id == arguments[1]); if (arguments.length == 2) return this.collections[0].find(c => c.id == arguments[0]).settings.find(s => s.id == arguments[1]);
return this.config.find(c => c.id == collection).find(c => c.id == category).settings.find(s => s.id == id); return this.collections.find(c => c.id == collection).find(c => c.id == category).settings.find(s => s.id == id);
} }
get(collection, category, id) { get(collection, category, id) {
if (arguments.length == 2) return this.state[this.config[0].id][arguments[0]][arguments[1]]; if (arguments.length == 2) {
id = category;
category = collection;
collection = "settings";
}
if (!this.state[collection] || !this.state[collection][category]) return false;
return this.state[collection][category][id]; return this.state[collection][category][id];
} }

View File

@ -17,7 +17,7 @@ export default new class ThemeManager extends ContentManager {
/* Aliases */ /* Aliases */
updateThemeList() {return this.updateList();} updateThemeList() {return this.updateList();}
loadAllThemes() { loadAllThemes() {
Settings.registerPanel("Themes", {element: () => SettingsRenderer.getThemesPanel(this.contentList)}); Settings.registerPanel("Themes", {element: () => SettingsRenderer.getThemesPanel(this.contentList, this.contentFolder)});
return this.loadAllContent(); return this.loadAllContent();
} }

View File

@ -11,7 +11,7 @@ export default class BuiltinModule {
get id() {return "None";} get id() {return "None";}
async initialize() { async initialize() {
if (SettingsState[this.collection][this.category][this.id]) await this.enable(); if (Settings.get(this.collection, this.category, this.id)) await this.enable();
Events.on("setting-updated", (collection, category, id, enabled) => { Events.on("setting-updated", (collection, category, id, enabled) => {
if (collection != this.collection || category !== this.category || id !== this.id) return; if (collection != this.collection || category !== this.category || id !== this.id) return;
if (enabled) this.enable(); if (enabled) this.enable();

View File

@ -1,5 +1,5 @@
// import {SettingsCookie, PluginCookie, Plugins} from "data"; // import {SettingsCookie, PluginCookie, Plugins} from "data";
import {React, ReactDOM, Utilities, PluginManager} from "modules"; import {React, Utilities, PluginManager} from "modules";
import CloseButton from "../icons/close"; import CloseButton from "../icons/close";
// import ReloadIcon from "../icons/reload"; // import ReloadIcon from "../icons/reload";
@ -15,6 +15,7 @@ export default class V2C_PluginCard extends React.Component {
}; };
this.hasSettings = typeof this.props.content.plugin.getSettingsPanel === "function"; this.hasSettings = typeof this.props.content.plugin.getSettingsPanel === "function";
this.settingsPanel = ""; this.settingsPanel = "";
this.panelRef = React.createRef();
// this.reload = this.reload.bind(this); // this.reload = this.reload.bind(this);
// this.onReload = this.onReload.bind(this); // this.onReload = this.onReload.bind(this);
@ -23,7 +24,7 @@ export default class V2C_PluginCard extends React.Component {
componentDidUpdate() { componentDidUpdate() {
if (this.state.settings) { if (this.state.settings) {
if (typeof this.settingsPanel === "object") { if (typeof this.settingsPanel === "object") {
this.refs.settingspanel.appendChild(this.settingsPanel); this.panelRef.current.appendChild(this.settingsPanel);
} }
// if (!SettingsCookie["fork-ps-3"]) return; // if (!SettingsCookie["fork-ps-3"]) return;
@ -38,7 +39,7 @@ export default class V2C_PluginCard extends React.Component {
return (eTop < cTop || eBottom > cBottom); return (eTop < cTop || eBottom > cBottom);
}; };
const self = $(ReactDOM.findDOMNode(this)); const self = $(this.panelRef.current);
const container = self.parents(".scroller-2FKFPG"); const container = self.parents(".scroller-2FKFPG");
if (!isHidden(container[0], self[0])) return; if (!isHidden(container[0], self[0])) return;
container.animate({ container.animate({
@ -67,13 +68,13 @@ export default class V2C_PluginCard extends React.Component {
return React.createElement("li", {className: "settings-open ui-switch-item"}, return React.createElement("li", {className: "settings-open ui-switch-item"},
React.createElement("div", {style: {"float": "right", "cursor": "pointer"}, onClick: () => { React.createElement("div", {style: {"float": "right", "cursor": "pointer"}, onClick: () => {
this.refs.settingspanel.innerHTML = ""; this.panelRef.current.innerHTML = "";
self.setState({settings: false}); self.setState({settings: false});
}}, }},
React.createElement(CloseButton, null) React.createElement(CloseButton, null)
), ),
typeof self.settingsPanel === "object" && React.createElement("div", {id: `plugin-settings-${name}`, className: "plugin-settings", ref: "settingspanel"}), typeof self.settingsPanel === "object" && React.createElement("div", {id: `plugin-settings-${name}`, className: "plugin-settings", ref: this.panelRef}),
typeof self.settingsPanel !== "object" && React.createElement("div", {id: `plugin-settings-${name}`, className: "plugin-settings", ref: "settingspanel", dangerouslySetInnerHTML: {__html: self.settingsPanel}}) typeof self.settingsPanel !== "object" && React.createElement("div", {id: `plugin-settings-${name}`, className: "plugin-settings", ref: this.panelRef, dangerouslySetInnerHTML: {__html: self.settingsPanel}})
); );
} }

View File

@ -1,74 +1,41 @@
import {Config} from "data"; import {Config} from "data";
import {React/*, ReactDOM, Utilities, ContentManager, Events, PluginManager, ThemeManager*/} from "modules"; import {React} from "modules";
// import Sidebar from "./sidebar";
// import Scroller from "../scroller";
// import List from "../list";
// import ContentColumn from "./contentcolumn";
// import SectionedSettingsPanel from "./sectionedsettings";
// import Tools from "./exitbutton";
// import SettingsPanel from "./panel";
import PluginCard from "./plugincard"; import PluginCard from "./plugincard";
import ThemeCard from "./themecard"; import ThemeCard from "./themecard";
// import ReloadIcon from "../icons/reload";
// import CssEditor from "../customcss/editor";
// import SettingsGroup from "../settings/settingsgroup";
import SettingsGroup from "../settings/group"; import SettingsGroup from "../settings/group";
import SettingsTitle from "./title"; import SettingsTitle from "./title";
export default class V2_SettingsPanel { export default class V2_SettingsPanel {
static buildSettingsPanel(title, config, state, onChange) { static buildSettingsPanel(title, config, state, onChange, button = null) {
config.forEach(section => { config.forEach(section => {
section.settings.forEach(item => item.value = state[section.id][item.id]); section.settings.forEach(item => item.value = state[section.id][item.id]);
}); });
return this.getSettingsPanel(title, config, onChange); return this.getSettingsPanel(title, config, onChange, button);
} }
static getSettingsPanel(title, groups, onChange) { static getSettingsPanel(title, groups, onChange, button = null) {
return [React.createElement(SettingsTitle, {text: title}), groups.map(section => { return [React.createElement(SettingsTitle, {text: title, button: button}), groups.map(section => {
return React.createElement(SettingsGroup, Object.assign({}, section, {onChange})); return React.createElement(SettingsGroup, Object.assign({}, section, {onChange}));
})]; })];
} }
static getPluginsPanel(plugins) { static getPluginsPanel(plugins, folder) {
const titleComponent = React.createElement(SettingsTitle, {text: "Plugins", button: {title: "Open Plugin Folder", onClick: () => { require("electron").shell.openItem(""); }}}); const titleComponent = React.createElement(SettingsTitle, {text: "Plugins", button: {title: "Open Plugin Folder", onClick: () => { require("electron").shell.openItem(folder); }}});
const cards = plugins.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(plugin => const cards = plugins.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(plugin =>
React.createElement(PluginCard, {key: plugin.id, content: plugin}) React.createElement(PluginCard, {key: plugin.id, content: plugin})
); );
console.log(cards);
return [titleComponent, React.createElement("ul", {className: "bda-slist"}, ...cards)]; return [titleComponent, React.createElement("ul", {className: "bda-slist"}, ...cards)];
// const plugins = Object.keys(Plugins).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).reduce((arr, key) => {
// arr.push(React.createElement(PluginCard, {key: key, plugin: Plugins[key].plugin}));return arr;
// }, []);
// const list = React.createElement(List, {key: "plugin-list", className: "bda-slist", children: plugins});
// const refreshIcon = !SettingsCookie["fork-ps-5"] && React.createElement(ReloadIcon, {className: "bd-reload-header", size: "18px", onClick: async () => {
// PluginManager.updatePluginList();
// this.sideBarOnClick("plugins");
// }});
// const pfBtn = React.createElement("button", {key: "folder-button", className: "bd-pfbtn", onClick: () => { require("electron").shell.openItem(ContentManager.pluginsFolder); }}, "Open Plugin Folder");
// const contentColumn = React.createElement(ContentColumn, {key: "pcolumn", title: "Plugins", children: [refreshIcon, pfBtn, list]});
// return React.createElement(Scroller, {contentColumn: true, fade: true, dark: true, children: [contentColumn, React.createElement(Tools, {key: "tools"})]});
} }
static getThemesPanel(themes) { static getThemesPanel(themes, folder) {
const titleComponent = React.createElement(SettingsTitle, {text: "Themes", button: {title: "Open Theme Folder", onClick: () => { require("electron").shell.openItem(""); }}}); const titleComponent = React.createElement(SettingsTitle, {text: "Themes", button: {title: "Open Theme Folder", onClick: () => { require("electron").shell.openItem(folder); }}});
const cards = themes.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(theme => const cards = themes.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(theme =>
React.createElement(ThemeCard, {key: theme.id, content: theme}) React.createElement(ThemeCard, {key: theme.id, content: theme})
); );
console.log(cards);
return [titleComponent, React.createElement("ul", {className: "bda-slist"}, ...cards)]; return [titleComponent, React.createElement("ul", {className: "bda-slist"}, ...cards)];
// const plugins = Object.keys(Plugins).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).reduce((arr, key) => {
// arr.push(React.createElement(PluginCard, {key: key, plugin: Plugins[key].plugin}));return arr;
// }, []);
// const list = React.createElement(List, {key: "plugin-list", className: "bda-slist", children: plugins});
// const refreshIcon = !SettingsCookie["fork-ps-5"] && React.createElement(ReloadIcon, {className: "bd-reload-header", size: "18px", onClick: async () => {
// PluginManager.updatePluginList();
// this.sideBarOnClick("plugins");
// }});
// const pfBtn = React.createElement("button", {key: "folder-button", className: "bd-pfbtn", onClick: () => { require("electron").shell.openItem(ContentManager.pluginsFolder); }}, "Open Plugin Folder");
// const contentColumn = React.createElement(ContentColumn, {key: "pcolumn", title: "Plugins", children: [refreshIcon, pfBtn, list]});
// return React.createElement(Scroller, {contentColumn: true, fade: true, dark: true, children: [contentColumn, React.createElement(Tools, {key: "tools"})]});
} }
static get attribution() { static get attribution() {
@ -83,85 +50,4 @@ export default class V2_SettingsPanel {
) )
); );
} }
// get coreComponent() {
// return React.createElement(Scroller, {contentColumn: true, fade: true, dark: true, children: [
// React.createElement(SectionedSettingsPanel, {key: "cspanel", onChange: this.onChange, sections: this.coreSettings}),
// React.createElement(Tools, {key: "tools"})
// ]});
// }
// get emoteComponent() {
// return React.createElement(Scroller, {
// contentColumn: true, fade: true, dark: true, children: [
// React.createElement(SettingsPanel, {key: "espanel", title: "Emote Settings", onChange: this.onChange, settings: this.emoteSettings, button: {
// title: "Clear Emote Cache",
// onClick: () => { Events.dispatch("emotes-clear"); /*EmoteModule.clearEmoteData(); EmoteModule.init();*/ }
// }}),
// React.createElement(Tools, {key: "tools"})
// ]});
// }
// get customCssComponent() {
// return React.createElement(Scroller, {contentColumn: true, fade: true, dark: true, children: [React.createElement(CssEditor, {key: "csseditor"}), React.createElement(Tools, {key: "tools"})]});
// }
// contentComponent(type) {
// const componentElement = type == "plugins" ? this.pluginsComponent : this.themesComponent;
// const prefix = type.replace("s", "");
// const settingsList = this;
// class ContentList extends React.Component {
// constructor(props) {
// super(props);
// this.onChange = this.onChange.bind(this);
// }
// componentDidMount() {
// Events.on(`${prefix}-reloaded`, this.onChange);
// Events.on(`${prefix}-loaded`, this.onChange);
// Events.on(`${prefix}-unloaded`, this.onChange);
// }
// componentWillUnmount() {
// Events.off(`${prefix}-reloaded`, this.onChange);
// Events.off(`${prefix}-loaded`, this.onChange);
// Events.off(`${prefix}-unloaded`, this.onChange);
// }
// onChange() {
// settingsList.sideBarOnClick(type);
// }
// render() {return componentElement;}
// }
// return React.createElement(ContentList);
// }
// get pluginsComponent() {
// const plugins = Object.keys(Plugins).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).reduce((arr, key) => {
// arr.push(React.createElement(PluginCard, {key: key, plugin: Plugins[key].plugin}));return arr;
// }, []);
// const list = React.createElement(List, {key: "plugin-list", className: "bda-slist", children: plugins});
// const refreshIcon = !SettingsCookie["fork-ps-5"] && React.createElement(ReloadIcon, {className: "bd-reload-header", size: "18px", onClick: async () => {
// PluginManager.updatePluginList();
// this.sideBarOnClick("plugins");
// }});
// const pfBtn = React.createElement("button", {key: "folder-button", className: "bd-pfbtn", onClick: () => { require("electron").shell.openItem(ContentManager.pluginsFolder); }}, "Open Plugin Folder");
// const contentColumn = React.createElement(ContentColumn, {key: "pcolumn", title: "Plugins", children: [refreshIcon, pfBtn, list]});
// return React.createElement(Scroller, {contentColumn: true, fade: true, dark: true, children: [contentColumn, React.createElement(Tools, {key: "tools"})]});
// }
// get themesComponent() {
// const themes = Object.keys(Themes).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).reduce((arr, key) => {
// arr.push(React.createElement(ThemeCard, {key: key, theme: Themes[key]}));return arr;
// }, []);
// const list = React.createElement(List, {key: "theme-list", className: "bda-slist", children: themes});
// const refreshIcon = !SettingsCookie["fork-ps-5"] && React.createElement(ReloadIcon, {className: "bd-reload-header", size: "18px", onClick: async () => {
// ThemeManager.updateThemeList();
// this.sideBarOnClick("themes");
// }});
// const tfBtn = React.createElement("button", {key: "folder-button", className: "bd-pfbtn", onClick: () => { require("electron").shell.openItem(ContentManager.themesFolder); }}, "Open Theme Folder");
// const contentColumn = React.createElement(ContentColumn, {key: "tcolumn", title: "Themes", children: [refreshIcon, tfBtn, list]});
// return React.createElement(Scroller, {contentColumn: true, fade: true, dark: true, children: [contentColumn, React.createElement(Tools, {key: "tools"})]});
// }
} }

View File

@ -1,5 +1,5 @@
import {React, ThemeManager} from "modules"; import {React, ThemeManager} from "modules";
import ReloadIcon from "../icons/reload"; // import ReloadIcon from "../icons/reload";
// import Toasts from "../toasts"; // import Toasts from "../toasts";
export default class V2C_ThemeCard extends React.Component { export default class V2C_ThemeCard extends React.Component {