Fix store to work under SWC/1.8.0 codebase

This commit is contained in:
Tropical 2022-10-11 19:37:28 -05:00
parent aa3f48785a
commit b798ef0c0d
24 changed files with 217 additions and 397 deletions

View File

@ -1,166 +0,0 @@
import {React, WebpackModules, WebAPI, Strings} from "modules";
import {Web} from "data";
import Builtin from "../../structs/builtin";
import PluginManager from "../../modules/pluginmanager";
import ThemeManager from "../../modules/thememanager";
import StoreCard from "../../ui/settings/addonlist/storecard";
import Modals from "../../ui/modals";
import Toasts from "../../ui/toasts";
import {URL} from "url";
const PROTOCOL_REGEX = new RegExp("<([^: >]+:/[^ >]+)>", "g");
const BD_PROTOCOL = "betterdiscord:";
const BD_PROTOCOL_REGEX = new RegExp(BD_PROTOCOL + "//", "i");
export default new class Store extends Builtin {
get name() {return "Store";}
get category() {return "addons";}
get id() {return "store";}
enabled() {
this.patchMarkdownParser();
this.patchTrustedModule();
this.patchEmbeds();
}
disabled() {
this.unpatchAll();
}
patchEmbeds() {
const MessageAccessories = WebpackModules.getByProps("MessageAccessories")?.MessageAccessories;
if (!MessageAccessories?.prototype.renderEmbeds) return;
this.instead(MessageAccessories.prototype, "renderEmbeds", (thisObject, methodArguments, renderEmbeds) => {
const embeds = Reflect.apply(renderEmbeds, thisObject, methodArguments);
const matchedProtocols = methodArguments[0]?.content.match(PROTOCOL_REGEX)?.map(match => {
const url = new URL(match.replace(/\s+/g, " ").replaceAll(/[<>]+/g, "").trim());
if (url.hostname === "addon" && url.protocol === BD_PROTOCOL) return url;
}).filter(m => m != null);
if (!matchedProtocols || !matchedProtocols?.length) return embeds;
return (embeds ?? []).concat(matchedProtocols.map((url) => {
const parameter = url.pathname.slice(1);
const addonId = Number.isNaN(Number(parameter)) ? parameter : Number(parameter);
if (!addonId) return null;
return React.createElement(EmbeddedStoreCard, {addonId});
}));
});
}
patchTrustedModule() {
const TrustedModule = WebpackModules.getByProps("isLinkTrusted", "handleClick");
if (!TrustedModule) return;
this.instead(TrustedModule, "handleClick", (thisObject, methodArguments, handleClick) => {
const [{href, onClick}, event] = methodArguments;
if (BD_PROTOCOL_REGEX.test(href)) {
if (typeof onClick === "function") onClick(event);
if (event) {
event.preventDefault();
event.stopPropagation();
}
return true;
}
return Reflect.apply(handleClick, thisObject, methodArguments);
});
}
patchMarkdownParser() {
const SimpleMarkdown = WebpackModules.getByProps("parseTopic", "defaultRules");
if (!SimpleMarkdown?.defaultRules?.link) return;
this.after(SimpleMarkdown.defaultRules.link, "react", (_, [{target: url}], returnValue) => {
if (!BD_PROTOCOL_REGEX.test(url)) return;
return this.renderContent(url, returnValue);
});
}
renderContent(path, link) {
const url = new URL(path);
if (url.hostname === "addon") {
const parameter = url.pathname.slice(1);
const addonId = Number.isNaN(Number(parameter)) ? parameter : Number(parameter);
if (!addonId) return link;
link.props.onClick = async (event) => {
const addon = await WebAPI.getAddon(addonId);
if (addon?.type) {
event.preventDefault();
window.open((Web.PAGES[addon.type] + (typeof(addonId) === "number" ? "?id=" : "/") + addonId), "_blank");
}
};
}
return link;
}
};
class EmbeddedStoreCard extends React.Component {
constructor(props) {
super(props);
this.state = {
addon: null
};
}
componentDidMount() {
WebAPI.getAddon(this.props.addonId).then(data => {
if (data?.id) this.setState({addon: data});
});
}
get manager() {
return this.state.addon?.type === "theme" ? ThemeManager : PluginManager;
}
async install(id, filename) {
try {
const contents = await WebAPI.getAddonContents(id);
this.manager.installAddon(contents, filename);
}
catch (error) {
Toasts.error(Strings.Store.downloadError.format({type: this.props.type}));
}
}
render() {
const {addon} = this.state;
return addon ? React.createElement(StoreCard, {
...addon,
key: addon.id,
className: "bd-store-card-embedded",
thumbnail: Web.ENDPOINTS.thumbnail(addon.thumbnail_url),
filename: addon.file_name,
releaseDate: new Date(addon.release_date),
isInstalled: this.manager.isLoaded(addon.file_name),
onInstall: () => Modals.showInstallationModal({
...addon,
thumbnail: Web.ENDPOINTS.thumbnail(addon.thumbnail_url),
filename: addon.file_name,
releaseDate: new Date(addon.release_date),
onInstall: () => this.install(addon.id, addon.file_name)
}),
onForceInstall: () => this.install(addon.id, addon.file_name),
onDelete: () => this.manager.confirmAddonDelete(addon.file_name),
onForceDelete: () => this.manager.deleteAddon(addon.file_name)
}) : null;
}
}

View File

@ -6,8 +6,6 @@ export {default as PublicServers} from "./general/publicservers";
export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as MediaKeys} from "./general/mediakeys";
export {default as Store} from "./addons/store";
// export {default as EmoteModule} from "./emotes/emotes";
// export {default as EmoteMenu} from "./emotes/emotemenu";
// export {default as EmoteAutocaps} from "./emotes/emoteautocaps";

View File

@ -56,7 +56,8 @@ export default new class PluginManager extends AddonManager {
deleteAddon: this.deleteAddon.bind(this),
confirmAddonDelete: this.confirmAddonDelete.bind(this),
isLoaded: this.isLoaded.bind(this),
installAddon: this.installAddon.bind(this)
installAddon: this.installAddon.bind(this),
prefix: this.prefix
})
});

View File

@ -1,6 +1,6 @@
import request from "request";
import fileSystem from "fs";
import {Config} from "data";
import {Config, Web} from "data";
import path from "path";
import Logger from "common/logger";
@ -21,13 +21,9 @@ import UpdaterPanel from "../ui/updater";
import DiscordModules from "./discordmodules";
const React = DiscordModules.React;
const UserSettingsWindow = WebpackModules.getByProps("updateAccount");
const base = "https://api.betterdiscord.app/v2/store/";
const route = r => `${base}${r}s`;
const redirect = addonId => `https://betterdiscord.app/gh-redirect?id=${addonId}`;
const route = r => `${Web.API_BASE}/${r}s`;
const getJSON = url => {
return new Promise(resolve => {
@ -183,7 +179,7 @@ class AddonUpdater {
async updateAddon(filename) {
const info = this.cache[filename];
request(redirect(info.id), (error, _, body) => {
request(Web.ENDPOINTS.githubRedirect(info.id), (error, _, body) => {
if (error) {
Logger.stacktrace("AddonUpdater", `Failed to download body for ${info.id}:`, error);
return;

View File

@ -1,6 +1,21 @@
import Logger from "common/logger";
export default class Utilities {
static splitArray(array, max) {
const newArray = [];
for (const child of array) {
let lastIndex = newArray.length ? newArray.length - 1 : 0;
if (!newArray[lastIndex]) {newArray.push([]);}
else if (newArray[lastIndex].length >= max) {
lastIndex++;
newArray.push([]);
}
newArray[lastIndex].push(child);
}
return newArray;
}
/**
* Generates an automatically memoizing version of an object.
* @author Zerebos

View File

@ -4,7 +4,7 @@ import Logger from "common/logger";
import Utilities from "./utilities";
import Strings from "./strings";
import https from "https";
import request from "request";
const API_CACHE = {plugins: [], themes: [], addon: []};
// const README_CACHE = {plugins: {}, themes: {}};
@ -17,6 +17,15 @@ export default new class WebAPI {
get pages() {return Web.PAGES;}
get tags() {return Web.TAGS;}
testJSON(data) {
try {
return JSON.parse(data);
}
catch (err) {
return false;
}
}
/**
* Fetches a list of all addons from the site.
* @param {"themes" | "plugins"} type - The type of the addon (theme or plugin).
@ -25,29 +34,17 @@ export default new class WebAPI {
getAddons(type) {
return new Promise((resolve, reject) => {
if (API_CACHE[type].length) resolve(API_CACHE[type]);
const request = https.get(Web.ENDPOINTS.store(type), (res) => {
const chunks = [];
res.on("data", chunk => chunks.push(chunk));
res.on("end", () => {
const json = Utilities.testJSON(chunks.join(""));
if (!Array.isArray(json)) return res.emit("error");
API_CACHE[type] = Utilities.splitArray(json, 30);
resolve(Utilities.splitArray(json, 30));
});
res.on("error", (error) => {
request(Web.ENDPOINTS.store(type), (error, _, body) => {
if (error) {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
});
});
}
request.on("error", (error) => {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
const json = this.testJSON(body);
API_CACHE[type] = Utilities.splitArray(json, 30);
resolve(Utilities.splitArray(json, 30));
});
});
}
@ -62,29 +59,17 @@ export default new class WebAPI {
const cacheMatch = API_CACHE.addon.find(a => a[typeof addon === "number" ? "id" : "name"] === addon);
if (cacheMatch) resolve(cacheMatch);
const request = https.get(Web.ENDPOINTS.addon(addon), (res) => {
const chunks = [];
res.on("data", chunk => chunks.push(chunk));
res.on("end", () => {
const json = Utilities.testJSON(chunks.join(""));
if (!json) return res.emit("error");
API_CACHE.addon.push(json);
resolve(json);
});
res.on("error", (error) => {
request(Web.ENDPOINTS.addon(addon), (error, _, body) => {
if (error) {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
});
});
}
request.on("error", (error) => {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
const json = this.testJSON(body);
API_CACHE.addon.push(json);
resolve(json);
});
});
}
@ -96,24 +81,16 @@ export default new class WebAPI {
*/
getAddonContents(id) {
return new Promise((resolve, reject) => {
const request = https.get(Web.ENDPOINTS.download(id), (res) => {
const chunks = [];
res.on("data", chunk => chunks.push(chunk));
res.on("end", () => {
const data = chunks.join("");
resolve(data);
});
const cacheMatch = API_CACHE.addon.find(addon => addon.id === id);
if (cacheMatch) resolve(cacheMatch);
res.on("error", (error) => {
request(Web.ENDPOINTS.download(id), (error, _, body) => {
if (error) {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
});
});
}
request.on("error", (error) => {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
resolve(body);
});
});
}

View File

@ -12,9 +12,4 @@
.bd-empty-image-message {
color: var(--header-secondary);
margin-bottom: 8px;
}
.bd-empty-image-container .bd-button {
margin-top: 10px;
padding: 10px 16px;
}

View File

@ -3,8 +3,9 @@
justify-content: center;
align-items: center;
background-color: #3E82E5;
color: #FFFFFF;
color: hsl(0, calc(var(--saturation-factor, 1) * 0%), 100%);
border-radius: 3px;
width: auto;
padding: 4px 8px;
transition: background-color 0.17s ease, color 0.17s ease, opacity 250ms ease;
}
@ -18,15 +19,15 @@
}
.bd-button.bd-button-success {
background-color: #43B581;
background-color: var(--button-positive-background, #43B581);
}
.bd-button.bd-button-success:hover {
background-color: #3CA374;
background-color: var(--button-positive-background-hover, #3CA374);
}
.bd-button.bd-button-success:active {
background-color: #369167;
background-color: var(--button-positive-background-active, #369167);
}
.bd-button.bd-button-warning {
@ -42,15 +43,15 @@
}
.bd-button.bd-button-danger {
background-color: #F04747;
background-color: var(--button-danger-background, #F04747);
}
.bd-button.bd-button-danger:hover {
background-color: #D84040;
background-color: var(--button-danger-background-hover, #D84040);
}
.bd-button.bd-button-danger:active {
background-color: #C03939;
background-color: var(--button-danger-background-active, #C03939);
}
.bd-button-disabled {
@ -59,4 +60,29 @@
.bd-button-disabled:hover {
cursor: not-allowed;
}
.bd-button.size-small {
height: 32px;
min-width: 60px;
min-height: 32px;
font-size: 14px;
font-weight: 500;
line-height: 16px;
padding: 2px 16px;
}
.bd-button.size-medium {
width: 96px;
height: 38px;
min-width: 96px;
min-height: 38px;
font-size: 14px;
font-weight: 500;
line-height: 16px;
padding: 2px 16px;
}
.bd-button:focus-visible {
box-shadow: 0 0 0 4px var(--focus-primary);
}

View File

@ -133,25 +133,26 @@
.bd-description-wrap .banner {
padding: 5px;
border: 2px solid gray;
border: 1px solid gray;
background: #26191E;
color: #ffffff;
font-weight: 700px;
border-radius: 5px;
font-size: 16px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
}
.banner.banner-danger {
border-color: #F04747;
background: #473C41;
border-color: var(--info-danger-foreground, #F04747);
background: var(--info-danger-background, #473C41);
color: var(--info-danger-text, #ffffff);
}
.banner .bd-icon {
fill: #ffffff;
margin-right: 5px;
height: 16px !important;
height: 24px;
}
.banner-danger .bd-icon {
@ -250,18 +251,6 @@
padding: 0 16px 16px 16px;
}
.bd-addon-modal-footer .bd-button {
background-color: #3e82e5;
}
.bd-addon-modal-footer .bd-button:hover {
background-color: rgb(56, 117, 206);
}
.bd-addon-modal-footer .bd-button:active {
background-color: rgb(50, 104, 183);
}
.bd-addon-views {
display: flex;
margin-left: 10px;

View File

@ -59,15 +59,10 @@
color: #fff;
}
.bd-modal .footer .bd-button {
min-width: 80px;
height: 38px;
}
@keyframes bd-modal-close {
to {transform: scale(0.7);}
}
@keyframes bd-modal-open {
from {transform: scale(0.7);}
}
}

View File

@ -243,7 +243,7 @@
height: 16px;
padding: 0 4px;
margin-left: 6px;
background-color: hsl(359, calc(var(--saturation-factor, 1) * 82.6%), 59.4%);
background-color: var(--status-danger);
color: #fff;
text-align: center;
font-size: 12px;
@ -337,10 +337,11 @@
font-weight: 600;
box-sizing: border-box;
justify-content: center;
height: 28px;
height: 38px;
min-width: 28px;
padding: 0 8px;
margin: 4px;
border-radius: 3px;
color: var(--header-primary);
background-color: transparent;
flex: 0 0 auto;
@ -349,11 +350,6 @@
width: min-content;
}
.bd-page-button .flexChild-3PzYmX {
display: flex;
align-items: center;
}
.bd-page-button svg {
display: inline-block;
width: 1em;
@ -365,24 +361,29 @@
background-color: var(--background-secondary-alt);
}
.bd-page-button:first-child {
padding-right: 12px;
.bd-page-button:focus-visible {
box-shadow: 0 0 0 4px var(--focus-primary);
}
.bd-page-button:last-child {
padding-left: 12px;
.bd-page-button:not(.bd-page-item):first-child {
padding-right: 12px;
}
.bd-page-button:first-child svg {
margin-right: 4px;
}
.bd-page-button:not(.bd-page-item):last-child {
padding-left: 12px;
}
.bd-page-button:last-child svg {
margin-left: 4px;
}
.bd-page-item {
border-radius: 14px;
height: 28px;
}
.bd-page-item.selected {
@ -509,6 +510,10 @@
margin-top: 16px;
}
.bd-installation-info *:focus-visible {
box-shadow: 0 0 0 4px var(--focus-primary);
}
.bd-installation-info li {
padding: 20px 0;
overflow: hidden;

View File

@ -30,7 +30,7 @@ class AddonError extends React.Component {
render() {
const {err} = this.props;
return <div key={`${err.type}-${this.props.index}`} className={Utilities.joinClassNames("bd-addon-error", (this.state.expanded) ? "expanded" : "collapsed")}>
return <div key={`${err.type}-${this.props.index}`} className={Utilities.className("bd-addon-error", (this.state.expanded) ? "expanded" : "collapsed")}>
<div className="bd-addon-error-header" onClick={() => {this.toggle();}} >
<div className="bd-addon-error-icon">
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}

View File

@ -3,7 +3,7 @@ import {React} from "modules";
export default class Error extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
return <svg viewBox="0 0 24 24" fill="#FFFFFF" width={size} height={size} onClick={this.props.onClick} className={this.props.className}>
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
</svg>;

View File

@ -2,6 +2,8 @@ import {React, Strings, WebpackModules} from "modules";
import {Web} from "data";
import Spinner from "./spinner";
import Tooltip from "./tooltip";
import Support from "./icons/support";
import Version from "./icons/version";
import Github from "./icons/github";
@ -9,14 +11,14 @@ import Author from "./icons/author";
import Description from "./icons/description";
import Clock from "./icons/clock";
const Button = WebpackModules.getByProps("BorderColors");
const {TooltipContainer: Tooltip} = WebpackModules.getByProps("TooltipContainer");
const {ModalRoot, ModalHeader, ModalContent, ModalCloseButton, ModalFooter} = WebpackModules.getByProps("ModalRoot");
const {Header, Content, CloseButton, Footer} = WebpackModules.getByProps("Header", "Footer");
const ModalRoot = WebpackModules.getModule(m => m?.toString?.()?.includes("ENTERING"), {searchExports: true});
export default class InstallationModal extends React.Component {
constructor() {
super(...arguments);
this.authorRef = React.createRef();
this.onKeyDown = this.onKeyDown.bind(this);
this.state = {
isInstalling: false
@ -40,18 +42,22 @@ export default class InstallationModal extends React.Component {
}
}
componentDidMount() {
Tooltip.create(this.authorRef.current, this.props.author.display_name);
}
render() {
const {name, id, description, author, releaseDate, type, version, filename} = this.props;
return <ModalRoot {...this.props} onKeyDown={this.onKeyDown} size="small" className="bd-installation-modal">
<ModalHeader className="bd-installation-header">
<Header className="bd-installation-header">
<img className="bd-installation-thumbnail" src={this.props.thumbnail} alt={`${name} thumbnail`}/>
<Tooltip className="bd-installation-icon" color="primary" position="top" text={author.display_name}>
<div ref={this.authorRef} className="bd-installation-icon">
<img alt={author.display_name} src={`https://github.com/${author.github_name}.png?size=44`} />
</Tooltip>
<ModalCloseButton onClick={this.props.onClose} className="bd-installation-close"/>
</ModalHeader>
<ModalContent className="bd-installation-content">
</div>
<CloseButton onClick={this.props.onClose} className="bd-installation-close"/>
</Header>
<Content className="bd-installation-content">
<h5 className="bd-installation-name">{name}</h5>
<div className="bd-installation-subtitle">
{Strings.Store.installConfirmation.format({type})}
@ -77,24 +83,32 @@ export default class InstallationModal extends React.Component {
<a href={`${Web.PAGES.developer}/${author.display_name}`} target="_blank" rel="noreferrer noopener">{author.display_name}</a>
</InfoItem>
</ul>
</ModalContent>
<ModalFooter>
<Button onClick={() => this.install(id, filename)} color={Button.Colors.GREEN} disabled={this.state.isInstalling}>
</Content>
<Footer className="bd-modal-footer">
<button className="bd-button size-medium bd-button-success" onClick={() => this.install(id, filename)} disabled={this.state.isInstalling}>
{this.state.isInstalling ? <Spinner type={Spinner.Type.PULSING_ELLIPSIS} /> : (Strings.Modals.install ?? "Install")}
</Button>
</ModalFooter>
</button>
</Footer>
</ModalRoot>;
}
}
class InfoItem extends React.Component {
constructor(props) {
super(props);
this.iconRef = React.createRef();
}
componentDidMount() {
new Tooltip(this.iconRef.current, this.props.label);
}
render() {
return <li id={this.props.id}>
<Tooltip className="bd-info-icon" color="primary" position="top" text={this.props.label}>
{
this.props.icon ? this.props.icon : <Support />
}
</Tooltip>
<div ref={this.iconRef} className="bd-info-icon">
{this.props.icon ? this.props.icon : <Support />}
</div>
<span>
{this.props.children}
</span>

View File

@ -6,7 +6,6 @@ import AddonErrorModal from "./addonerrormodal";
import ErrorBoundary from "./errorboundary";
import InstallationModal from "./installationmodal";
export default class Modals {
static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");}
@ -89,7 +88,7 @@ export default class Modals {
handleClose();
},
type: "button",
className: "bd-button"
className: "bd-button size-medium"
});
if (button.danger) buttonEl.classList.add("bd-button-danger")
@ -321,9 +320,9 @@ export default class Modals {
}
static showInstallationModal(options = {}) {
this.ModalActions.openModal(props => React.createElement(InstallationModal, {
this.ModalActions.openModal(props => React.createElement(ErrorBoundary, null, React.createElement(InstallationModal, {
...props,
...options
}));
})));
}
}

View File

@ -1,21 +1,21 @@
import Logger from "common/logger";
import {React, Strings, WebpackModules, DiscordModules} from "modules";
import SimpleMarkdown from "../../structs/markdown";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";
import CogIcon from "../icons/cog";
import Switch from "./components/switch";
import SimpleMarkdown from "../../../structs/markdown";
import Modals from "../../modals";
import Toasts from "../../toasts";
import Switch from "../components/switch";
import GitHubIcon from "../icons/github";
import MoneyIcon from "../icons/dollarsign";
import WebIcon from "../icons/globe";
import PatreonIcon from "../icons/patreon";
import SupportIcon from "../icons/support";
import ExtIcon from "../icons/extension";
import ErrorIcon from "../icons/error";
import ThemeIcon from "../icons/theme";
import Modals from "../modals";
import Toasts from "../toasts";
import GitHubIcon from "../../icons/github";
import MoneyIcon from "../../icons/dollarsign";
import WebIcon from "../../icons/globe";
import PatreonIcon from "../../icons/patreon";
import SupportIcon from "../../icons/support";
import ExtIcon from "../../icons/extension";
import ErrorIcon from "../../icons/error";
import ThemeIcon from "../../icons/theme";
import EditIcon from "../../icons/edit";
import DeleteIcon from "../../icons/delete";
import CogIcon from "../../icons/cog";
const LinkIcons = {
website: WebIcon,

View File

@ -1,4 +1,4 @@
import {React, Settings, Strings, Events, WebpackModules, Utilities, DataStore, DiscordClasses} from "modules";
import {React, Settings, Strings, Events, Utilities, DataStore, DiscordClasses} from "modules";
import {Web} from "data";
import {shell} from "electron";
@ -12,8 +12,6 @@ import SettingsTitle from "../title";
import StorePage from "./store";
import InstalledPage from "./installed";
const Button = WebpackModules.getByProps("BorderColors");
const CONTROLS = {
installed: {
sortOptions: [
@ -72,7 +70,7 @@ const PAGES = {
{Web.TAGS[type].map(tag => {
return <span
onClick={() => setState({selectedTag: tag})}
className={Utilities.joinClassNames({selected: state.selectedTag === tag})}
className={Utilities.className({selected: state.selectedTag === tag})}
>{tag}</span>;
})}
</div>
@ -265,13 +263,13 @@ export default class AddonList extends React.Component {
value={this.state.query}
placeholder={Strings.Addons.search.format({type: this.props.title})}
/>
<Button
size={Button.Sizes.SMALL}
<button
className="bd-button size-small"
onClick={() => this.openFolder(this.props.folder)}
>{Strings.Addons.openFolder.format({type: this.props.type})}</Button>
>{Strings.Addons.openFolder.format({type: this.props.type})}</button>
</div>
{this.pageControls}
<Divider className={Utilities.joinClassNames(DiscordClasses.Margins.marginTop20.toString(), DiscordClasses.Margins.marginBottom20.toString())} />
<Divider className={Utilities.className(DiscordClasses.Margins.marginTop20.toString(), DiscordClasses.Margins.marginBottom20.toString())} />
<Page
key={`${this.props.type}-${this.currentPage}`}
state={Object.assign({}, PAGES[this.currentPage].state, this.state)}

View File

@ -68,6 +68,7 @@ export default class InstalledPage extends React.Component {
type={this.props.type}
editAddon={this.props.editAddon.bind(this, addon.id)}
confirmAddonDelete={this.props.confirmAddonDelete.bind(this, addon)}
disabled={addon.partial}
key={addon.id}
enabled={addonState[addon.id]}
addon={addon}

View File

@ -1,4 +1,4 @@
import {React, Strings, Utilities, WebpackModules, DiscordClasses, WebAPI} from "modules";
import {React, Strings, Utilities, DiscordClasses, WebAPI} from "modules";
import {Web} from "data";
import Spinner from "../../spinner";
@ -9,8 +9,6 @@ import StoreCard from "./storecard";
import Modals from "../../modals";
import Toasts from "../../toasts";
const Button = WebpackModules.getByProps("DropdownSizes");
export default class StorePage extends React.Component {
constructor(props) {
super(props);
@ -37,8 +35,6 @@ export default class StorePage extends React.Component {
});
}
catch (error) {
console.log("error");
Modals.showConfirmationModal(Strings.Store.connectionError, Strings.Store.connectionErrorMessage, {
cancelText: Strings.Modals.close,
confirmText: Strings.Modals.retry,
@ -109,7 +105,7 @@ export default class StorePage extends React.Component {
return <div className="bd-addon-store">
{!this.state.isLoaded && <Spinner className="bd-store-spinner" type={Spinner.Type.SPINNING_CIRCLE}/>}
{(this.state.isLoaded && addons?.length && addons[this.state.selectedPage])
? <div className={Utilities.joinClassNames("bd-store-addons", this.props.view + "-view")}>
? <div className={Utilities.className("bd-store-addons", this.props.view + "-view")}>
{addons[this.state.selectedPage].map(addon => {
const thumbnail = Web.ENDPOINTS.thumbnail(addon.thumbnail_url);
@ -137,10 +133,10 @@ export default class StorePage extends React.Component {
: this.state.isLoaded && <NoResults />
}
{this.state.isLoaded && addons.length > 1 && <nav className="bd-page-control">
<Button look={Button.Looks.BLANK} className="bd-page-button" onClick={handleSelect(s => s - 1)} disabled={!canGoBackward}>
<button className="bd-page-button" onClick={handleSelect(s => s - 1)} disabled={!canGoBackward}>
<Previous />
{Strings.Store.back}
</Button>
</button>
<div className={`bd-page-buttons ${DiscordClasses.Scrollers.thin}`}>
{addons.length
? addons.map((_, index) => <div
@ -148,7 +144,7 @@ export default class StorePage extends React.Component {
aria-label={`Page ${index + 1}`}
aria-current={index === this.state.selectedPage ? "page" : undefined}
tabIndex="0"
className={Utilities.joinClassNames("bd-page-item bd-page-button", {selected: index === this.state.selectedPage})}
className={Utilities.className("bd-page-item bd-page-button", {selected: index === this.state.selectedPage})}
onClick={handleSelect(() => index)}
>
<span>{index + 1}</span>
@ -156,10 +152,10 @@ export default class StorePage extends React.Component {
: null
}
</div>
<Button look={Button.Looks.BLANK} className="bd-page-button" onClick={handleSelect(s => s + 1)} disabled={!canGoForward}>
<button className="bd-page-button" onClick={handleSelect(s => s + 1)} disabled={!canGoForward}>
{Strings.Store.next}
<Next />
</Button>
</button>
</nav>}
</div>;
}

View File

@ -1,13 +1,19 @@
import {React, Strings, Utilities, WebpackModules} from "modules";
import {React, Strings, Utilities} from "modules";
import Modals from "../../modals";
import Heart from "../../icons/heart";
import Download from "../../icons/download";
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
const Button = WebpackModules.getByProps("DropdownSizes");
import Tooltip from "../../tooltip";
export default class StoreCard extends React.PureComponent {
constructor(props) {
super(props);
this.authorRef = React.createRef();
this.newBadgeRef = React.createRef();
this.likesRef = React.createRef();
this.downloadsRef = React.createRef();
}
abbreviateStat(n) {
if (n < 1e3) return n;
if (n >= 1e3 && n < 1e6) return +(n / 1e3).toFixed(1) + "K";
@ -22,16 +28,6 @@ export default class StoreCard extends React.PureComponent {
return Math.max(months, 0);
}
preview(event) {
event.preventDefault();
event.stopPropagation();
Modals.showImageModal(this.props.thumbnail, {
width: Utilities.getNestedProp(event, "target.naturalWidth"),
height: Utilities.getNestedProp(event, "target.naturalHeight")
});
}
async onButtonClick(event) {
event.stopPropagation();
event.preventDefault();
@ -46,6 +42,15 @@ export default class StoreCard extends React.PureComponent {
}
}
componentDidMount() {
if (this.newBadgeRef?.current) {
Tooltip.create(this.newBadgeRef.current, Strings.Store.uploadDate.format({date: new Date(this.props.releaseDate).toLocaleString()}));
}
Tooltip.create(this.authorRef.current, this.props.author.display_name);
Tooltip.create(this.likesRef.current, Strings.Store.likesAmount.format({amount: this.props.likes}));
Tooltip.create(this.downloadsRef.current, Strings.Store.likesAmount.format({amount: this.props.downloads}));
}
render() {
const {name, description, author, tags, selectedTag, likes, downloads, releaseDate, thumbnail, className} = this.props;
@ -54,61 +59,40 @@ export default class StoreCard extends React.PureComponent {
<div className="bd-store-card-splash">
<img
key={thumbnail}
onClick={this.preview.bind(this)}
alt={name}
src={thumbnail}
/>
</div>
<div className="bd-store-card-icon">
<Tooltip color="primary" position="top" text={author.display_name}>
{props =>
<img {...props} alt={author.display_name} src={`https://github.com/${author.github_name}.png?size=44`} />
}
</Tooltip>
<img ref={this.authorRef} alt={author.display_name} src={`https://github.com/${author.github_name}.png?size=44`} />
</div>
</div>
<div className="bd-store-card-body">
<div className="bd-store-card-title">
<h5>{name}</h5>
{this.monthsAgo(releaseDate) <= 3
? <Tooltip color="primary" position="top" text={Strings.Store.uploadDate.format({date: new Date(releaseDate).toLocaleString()})}>
{props => <span {...props} className="bd-store-card-new-badge">{Strings.Store.new}</span>}
</Tooltip>
: null
}
{this.monthsAgo(releaseDate) <= 3 && <span ref={this.newBadgeRef} className="bd-store-card-new-badge">{Strings.Store.new}</span>}
</div>
<p>{description}</p>
<div className="bd-card-tags">
{
tags.map(tag => <span className={Utilities.joinClassNames({selected: tag === selectedTag})}>{tag}</span>)
}
{tags.map(tag => <span className={Utilities.className({selected: tag === selectedTag})}>{tag}</span>)}
</div>
<div className="bd-store-card-footer">
<div className="bd-store-card-stats">
<Tooltip color="primary" position="top" text={Strings.Store.likesAmount.format({amount: likes})}>
{props =>
<div {...props} className="bd-store-card-stat">
<Heart />
<span>{this.abbreviateStat(likes)}</span>
</div>
}
</Tooltip>
<Tooltip color="primary" position="top" text={Strings.Store.downloadsAmount.format({amount: downloads})}>
{props =>
<div {...props} className="bd-store-card-stat">
<Download />
<span>{this.abbreviateStat(downloads)}</span>
</div>
}
</Tooltip>
<div ref={this.likesRef} className="bd-store-card-stat">
<Heart />
<span>{this.abbreviateStat(likes)}</span>
</div>
<div ref={this.downloadsRef} className="bd-store-card-stat">
<Download />
<span>{this.abbreviateStat(downloads)}</span>
</div>
</div>
<Button
color={this.props.isInstalled ? Button.Colors.RED : Button.Colors.GREEN}
size={Button.Sizes.SMALL}
<button
className={Utilities.className("bd-button", "size-small", (this.props.isInstalled ? "bd-button-danger" : "bd-button-success"))}
onClick={this.onButtonClick.bind(this)}
>
{this.props.isInstalled ? Strings.Addons.deleteAddon : Strings.Addons.install}
</Button>
</button>
</div>
</div>
</div>;

View File

@ -30,11 +30,11 @@ export default class SearchBar extends React.Component {
render() {
const {className, size = Sizes.SMALL, placeholder, disabled = false} = this.props;
return <div className={Utilities.joinClassNames("bd-searchbar", className, {disabled}, `size-${size}`)}>
return <div className={Utilities.className("bd-searchbar", className, {disabled}, `size-${size}`)}>
<input onKeyDown={this.props.onKeyDown} onChange={this.onChange} disabled={disabled} type="text" placeholder={placeholder} maxLength="50" value={this.state.value} />
<div onClick={() => this.onChange({target: {value: ""}})} className={Utilities.joinClassNames("bd-search-icon", {clickable: this.state.hasContent})} tabIndex="-1" role="button">
<Close className={Utilities.joinClassNames("bd-search-close", {visible: this.state.hasContent})}/>
<Search className={Utilities.joinClassNames({visible: !this.state.hasContent})} />
<div onClick={() => this.onChange({target: {value: ""}})} className={Utilities.className("bd-search-icon", {clickable: this.state.hasContent})} tabIndex="-1" role="button">
<Close className={Utilities.className("bd-search-close", {visible: this.state.hasContent})}/>
<Search className={Utilities.className({visible: !this.state.hasContent})} />
</div>
</div>;
}

View File

@ -1,7 +1,5 @@
import {React} from "modules";
import Drawer from "./drawer";
import Title from "./title";
import Divider from "../divider";
import Switch from "./components/switch";
import Dropdown from "./components/dropdown";
import Number from "./components/number";
@ -12,7 +10,6 @@ import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
export default class Group extends React.Component {
constructor(props) {
super(props);

View File

@ -24,7 +24,7 @@ export default class Spinner extends React.Component {
render() {
const {className, type = Type.WANDERING_CUBES, ...props} = this.props;
return <div className={Utilities.joinClassNames("bd-spinner", `bd-spinner-${type}`, className)} {...props}>
return <div className={Utilities.className("bd-spinner", `bd-spinner-${type}`, className)} {...props}>
<span className="bd-spinner-inner">
{this.renderItems(type)}
</span>

View File

@ -49,7 +49,7 @@ export default class TabBar extends React.Component {
role="tab"
key={item.value}
tabIndex={((item.value === selected.value) && !item.disabled) ? "0" : "-1"}
className={Utilities.joinClassNames("bd-tab-item", {selected: item.value === selected.value}, {disabled: item.disabled})}
className={Utilities.className("bd-tab-item", {selected: item.value === selected.value}, {disabled: item.disabled})}
onClick={() => this.onChange(item)}
onKeyDown={this.onKeyDown}
aria-disabled={item.disabled}