rearrange + etags

This commit is contained in:
Zack Rauen 2019-06-27 19:50:20 -04:00
parent 1e084d31b4
commit 2d1d48e0fd
20 changed files with 1469 additions and 1678 deletions

File diff suppressed because it is too large Load Diff

1369
data/emotes/blacklist.json Normal file

File diff suppressed because it is too large Load Diff

1
data/emotes/bttv.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -168,7 +168,7 @@
"title": "{{name}} v{{version}} by {{author}}",
"openFolder": "Open {{type}} Folder",
"reload": "Reload",
"pluginSettings": "Settings",
"addonSettings": "Settings",
"website": "Website",
"source": "Source",
"server": "Support Server",

View File

@ -1,4 +1,3 @@
{
"LatestVersion":"0.3.2",
"CDN":"cdn.rawgit.com"
"injectorVersion": "0.3.2"
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@ import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings,
import BDEmote from "../ui/emote";
import Toasts from "../ui/toasts";
// import EmoteMenu from "./emotemenu";
const request = require("request");
const Emotes = {
TwitchGlobal: {},
@ -67,11 +68,11 @@ export default new class EmoteModule extends Builtin {
async enabled() {
Settings.registerCollection("emotes", "Emotes", EmoteConfig, {title: Strings.Emotes.clearEmotes, onClick: () => { this.clearEmoteData(); this.loadEmoteData(EmoteInfo); }});
// Disable emote module for now because it's annoying and slow
// await this.getBlacklist();
// await this.loadEmoteData(EmoteInfo);
await this.getBlacklist();
await this.loadEmoteData(EmoteInfo);
// while (!this.MessageContentComponent) await new Promise(resolve => setTimeout(resolve, 100));
// this.patchMessageContent();
while (!this.MessageContentComponent) await new Promise(resolve => setTimeout(resolve, 100));
this.patchMessageContent();
Events.on("emotes-favorite-added", this.addFavorite);
Events.on("emotes-favorite-removed", this.removeFavorite);
}
@ -234,7 +235,10 @@ export default new class EmoteModule extends Builtin {
}
downloadEmotes(emoteMeta) {
const request = require("request");
const repoFile = Utilities.repoUrl(`data/emotes/${emoteMeta.variable.toLowerCase()}.json`);
if (emoteMeta.url && !emoteMeta.backup) emoteMeta.backup = repoFile;
if (!emoteMeta.url) emoteMeta.url = repoFile;
const options = {
url: emoteMeta.url,
timeout: emoteMeta.timeout ? emoteMeta.timeout : 5000,
@ -244,10 +248,10 @@ export default new class EmoteModule extends Builtin {
this.log(`Downloading: ${emoteMeta.variable} (${emoteMeta.url})`);
return new Promise((resolve, reject) => {
request(options, (error, response, parsedData) => {
request.get(options, (error, response, parsedData) => {
if (error) {
this.stacktrace("Could not download " + emoteMeta.variable, error);
if (emoteMeta.backup) {
if (emoteMeta.backup || emoteMeta.url) {
emoteMeta.url = emoteMeta.backup;
emoteMeta.backup = null;
if (emoteMeta.backupParser) emoteMeta.parser = emoteMeta.backupParser;
@ -273,8 +277,9 @@ export default new class EmoteModule extends Builtin {
getBlacklist() {
return new Promise(resolve => {
$.getJSON(`https://rauenzi.github.io/BetterDiscordApp/data/emotefilter.json`, function (data) {
resolve(blacklist.push(...data.blacklist));
request.get({url: Utilities.repoUrl(`data/emotes/blacklist.json`), json: true}, (err, resp, data) => {
if (err || resp.statusCode != 200) return resolve();
resolve(blacklist.push(...data));
});
});
}

View File

@ -1,19 +1,16 @@
export default {
TwitchGlobal: {
url: "https://twitchemotes.com/api_cache/v3/global.json",
backup: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_twitch_global.json`,
variable: "TwitchGlobal",
getEmoteURL: (e) => `https://static-cdn.jtvnw.net/emoticons/v1/${e.id}/1.0`,
getOldData: (url, name) => { return {id: url.match(/\/([0-9]+)\//)[1], code: name, emoticon_set: 0, description: null}; }
getOldData: (url, name) => {return {id: url.match(/\/([0-9]+)\//)[1], code: name, emoticon_set: 0, description: null};}
},
TwitchSubscriber: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_twitch_subscriber.json`,
variable: "TwitchSubscriber",
getEmoteURL: (e) => `https://static-cdn.jtvnw.net/emoticons/v1/${e}/1.0`,
getOldData: (url) => url.match(/\/([0-9]+)\//)[1]
},
FrankerFaceZ: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_ffz.json`,
variable: "FrankerFaceZ",
getEmoteURL: (e) => `https://cdn.frankerfacez.com/emoticon/${e}/1`,
getOldData: (url) => url.match(/\/([0-9]+)\//)[1]
@ -33,7 +30,6 @@ export default {
getOldData: (url) => url
},
BTTV2: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_bttv.json`,
variable: "BTTV2",
oldVariable: "emotesBTTV2",
getEmoteURL: (e) => `https://cdn.betterttv.net/emote/${e}/1x`,

View File

@ -168,7 +168,7 @@ export default {
title: "{{name}} v{{version}} by {{author}}",
openFolder: "Open {{type}} Folder",
reload: "Reload",
pluginSettings: "Settings",
addonSettings: "Settings",
website: "Website",
source: "Source",
server: "Support Server",

View File

@ -17,44 +17,31 @@ export default new class DataStore {
constructor() {
this.data = {misc: {}};
this.pluginData = {};
this.localeHashes = {};
}
initialize() {
if (!fs.existsSync(this.baseFolder)) fs.mkdirSync(this.baseFolder);
if (!fs.existsSync(this.dataFolder)) fs.mkdirSync(this.dataFolder);
if (!fs.existsSync(this.localeFolder)) fs.mkdirSync(this.localeFolder);
if (!fs.existsSync(this.localeCache)) fs.writeFileSync(this.localeCache, JSON.stringify({}));
if (!fs.existsSync(this.BDFile)) fs.writeFileSync(this.BDFile, JSON.stringify(this.data.misc, null, 4));
if (!fs.existsSync(this.customCSS)) fs.writeFileSync(this.customCSS, "");
const dataFiles = fs.readdirSync(this.dataFolder).filter(f => !fs.statSync(path.resolve(this.dataFolder, f)).isDirectory() && f.endsWith(".json"));
for (const file of dataFiles) {
this.data[file.split(".")[0]] = __non_webpack_require__(path.resolve(this.dataFolder, file));
}
// this.data = __non_webpack_require__(this.BDFile);
// if (data.hasOwnProperty("settings")) this.data = data;
// if (!fs.existsSync(this.settingsFile)) return;
// let settings = __non_webpack_require__(this.settingsFile);
// fs.unlinkSync(this.settingsFile);
// if (settings.hasOwnProperty("settings")) settings = Object.assign({stable: {}, canary: {}, ptb: {}}, {[releaseChannel]: settings});
// else settings = Object.assign({stable: {}, canary: {}, ptb: {}}, settings);
// this.setBDData("settings", settings);
this.localeHashes = JSON.parse(fs.readFileSync(this.localeCache).toString());
}
get customCSS() {return this._customCSS || (this._customCSS = path.resolve(this.dataFolder, "custom.css"));}
get baseFolder() {return this._baseFolder || (this._baseFolder = path.resolve(Config.dataPath, "data"));}
get dataFolder() {return this._dataFolder || (this._dataFolder = path.resolve(this.baseFolder, `${releaseChannel}`));}
get localeFolder() {return this._localeFolder || (this._localeFolder = path.resolve(this.baseFolder, `locales`));}
get localeCache() {return this._localeCache || (this._localeCache = path.resolve(this.localeFolder, `.cache`));}
get BDFile() {return this._BDFile || (this._BDFile = path.resolve(Config.dataPath, "data", `${releaseChannel}.json`));}
// get settingsFile() {return this._settingsFile || (this._settingsFile = path.resolve(Config.dataPath, "bdsettings.json"));}
getPluginFile(pluginName) {return path.resolve(Config.dataPath, "plugins", pluginName + ".config.json");}
// getSettingGroup(key) {
// return this.data.settings[key] || null;
// }
// setSettingGroup(key, data) {
// this.data.settings[key] = data;
// fs.writeFileSync(this.BDFile, JSON.stringify(this.data, null, 4));
// }
_getFile(key) {
if (key == "settings" || key == "plugins" || key == "themes") return path.resolve(this.dataFolder, `${key}.json`);
@ -80,6 +67,15 @@ export default new class DataStore {
fs.writeFileSync(path.resolve(this.localeFolder, `${locale}.json`), JSON.stringify(strings, null, 4));
}
getLocaleHash(locale) {
return this.localeHashes[locale] || "";
}
saveLocaleHash(locale, hash) {
this.localeHashes[locale] = hash;
fs.writeFileSync(this.localeCache, JSON.stringify(this.localeHashes, null, 4));
}
getData(key) {
return this.data[key] || "";
}

View File

@ -15,7 +15,7 @@ export default new class LocaleManager {
this.locale = "";
this.strings = {};
}
async initialize() {
await this.setLocale(this.discordLocale);
Dispatcher.subscribe(DiscordConstants.ActionTypes.USER_SETTINGS_UPDATE, ({settings}) => {
@ -27,8 +27,7 @@ export default new class LocaleManager {
async setLocale(newLocale) {
let newStrings;
if (newLocale != this.defaultLocale) {
const savedStrings = DataStore.getLocale(newLocale);
newStrings = savedStrings || await this.downloadLocale(newLocale);
newStrings = await this.getLocaleStrings(newLocale);
if (!newStrings) return this.setLocale(this.defaultLocale);
}
else {
@ -39,16 +38,28 @@ export default new class LocaleManager {
Events.emit("strings-updated");
}
downloadLocale(locale) {
async getLocaleStrings(locale) {
const hash = DataStore.getLocaleHash(locale);
if (!hash) return await this.downloadLocale(locale);
const invalid = await this.downloadLocale(locale, hash);
if (!invalid) return DataStore.getLocale(locale);
return invalid;
}
downloadLocale(locale, hash = "") {
return new Promise(resolve => {
const options = {
url: `https://raw.githubusercontent.com/rauenzi/BetterDiscordApp/development/data/locales/${locale}.json`,//`https://rauenzi.github.io/BetterDiscordApp/data/locales/${discordLocale}.json`,
url: Utilities.repoUrl(`data/locales/${locale}.json`),
timeout: 2000,
json: true
};
if (hash) options.headers = {"If-None-Match": hash};
console.log(options.headers);
request.get(options, (err, resp, newStrings) => {
if (err || resp.statusCode !== 200) return resolve(null);
DataStore.saveLocale(locale, newStrings);
if (err || resp.statusCode !== 200) return resolve(null);
console.log(resp);
DataStore.saveLocale(locale, newStrings);
DataStore.saveLocaleHash(locale, resp.headers.etag);
resolve(newStrings);
});
});

View File

@ -1,7 +1,12 @@
import {Config} from "data";
import Logger from "./logger";
export default class Utilities {
static repoUrl(path) {
return `https://cdn.staticaly.com/gh/${Config.repo}/BetterDiscordApp/${Config.hash}/${path}`;
}
/**
* Parses a string of HTML and returns the results. If the second parameter is true,
* the parsed HTML will be returned as a document fragment {@see https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment}.

View File

@ -2,22 +2,21 @@ import {React, Logger, Strings} from "modules";
import CloseButton from "../icons/close";
import ReloadIcon from "../icons/reload";
export default class PluginCard extends React.Component {
export default class AddonCard extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.showSettings = this.showSettings.bind(this);
this.state = {
checked: this.props.enabled,
settingsOpen: false
};
this.hasSettings = typeof this.props.addon.plugin.getSettingsPanel === "function";
this.settingsPanel = "";
this.panelRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.reload = this.reload.bind(this);
// this.onReload = this.onReload.bind(this);
this.showSettings = this.showSettings.bind(this);
this.closeSettings = this.closeSettings.bind(this);
}
@ -50,6 +49,16 @@ export default class PluginCard extends React.Component {
getString(value) {return typeof value == "string" ? value : value.toString();}
onChange() {
this.setState({checked: !this.state.checked});
this.props.onChange && this.props.onChange(this.props.addon.id);
}
showSettings() {
if (!this.props.hasSettings) return;
this.setState({settingsOpen: true});
}
closeSettings() {
this.panelRef.current.innerHTML = "";
this.setState({settingsOpen: false});
@ -69,10 +78,10 @@ export default class PluginCard extends React.Component {
get settingsComponent() {
const addon = this.props.addon;
const name = this.getString(addon.name);
try { this.settingsPanel = addon.plugin.getSettingsPanel(); }
catch (err) { Logger.stacktrace("Plugin Settings", "Unable to get settings panel for " + name + ".", err); }
try { this.settingsPanel = this.props.getSettingsPanel(); }
catch (err) { Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err); }
const props = {id: `plugin-settings-${name}`, className: "plugin-settings", ref: this.panelRef};
const props = {id: `${name}-settings`, className: "addon-settings", ref: this.panelRef};
if (typeof(settingsPanel) == "string") props.dangerouslySetInnerHTML = this.settingsPanel;
return <li className="settings-open bd-switch-item">
@ -89,18 +98,18 @@ export default class PluginCard extends React.Component {
get footer() {
const links = ["website", "source"];
if (!links.some(l => this.props.addon[l]) && !this.hasSettings) return null;
if (!links.some(l => this.props.addon[l]) && !this.props.hasSettings) return null;
const linkComponents = links.map(this.buildLink.bind(this)).filter(c => c);
return <div className="bd-footer">
<span className="bd-links">{linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : [comp]).flat()}</span>
{this.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-plugin-settings" disabled={!this.state.checked}>{Strings.Addons.pluginSettings}</button>}
{this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.state.checked}>{Strings.Addons.addonSettings}</button>}
</div>;
}
render() {
if (this.state.settingsOpen) return this.settingsComponent;
const {addon} = this.props;
const addon = this.props.addon;
const name = this.getString(addon.name);
const author = this.getString(addon.author);
const description = this.getString(addon.description);
@ -111,7 +120,7 @@ export default class PluginCard extends React.Component {
<span className="bd-header-title">{this.buildTitle(name, version, author)}</span>
<div className="bd-controls">
{this.props.showReloadIcon && <ReloadIcon className="bd-reload bd-reload-card" onClick={this.reload} />}
<label className="bd-switch-wrapper bd-flex-child">
<label className="bd-switch-wrapper">
<input className="bd-switch-checkbox" checked={this.state.checked} onChange={this.onChange} type="checkbox" />
<div className={this.state.checked ? "bd-switch checked" : "bd-switch"} />
</label>
@ -121,14 +130,4 @@ export default class PluginCard extends React.Component {
{this.footer}
</li>;
}
onChange() {
this.setState({checked: !this.state.checked});
this.props.onChange && this.props.onChange(this.props.addon.id);
}
showSettings() {
if (!this.hasSettings) return;
this.setState({settingsOpen: true});
}
}
}

View File

@ -1,9 +1,8 @@
import {React, Settings, Strings} from "modules";
import SettingsTitle from "./title";
import PluginCard from "./plugincard";
import ThemeCard from "./themecard";
import ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard";
export default class AddonList extends React.Component {
@ -20,8 +19,9 @@ export default class AddonList extends React.Component {
<SettingsTitle key="title" text={title} button={button} otherChildren={showReloadIcon && <ReloadIcon className="bd-reload" onClick={this.reload.bind(this)} />} />,
<ul key="addonList" className={"bd-slist"}>
{addonList.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(addon => {
const CardType = addon.type ? PluginCard : ThemeCard;
return <CardType showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} />;
const hasSettings = addon.type && typeof(addon.plugin.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.plugin.getSettingsPanel.bind(addon.plugin);
return <AddonCard showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />;
})}
</ul>
];

View File

@ -1,134 +0,0 @@
import {React, Logger, Strings} from "modules";
import CloseButton from "../icons/close";
import ReloadIcon from "../icons/reload";
export default class PluginCard extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.showSettings = this.showSettings.bind(this);
this.state = {
checked: this.props.enabled,
settingsOpen: false
};
this.hasSettings = typeof this.props.addon.plugin.getSettingsPanel === "function";
this.settingsPanel = "";
this.panelRef = React.createRef();
this.reload = this.reload.bind(this);
// this.onReload = this.onReload.bind(this);
this.closeSettings = this.closeSettings.bind(this);
}
reload() {
if (!this.props.reload) return;
this.props.addon = this.props.reload(this.props.addon.id);
this.forceUpdate();
}
componentDidUpdate() {
if (!this.state.settingsOpen) return;
if (this.settingsPanel instanceof Node) this.panelRef.current.appendChild(this.settingsPanel);
// if (!SettingsCookie["fork-ps-3"]) return;
const isHidden = (container, element) => {
const cTop = container.scrollTop;
const cBottom = cTop + container.clientHeight;
const eTop = element.offsetTop;
const eBottom = eTop + element.clientHeight;
return (eTop < cTop || eBottom > cBottom);
};
const panel = $(this.panelRef.current);
const container = panel.parents(".scroller-2FKFPG");
if (!isHidden(container[0], panel[0])) return;
container.animate({
scrollTop: panel.offset().top - container.offset().top + container.scrollTop() - 30
}, 300);
}
getString(value) {return typeof value == "string" ? value : value.toString();}
closeSettings() {
this.panelRef.current.innerHTML = "";
this.setState({settingsOpen: false});
}
buildTitle(name, version, author) {
const title = Strings.Addons.title.split(/({{[A-Za-z]+}})/);
const nameIndex = title.findIndex(s => s == "{{name}}");
if (nameIndex) title[nameIndex] = React.createElement("span", {className: "bd-name"}, name);
const versionIndex = title.findIndex(s => s == "{{version}}");
if (nameIndex) title[versionIndex] = React.createElement("span", {className: "bd-version"}, version);
const authorIndex = title.findIndex(s => s == "{{author}}");
if (nameIndex) title[authorIndex] = React.createElement("span", {className: "bd-author"}, author);
return title.flat();
}
get settingsComponent() {
const addon = this.props.addon;
const name = this.getString(addon.name);
try { this.settingsPanel = addon.plugin.getSettingsPanel(); }
catch (err) { Logger.stacktrace("Plugin Settings", "Unable to get settings panel for " + name + ".", err); }
const props = {id: `plugin-settings-${name}`, className: "plugin-settings", ref: this.panelRef};
if (typeof(settingsPanel) == "string") props.dangerouslySetInnerHTML = this.settingsPanel;
return <li className="settings-open bd-switch-item">
<div className="bd-close" onClick={this.closeSettings}><CloseButton /></div>
<div {...props}>{this.settingsPanel instanceof React.Component ? this.settingsPanel : null}</div>
</li>;
}
buildLink(which) {
const url = this.props.addon[which];
if (!url) return null;
return <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{Strings.Addons[which]}</a>;
}
get footer() {
const links = ["website", "source"];
if (!links.some(l => this.props.addon[l]) && !this.hasSettings) return null;
const linkComponents = links.map(this.buildLink.bind(this)).filter(c => c);
return <div className="bd-footer">
<span className="bd-links">{linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : [comp]).flat()}</span>
{this.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-plugin-settings" disabled={!this.state.checked}>{Strings.Addons.pluginSettings}</button>}
</div>;
}
render() {
if (this.state.settingsOpen) return this.settingsComponent;
const {addon} = this.props;
const name = this.getString(addon.name);
const author = this.getString(addon.author);
const description = this.getString(addon.description);
const version = this.getString(addon.version);
return <li dataName={name} dataVersion={version} className="settings-closed bd-switch-item">
<div className="bd-header">
<span className="bd-header-title">{this.buildTitle(name, version, author)}</span>
<div className="bd-controls">
{this.props.showReloadIcon && <ReloadIcon className="bd-reload bd-reload-card" onClick={this.reload} />}
<label className="bd-switch-wrapper bd-flex-child">
<input className="bd-switch-checkbox" checked={this.state.checked} onChange={this.onChange} type="checkbox" />
<div className={this.state.checked ? "bd-switch checked" : "bd-switch"} />
</label>
</div>
</div>
<div className="bd-description-wrap scroller-wrap fade"><div className="bd-description scroller">{description}</div></div>
{this.footer}
</li>;
}
onChange() {
this.setState({checked: !this.state.checked});
this.props.onChange && this.props.onChange(this.props.addon.id);
}
showSettings() {
if (!this.hasSettings) return;
this.setState({settingsOpen: true});
}
}

View File

@ -1,73 +0,0 @@
import {React, Strings} from "modules";
import ReloadIcon from "../icons/reload";
// import Toasts from "../toasts";
export default class ThemeCard extends React.Component {
constructor(props) {
super(props);
this.state = {
checked: this.props.enabled,
reloads: 0
};
this.onChange = this.onChange.bind(this);
this.reload = this.reload.bind(this);
}
reload() {
if (!this.props.reload) return;
this.props.addon = this.props.reload(this.props.addon.id);
this.forceUpdate();
}
buildTitle(name, version, author) {
const title = Strings.Addons.title.split(/({{[A-Za-z]+}})/);
const nameIndex = title.findIndex(s => s == "{{name}}");
if (nameIndex) title[nameIndex] = React.createElement("span", {className: "bd-name"}, name);
const versionIndex = title.findIndex(s => s == "{{version}}");
if (nameIndex) title[versionIndex] = React.createElement("span", {className: "bd-version"}, version);
const authorIndex = title.findIndex(s => s == "{{author}}");
if (nameIndex) title[authorIndex] = React.createElement("span", {className: "bd-author"}, author);
return title.flat();
}
render() {
const {addon} = this.props;
const name = addon.name;
const description = addon.description;
const version = addon.version;
const author = addon.author;
const website = addon.website;
const source = addon.source;
return React.createElement("li", {"data-name": name, "data-version": version, "className": "settings-closed bd-switch-item"},
React.createElement("div", {className: "bd-header"},
React.createElement("span", {className: "bd-header-title"},
this.buildTitle(name, version, author)
),
React.createElement("div", {className: "bd-controls"},
this.props.showReloadIcon && React.createElement(ReloadIcon, {className: "bd-reload bd-reload-card", onClick: this.reload}),
React.createElement("label", {className: "bd-switch-wrapper bd-flex-child", style: {flex: "0 0 auto"}},
React.createElement("input", {checked: this.state.checked, onChange: this.onChange, className: "bd-switch-checkbox", type: "checkbox"}),
React.createElement("div", {className: this.state.checked ? "bd-switch checked" : "bd-switch"})
)
)
),
React.createElement("div", {className: "bd-description-wrap scroller-wrap fade"},
React.createElement("div", {className: "bd-description scroller"}, description)
),
(website || source) && React.createElement("div", {className: "bd-footer"},
React.createElement("span", {className: "bd-links"},
website && React.createElement("a", {className: "bd-link", href: website, target: "_blank"}, "Website"),
website && source && " | ",
source && React.createElement("a", {className: "bd-link", href: source, target: "_blank"}, "Source")
)
)
);
}
onChange() {
this.setState({checked: !this.state.checked});
this.props.onChange && this.props.onChange(this.props.addon.id);
}
}