This commit is contained in:
Tropical 2022-10-13 23:21:29 -05:00 committed by GitHub
commit ad9e4cbc9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 2260 additions and 464 deletions

View File

@ -228,7 +228,7 @@
"compileError": "Nepodařilo se zkompilovat.",
"wasUnloaded": "Doplněk {{name}} byl odnačten.",
"blankSlateHeader": "Nemáš žádné/žádný {{type}}!",
"blankSlateMessage": "Najdi si nějaké na [tomto webu]({{link}}) a přidej je do své {{type}} složky."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Máš neuložené změny ve vlastním CSS. Zavřením tohoto okna je všechny ztratíš.",

View File

@ -228,7 +228,7 @@
"compileError": "Konnte nicht kompiliert werden.",
"wasUnloaded": "{{name}} wurde entladen.",
"blankSlateHeader": "Du besitzt keine {{type}}s!",
"blankSlateMessage": "Lade dir welche von [this website]({{link}}) herunter und füge sie deinem {{type}} Ordner hinzu."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Du hast ungespeicherte Änderungen im Custom-CSS. Durch das Schließen dieses Fensters verlierst du alle Änderungen.",

View File

@ -33,6 +33,14 @@
},
"addons": {
"name": "Addon Manager",
"store": {
"name": "Addon Store",
"note": "Enables the Store, allowing you to download themes and plugins remotely."
},
"autoEnable": {
"name": "Enable Addons on Install",
"note": "Automatically enables addons when they are installed via the store."
},
"addonErrors": {
"name": "Show Addon Errors",
"note": "Shows a modal with plugin/theme errors"
@ -200,6 +208,12 @@
}
},
"Addons": {
"installed": "Installed",
"install": "Install",
"store": "Store",
"view": "View",
"list": "List",
"grid": "Grid",
"title": "{{name}} v{{version}} by {{author}}",
"byline": "by {{author}}",
"openFolder": "Open {{type}} Folder",
@ -213,10 +227,13 @@
"name": "Name",
"author": "Author",
"version": "Version",
"description": "Description",
"added": "Date Added",
"uploaded": "Date Uploaded",
"modified": "Date Modified",
"isEnabled": "Enabled",
"search": "Search {{type}}",
"noResults": "We searched far and wide. Unfortunately, no results were found.",
"editAddon": "Edit",
"deleteAddon": "Delete",
"confirmDelete": "Are you sure you want to delete {{name}}?",
@ -240,7 +257,7 @@
"wasUnloaded": "{{name}} was unloaded.",
"wasLoaded": "{{name}} v{{version}} was loaded.",
"blankSlateHeader": "You don't have any {{type}}s!",
"blankSlateMessage": "Grab some from [this website]({{link}}) and add them to your {{type}} folder."
"blankSlateMessage": "Download some from the store to get started."
},
"CustomCSS": {
"confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",
@ -249,7 +266,22 @@
"openNative": "Open in System Editor",
"openDetached": "Detach Window",
"settings": "Editor Settings",
"editorTitle": "Custom CSS Editor"
"editorTitle": "Custom CSS Editor",
"writeError": "Failed to write {{type}} to disk."
},
"Store": {
"connectionError": "Failed to connect to Store.",
"connectionErrorMessage": "There was an error connecting to the store, it's possible our website/api is down. Would you like to retry?",
"downloadError": "Failed to download {{type}}.",
"installConfirmation": "Do you want to add this to your installed {{type}}s?",
"back": "Back",
"next": "Next",
"new": "New",
"likes": "Likes",
"likesAmount": "{{amount}} likes",
"downloads": "Downloads",
"downloadsAmount": "{{amount}} downloads",
"uploadDate": "Uploaded on {{date}}"
},
"Emotes": {
"loading": "Loading emotes in the background do not reload.",
@ -287,6 +319,7 @@
"cancel": "Cancel",
"nevermind": "Nevermind",
"close": "Close",
"retry": "Retry",
"name": "Name",
"message": "Message",
"error": "Error",

View File

@ -228,7 +228,7 @@
"compileError": "No se ha podido compilar.",
"wasUnloaded": "{{name}} ha sido descargado.",
"blankSlateHeader": "¡No tienes {{type}}s!",
"blankSlateMessage": "Consigue alguno en [esta página web]({{link}}) y añadelos a tu carpeta de {{type}}."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Tienes cambios sin guardar en tu CSS Personalizado. Al cerrar esta ventana se perderán todos los cambios.",

View File

@ -228,7 +228,7 @@
"compileError": "Compilation impossible.",
"wasUnloaded": "{{name}} a été déchargé.",
"blankSlateHeader": "Vous n'avez pas de {{type}}!",
"blankSlateMessage": "Récupère-les depuis [ce site]({{link}}) et ajoute-les dans ton dossier {{type}}."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Vous avez des changements non sauvegardés dans votre CSS Personnalisé. Fermer cette fenêtre vous fera perdre ces changements.",

View File

@ -228,7 +228,7 @@
"compileError": "Błąd kompilacji.",
"wasUnloaded": "Dodatek {{name}} został odładowany.",
"blankSlateHeader": "Wygląda na to, że nic tu nie ma!",
"blankSlateMessage": "Pobierz nowe dodatki z [tej strony]({{link}}) i przenieś je do odpowiedniego folderu."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Masz niezapisane zmiany w swoim niestandardowym kodzie CSS. Zamknięcie tego okienka spowoduje utratę danych.",

View File

@ -228,7 +228,7 @@
"compileError": "Não foi possível compilar.",
"wasUnloaded": "{{name}} foi descarregado.",
"blankSlateHeader": "Você não tem nenhum {{type}}!",
"blankSlateMessage": "Baixe alguns [desse website]({{link}}) e os adicione na sua pasta de {{type}}s."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Você tem mudanças ainda não salvas no seu CSS Personalizado. Fechar essa janela fará você perder todas elas.",

View File

@ -228,7 +228,7 @@
"compileError": "Não foi possível compilar.",
"wasUnloaded": "{{name}} foi descarregado.",
"blankSlateHeader": "Não tem nenhum {{type}}!",
"blankSlateMessage": "Pega um pouco deste [this website]({{link}}) e adiciona eles à tua pasta {{type}}."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Tem alterações não guardadas no CSS Personalizado. Se fechar esta janela irá perder essas alterações.",

View File

@ -228,7 +228,7 @@
"compileError": "Nu s-a putut incărca.",
"wasUnloaded": "{{name}} a fost dezactivat.",
"blankSlateHeader": "Nu ai nici un {{type}}!",
"blankSlateMessage": "Descarcă câteva din [acest website]({{link}}) și adaugă-le in folder-ul tău {{type}}."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Ai modificări nesalvate in CSS-ul personalizat. Închiderea acestei ferestre va duce la anularea schimbărilor.",

View File

@ -228,7 +228,7 @@
"compileError": "Не удалось скомпилировать.",
"wasUnloaded": "{{name}} был выгружен.",
"blankSlateHeader": "У вас нету {{type}}",
"blankSlateMessage": "Возьмите что-то с [этого сайта]({{link}}) и добавьте их в свою папку {{type}}."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "У вас есть не сохраненные изменения в быстром CSS. При закрытии этого окна все изменения будут потеряны.",

View File

@ -228,7 +228,7 @@
"compileError": "Nepodarilo sa zostaviť.",
"wasUnloaded": "{{name}} bol odpojený.",
"blankSlateHeader": "Nenachádza sa žiadny/žiadna {{type}}!",
"blankSlateMessage": "Nájdi nejaké na [tejto stránke]({{link}}) a pridaj ich do zložky pre {{type}}."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Máte neuložené zmeny pre Custom CSS. Zatvorením tohto okna stratíte všetky zmeny.",

View File

@ -228,7 +228,7 @@
"compileError": "Derlenemedi.",
"wasUnloaded": "{{name}} çıkarıldı.",
"blankSlateHeader": "Hiç {{type}}(a/e)arın yok!",
"blankSlateMessage": "[Bu siteden]({{link}}) al ve {{type}} klasörüne ekle."
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "Özel CSS'inde kaydedilmemiş değişimler var. Bu pencereyi kapatmak değişikliklerin kaybolmasına yol açar.",

View File

@ -228,7 +228,7 @@
"compileError": "无法编译。",
"wasUnloaded": "已卸载{{name}}。",
"blankSlateHeader": "你还添加任何{{type}}",
"blankSlateMessage": "你可以在[此网站]({{link}})中获取,并将其添加到你的{{type}}文件夹中。"
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "你还有未保存的更改,关闭此窗口将失去所有对自定义样式所做的更改。",

View File

@ -228,7 +228,7 @@
"compileError": "無法編譯。",
"wasUnloaded": "已卸載{{name}}。",
"blankSlateHeader": "你还未添加任何{{type}}",
"blankSlateMessage": "你可以在[此网站]({{link}})中獲取,並將其添加到你的{{type}}資料夾中。"
"blankSlateMessage": ""
},
"CustomCSS": {
"confirmationText": "你还有未保存的更改,關閉此窗口將失去所有對客製化樣式所做的更改。",

View File

@ -1,4 +1,5 @@
export {default as Config} from "./config";
export {default as EmoteConfig} from "./emotesettings";
export {default as SettingsConfig} from "./settings";
export {default as Changelog} from "./changelog";
export {default as Changelog} from "./changelog";
export {default as Web} from "./web";

View File

@ -17,6 +17,8 @@ export default [
collapsible: true,
shown: false,
settings: [
{type: "switch", id: "store", value: true},
{type: "switch", id: "autoEnable", value: false},
{type: "switch", id: "addonErrors", value: true},
{type: "dropdown", id: "editAction", value: "detached", options: [{value: "detached"}, {value: "system"}]}
]

32
renderer/src/data/web.js Normal file
View File

@ -0,0 +1,32 @@
const WEB_HOSTNAME = "betterdiscord.app";
const API_VERSION = "v2";
const API_BASE = `https://api.${WEB_HOSTNAME}/${API_VERSION}`;
export default {
WEB_HOSTNAME,
API_VERSION,
API_BASE,
TAGS: {
theme: ["all", "flat", "transparent", "layout", "customizable", "fiction", "nature", "space", "dark", "light", "game", "anime", "red", "orange", "green", "purple", "black", "other", "yellow", "blue", "abstract"],
plugin: ["all", "fun", "roles", "activity", "status", "game", "edit", "library", "notifications", "emotes", "channels", "shortcut", "enhancement", "servers", "chat", "security", "organization", "friends", "members", "utility", "developers", "search", "text", "voice"]
},
ENDPOINTS: {
store: type => `${API_BASE}/store/${type}`,
addon: addon => `${API_BASE}/store/${addon}`,
download: id => `https://${WEB_HOSTNAME}/download?id=${id}`,
githubRedirect: id => `https://${WEB_HOSTNAME}/gh-redirect?id=${id}`,
thumbnail: thumbnailUrl => `https://${WEB_HOSTNAME}${thumbnailUrl ?? "/resources/store/missing.svg"}`,
},
PAGES: {
home: `https://${WEB_HOSTNAME}/`,
themes: `https://${WEB_HOSTNAME}/themes`,
plugins: `https://${WEB_HOSTNAME}/plugins`,
theme: `https://${WEB_HOSTNAME}/theme`,
plugin: `https://${WEB_HOSTNAME}/plugin`,
developers: `https://${WEB_HOSTNAME}/developers`,
developer: `https://${WEB_HOSTNAME}/developer`,
merch: `https://${WEB_HOSTNAME}/merch`,
faq: `https://${WEB_HOSTNAME}/faq`,
docs: `https://${WEB_HOSTNAME}/docs`
}
};

View File

@ -8,6 +8,7 @@ import DiscordModules from "./discordmodules";
import Strings from "./strings";
import AddonEditor from "../ui/misc/addoneditor";
import FloatingWindows from "../ui/floatingwindows";
import Modals from "../ui/modals";
const React = DiscordModules.React;
@ -27,7 +28,7 @@ const stripBOM = function(fileContent) {
};
export default class AddonManager {
get name() {return "";}
get extension() {return "";}
get duplicatePattern() {return /./;}
@ -262,8 +263,8 @@ export default class AddonManager {
return this.addonList.find(c => c.id == idOrFile || c.filename == idOrFile);
}
enableAddon(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
enableAddon(idOrFileOrAddon) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.getAddon(idOrFileOrAddon) : idOrFileOrAddon;
if (!addon || addon.partial) return;
if (this.state[addon.id]) return;
this.state[addon.id] = true;
@ -271,8 +272,8 @@ export default class AddonManager {
this.saveState();
}
disableAddon(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
disableAddon(idOrFileOrAddon) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.getAddon(idOrFileOrAddon) : idOrFileOrAddon;
if (!addon || addon.partial) return;
if (!this.state[addon.id]) return;
this.state[addon.id] = false;
@ -285,6 +286,21 @@ export default class AddonManager {
else this.enableAddon(id);
}
installAddon(text, filename, shouldToast = true) {
const enable = (id) => {
const installation = this.getAddon(id);
this.enableAddon(installation);
Events.off(`${this.prefix}-loaded`, enable);
};
fs.writeFileSync(path.resolve(this.addonFolder, filename), text, (error) => {
if (error) {
Logger.stacktrace(this.name, Strings.Addons.writeError.format({type: this.prefix}), error);
if (shouldToast) Toasts.error(Strings.Addons.writeError.format({type: this.prefix}));
}
});
if (Settings.get("settings", "addons", "autoEnable")) Events.on(`${this.prefix}-loaded`, enable);
}
loadNewAddons() {
const files = fs.readdirSync(this.addonFolder);
const removed = this.addonList.filter(t => !files.includes(t.filename)).map(c => c.id);
@ -354,6 +370,19 @@ export default class AddonManager {
return this.openDetached(addon);
}
confirmAddonDelete(idOrFileOrAddon, options = {}) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {
if (typeof(options.onDelete) === "function") options.onDelete();
this.deleteAddon(addon);
}
});
}
openDetached(addon) {
const fullPath = path.resolve(this.addonFolder, addon.filename);
const content = fs.readFileSync(fullPath).toString();

View File

@ -117,8 +117,8 @@ export default new class DataStore {
return path.resolve(this.dataFolder, `misc.json`);
}
getBDData(key) {
return this.data.misc[key] || "";
getBDData(key, defaultValue = "") {
return this.data.misc[key] ?? defaultValue;
}
setBDData(key, value) {

View File

@ -15,4 +15,5 @@ export {default as LocaleManager} from "./localemanager";
export {default as Strings} from "./strings";
export {default as IPC} from "./ipc";
export {default as Logger} from "common/logger";
export {default as DiscordClasses} from "./discordclasses";
export {default as DiscordClasses} from "./discordclasses";
export {default as WebAPI} from "./webapi";

View File

@ -42,6 +42,7 @@ export default new class PluginManager extends AddonManager {
initialize() {
const errors = super.initialize();
this.setupFunctions();
Settings.registerPanel("plugins", Strings.Panels.plugins, {
order: 3,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
@ -53,9 +54,13 @@ export default new class PluginManager extends AddonManager {
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
confirmAddonDelete: this.confirmAddonDelete.bind(this),
isLoaded: this.isLoaded.bind(this),
installAddon: this.installAddon.bind(this),
prefix: this.prefix
})
});
return errors;
}
@ -63,8 +68,8 @@ export default new class PluginManager extends AddonManager {
updatePluginList() {return this.updateList();}
loadAllPlugins() {return this.loadAllAddons();}
enablePlugin(idOrAddon) {return this.enableAddon(idOrAddon);}
disablePlugin(idOrAddon) {return this.disableAddon(idOrAddon);}
enablePlugin(idOrFileAddon) {return this.enableAddon(idOrFileAddon);}
disablePlugin(idOrFileAddon) {return this.disableAddon(idOrFileAddon);}
togglePlugin(id) {return this.toggleAddon(id);}
unloadPlugin(idOrFileOrAddon) {return this.unloadAddon(idOrFileOrAddon);}

View File

@ -32,6 +32,9 @@ export default new class ThemeManager extends AddonManager {
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
confirmAddonDelete: this.confirmAddonDelete.bind(this),
isLoaded: this.isLoaded.bind(this),
installAddon: this.installAddon.bind(this),
prefix: this.prefix
})
});
@ -42,8 +45,8 @@ export default new class ThemeManager extends AddonManager {
updateThemeList() {return this.updateList();}
loadAllThemes() {return this.loadAllAddons();}
enableTheme(idOrAddon) {return this.enableAddon(idOrAddon);}
disableTheme(idOrAddon) {return this.disableAddon(idOrAddon);}
enableTheme(idOrFileAddon) {return this.enableAddon(idOrFileAddon);}
disableTheme(idOrFileAddon) {return this.disableAddon(idOrFileAddon);}
toggleTheme(id) {return this.toggleAddon(id);}
unloadTheme(idOrFileOrAddon) {return this.unloadAddon(idOrFileOrAddon);}

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

@ -0,0 +1,97 @@
import {Web} from "data";
import Logger from "common/logger";
import Utilities from "./utilities";
import Strings from "./strings";
import request from "request";
const API_CACHE = {plugins: [], themes: [], addon: []};
// const README_CACHE = {plugins: {}, themes: {}};
export default new class WebAPI {
get apiVersion() {return Web.API_VERSION;}
get webHostname() {return Web.WEB_HOSTNAME;}
get apiBase() {return Web.API_BASE;}
get endpoints() {return Web.ENDPOINTS;}
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).
* @returns {Promise<Array<Object>>}
*/
getAddons(type) {
return new Promise((resolve, reject) => {
if (API_CACHE[type].length) resolve(API_CACHE[type]);
request(Web.ENDPOINTS.store(type), (error, _, body) => {
if (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));
});
});
}
/**
* Fetches a single addon by name from the site.
* @param {string|number} addon - The name or ID of the addon to fetch.
* @returns {Promise<Object>}
*/
getAddon(addon) {
return new Promise((resolve, reject) => {
const cacheMatch = API_CACHE.addon.find(a => a[typeof addon === "number" ? "id" : "name"] === addon);
if (cacheMatch) resolve(cacheMatch);
request(Web.ENDPOINTS.addon(addon), (error, _, body) => {
if (error) {
Logger.stacktrace("WebAPI", Strings.Store.connectionError, error);
reject(error);
}
const json = this.testJSON(body);
API_CACHE.addon.push(json);
resolve(json);
});
});
}
/**
* Fetches and return's an addon's raw text content.
* @param {number} id - The ID of the addon to fetch.
* @returns {Promise<Object>}
*/
getAddonContents(id) {
return new Promise((resolve, reject) => {
const cacheMatch = API_CACHE.addon.find(addon => addon.id === id);
if (cacheMatch) resolve(cacheMatch);
request(Web.ENDPOINTS.download(id), (error, _, body) => {
if (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

@ -1,27 +1,123 @@
.bd-search-wrapper {
padding: 3px;
border-radius: 3px;
outline: none;
border: 0;
/* Searchbars */
.bd-searchbar {
padding: 1px;
border-radius: 4px;
background-color: var(--background-tertiary);
color: var(--text-muted);
display: flex;
align-items: center;
}
.bd-search {
padding: 2px 3px;
.bd-searchbar.disabled input,
.bd-searchbar.disabled .bd-search-icon {
opacity: .3;
cursor: not-allowed;
}
.bd-searchbar input {
background: none;
border: 0;
color: var(--text-normal);
border: none;
resize: none;
flex: 1;
min-width: 48px;
margin: 1px;
-webkit-appearance: none;
color: var(--text-normal);
}
.bd-search::-webkit-input-placeholder {
.bd-searchbar input::-webkit-input-placeholder {
color: var(--text-muted);
}
.bd-search-wrapper > svg {
margin-right: 2px;
fill: var(--interactive-normal);
.bd-search-icon svg {
position: absolute;
box-sizing: border-box;
opacity: 0;
width: 20px;
height: 20px;
z-index: 2;
transition: transform 0.1s ease-out;
transform: rotate(90deg);
}
.bd-search-icon svg.visible {
transform: rotate(0);
opacity: 1;
}
.bd-search-icon {
color: var(--text-muted);
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
position: relative;
cursor: text;
}
.bd-search-icon.clickable {
cursor: pointer;
color: var(--interactive-normal);
}
.bd-search-icon.clickable:hover {
color: var(--interactive-hover);
}
.bd-search-icon.clickable:active {
color: var(--interactive-active);
}
/* Sizes */
.bd-searchbar.size-small input {
height: 20px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
padding: 0 4px;
}
.bd-searchbar.size-small .bd-search-icon {
width: 22px;
height: 22px;
}
.bd-searchbar.size-small svg {
width: 16px;
height: 16px;
}
.bd-searchbar.size-medium input {
height: 30px;
line-height: 32px;
font-size: 16px;
padding: 0 8px;
}
.bd-searchbar.size-medium .bd-search-icon {
width: 32px;
height: 32px;
}
.bd-searchbar.size-medium svg {
width: 20px;
height: 20px;
}
.bd-searchbar.size-large input {
height: 38px;
line-height: 40px;
font-size: 20px;
padding: 0 16px;
}
.bd-searchbar.size-large .bd-search-icon {
width: 40px;
height: 40px;
}
.bd-searchbar.size-large svg {
width: 24px;
height: 24px;
}

View File

@ -1,34 +1,15 @@
.bd-spinner {
width: 32px;
height: 32px;
position: relative;
@keyframes bd-spinner-rotate {
to {
transform: rotate(1turn);
}
}
.bd-cube1,
.bd-cube2 {
background-color: #3E82E5;
width: 10px;
height: 10px;
position: absolute;
top: 0;
left: 0;
animation: bd-sk-cubemove 1.8s infinite ease-in-out;
}
.bd-cube2 {
animation-delay: -0.9s;
}
@keyframes bd-sk-cubemove {
@keyframes bd-spinner-wandering-cubes {
25% {
transform: translateX(22px) rotate(-90deg) scale(0.5);
transform: translateX(22px) rotate(-90deg) scale(.5);
}
50% {
transform: translateX(22px) translateY(22px) rotate(-179deg);
}
50.1% {
transform: translateX(22px) translateY(22px) rotate(-180deg);
}
@ -36,7 +17,191 @@
transform: translateX(0) translateY(22px) rotate(-270deg) scale(0.5);
}
100% {
transform: rotate(-360deg);
to {
transform: rotate(-1turn);
}
}
@keyframes bd-spinner-chasing-dots-bounce {
0%,
to {
transform: scale(0);
}
50% {
transform: scale(1);
}
}
@keyframes bd-spinner-pulsing-ellipsis {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(0.8);
opacity: 0.3;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes bd-spinner-spinning-circle-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 130, 200
}
to {
stroke-dasharray: 130, 200;
stroke-dashoffset: -124;
}
}
@keyframes bd-spinner-low-motion {
0% {
opacity: 1;
}
50% {
opacity: 0.6
}
to {
opacity: 1;
}
}
.bd-spinner {
display: flex;
justify-content: center;
align-items: center;
}
.bd-spinner-inner {
position: relative;
display: inline-block;
width: 32px;
height: 32px;
contain: paint;
}
.bd-spinner-wandering-cubes .bd-spinner-item {
content: "";
background-color: var(--brand-experiment-400);
width: 10px;
height: 10px;
position: absolute;
top: 0;
left: 0;
animation: bd-spinner-wandering-cubes 1.8s ease-in-out infinite;
}
.bd-spinner-wandering-cubes .bd-spinner-item:last-of-type {
animation-delay: -0.9s;
}
.bd-spinner-chasing-dots .bd-spinner-inner {
animation: bd-spinner-rotate 2s linear infinite;
}
.bd-spinner-chasing-dots .bd-spinner-item {
width: 60%;
height: 60%;
display: inline-block;
position: absolute;
top: 0;
background-color: var(--brand-experiment, hsl(235, calc(var(--saturation-factor, 1) * 85.6%), 64.7%));
border-radius: 100%;
animation: bd-spinner-chasing-dots-bounce 2s ease-in-out infinite;
}
.bd-spinner-chasing-dots .bd-spinner-item:last-of-type {
top: auto;
bottom: 0;
animation-delay: -1s;
}
.bd-spinner-pulsing-ellipsis .bd-spinner-inner {
height: auto;
width: 28px;
display: flex;
justify-content: center;
position: relative;
}
.bd-spinner-pulsing-ellipsis .bd-spinner-item,
.bd-spinner-low-motion .bd-spinner-item {
width: 6px;
height: 6px;
margin-right: 2px;
border-radius: 3px;
display: inline-block;
opacity: 0.3;
}
.bd-spinner-pulsing-ellipsis .bd-spinner-item {
animation: bd-spinner-pulsing-ellipsis 1.4s ease-in-out infinite;
background-color: hsl(180, calc(var(--saturation-factor, 1) * 7.7%), 97.5%);
}
.bd-spinner-pulsing-ellipsis .bd-spinner-item:nth-of-type(2) {
animation-delay: 0.2s;
}
.bd-spinner-pulsing-ellipsis .bd-spinner-item:nth-of-type(3) {
animation-delay: 0.4s;
}
.bd-spinner-spinning-circle {
transform: rotate(280deg);
}
.bd-spinner-circular {
animation: bd-spinner-rotate 2s linear infinite;
height: 100%;
width: 100%;
}
.bd-spinner-circular circle {
stroke: var(--brand-experiment, hsl(235, calc(var(--saturation-factor, 1) * 85.6%), 64.7%));
animation: bd-spinner-spinning-circle-dash 2s ease-in-out infinite;
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
fill: none;
stroke-width: 6;
stroke-miterlimit: 10;
stroke-linecap: round;
}
.bd-spinner-circular circle:first-of-type {
animation-delay: 0.23s;
stroke: var(--brand-experiment-800, hsl(235, calc(var(--saturation-factor, 1) * 47.5%), 19.4%));
}
.bd-spinner-circular circle:nth-of-type(2) {
opacity: 0.6;
animation-delay: 0.15s;
}
.bd-spinner-low-motion .bd-spinner-item {
background-color: var(--interactive-normal);
animation: bd-spinner-low-motion 1.4s ease-in-out infinite;
}
.bd-spinner-low-motion .bd-spinner-item:nth-of-type(2) {
animation-delay: 0.2s;
}
.bd-spinner-low-motion .bd-spinner-item:nth-of-type(3) {
animation-delay: 0.4s;
}

View File

@ -1,11 +1,37 @@
.bd-addon-list-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.bd-addon-list-title .bd-settings-title:not(.bd-settings-group-title) {
margin-bottom: 0;
}
.bd-addon-list-filters,
.bd-addon-list-controls {
display: flex;
align-items: center;
}
.bd-addon-list-controls {
margin-top: 16px;
}
.bd-addon-list-controls .bd-searchbar {
flex: 1 1 auto;
margin-right: 8px;
}
.bd-reload {
margin-left: 10px;
cursor: pointer;
vertical-align: top;
fill: #DCDDDE;
fill: #dcddde;
}
.bd-reload:hover {
fill: #FFFFFF;
fill: #fff;
}
.bd-reload-header {
@ -16,11 +42,6 @@
margin-right: 5px;
}
.bd-controls,
.bd-controls-advanced {
display: flex;
}
.bd-addon-list {
user-select: text;
}
@ -60,7 +81,7 @@
.bd-addon-header .bd-icon {
margin-right: 8px;
fill: var(--header-primary);
color: var(--header-primary);
}
.disabled .bd-addon-header .bd-icon {
@ -112,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 {
@ -177,6 +199,11 @@
overflow: hidden;
}
.bd-controls,
.bd-controls-advanced {
display: flex;
}
.bd-controls > .bd-addon-button {
border-radius: 0;
padding: 4px 6px;
@ -187,7 +214,7 @@
}
.bd-controls > .bd-addon-button svg {
fill: #FFFFFF;
fill: #fff;
}
.bd-controls > .bd-addon-button:first-of-type {
@ -202,26 +229,6 @@
border-radius: 3px;
}
.bd-controls + .bd-addon-list {
margin-top: 10px;
}
.bd-addon-controls {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.bd-addon-controls .bd-search {
font-size: 13px;
margin: 0;
width: 200px;
}
.bd-addon-dropdowns {
display: flex;
}
/* Error Boundary stuff */
.react-error {
color: red;
@ -244,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;
@ -271,7 +266,7 @@
}
.bd-addon-views .bd-view-button.selected svg {
fill: #FFFFFF;
fill: #fff;
}
.bd-addon-views .bd-view-button:hover {
@ -283,7 +278,7 @@
}
.bd-addon-views .bd-view-button.selected {
background-color: #3E82E5;
background-color: #3e82e5;
}
.bd-addon-views .bd-view-button + .bd-view-button {

View File

@ -29,9 +29,9 @@
margin-left: 10px;
}
.bd-select-wrapper label {
/* .bd-select-wrapper label {
margin-right: 5px;
}
} */
.bd-select {
position: relative;
@ -58,6 +58,7 @@
background: none;
border: none;
padding: 0;
margin-bottom: -3px;
}
.bd-select-value {
@ -65,7 +66,7 @@
}
.bd-select-arrow {
margin-left: 10px;
margin-left: 5px;
fill: var(--interactive-normal);
}
@ -81,7 +82,7 @@
border-top: 0;
margin-top: -1px;
margin-left: -1px;
z-index: 2;
z-index: 3;
top: 100%;
}
@ -165,7 +166,7 @@
}
.bd-settings-title {
color: #FFFFFF;
color: var(--header-primary);
font-weight: 600;
cursor: default;
flex: 1;

View File

@ -74,11 +74,6 @@
color: var(--interactive-normal);
}
.floating-window-buttons .button svg,
.floating-window-buttons .button .fill {
fill: currentColor;
}
.floating-window-buttons .maximize-button:hover {
background: var(--background-modifier-hover);
}

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

@ -0,0 +1,568 @@
/* Cards */
@keyframes bd-store-card-splash {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.bd-store-addons {
position: relative;
z-index: 1;
margin-top: 16px;
display: grid;
grid-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
}
.bd-store-card {
display: flex;
flex-direction: column;
height: 375px;
width: 100%;
overflow: hidden;
line-height: normal;
border-radius: 8px;
position: relative;
transition: box-shadow 0.2s ease-out, transform 0.2s ease-out, background 0.2s ease-out, opacity 0.2s ease-in, -webkit-box-shadow 0.2s ease-out, -webkit-transform 0.2s ease-out;
cursor: pointer;
background-color: var(--activity-card-background);
}
.bd-store-card.bd-store-card-embedded {
max-width: 325px;
}
.theme-dark .bd-store-card {
background-color: var(--background-secondary-alt);
}
.theme-dark .bd-store-card:hover,
.theme-dark .bd-store-card:focus {
background-color: var(--background-tertiary);
}
.theme-light .bd-store-card,
.theme-light .bd-store-card {
box-shadow: 0 0 0 1px rgba(185, 187, 190, .3), var(--elevation-medium);
}
.theme-light .bd-store-card:hover,
.theme-light .bd-store-card:focus {
box-shadow: var(--elevation-high);
}
.bd-store-card:hover,
.bd-store-card:focus {
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
.bd-store-card:hover .card-splash,
.bd-store-card:focus .card-splash {
transform: scale(1.01) translateZ(0);
}
.bd-store-card .bd-store-card-header {
height: 180px;
position: relative;
display: block;
overflow: visible;
margin-bottom: 32px;
}
.bd-store-card-icon {
position: absolute;
bottom: -21px;
left: 12px;
width: 44px;
height: 44px;
background-color: var(--activity-card-background);
border-radius: 12px;
overflow: hidden;
transition: background-color .2s ease-out;
padding: 4px;
}
.bd-store-card-icon img,
.bd-installation-icon img {
-webkit-user-drag: none;
width: 100%;
height: 100%;
background-color: var(--background-accent);
border-radius: 8px;
}
.theme-dark .bd-store-card-icon {
background-color: var(--background-secondary-alt);
}
.theme-dark .bd-store-card:hover .bd-store-card-icon,
.theme-dark .bd-store-card:focus .bd-store-card-icon {
background-color: var(--background-tertiary);
}
.bd-store-card .bd-store-card-splash,
.bd-store-card-splash img {
-webkit-user-drag: none;
width: 100%;
height: 100%;
}
.bd-store-card-splash {
display: block;
position: absolute;
top: 0;
left: 0;
animation: bd-store-card-splash 0.2s linear;
transition: transform 0.2s ease-out;
transform: scale(1);
}
.bd-store-card-splash img {
object-fit: cover;
}
.bd-store-card-body {
display: flex;
flex: 1 1 auto;
position: relative;
flex-direction: column;
-ms-flex-line-pack: stretch;
align-content: stretch;
padding: 0 16px 16px;
overflow: hidden;
}
.bd-store-card-title {
display: flex;
align-items: center;
max-width: 100%;
}
.bd-store-card-body h5 {
max-width: 100%;
font-weight: 600;
color: var(--header-primary);
font-size: 16px;
line-height: 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.bd-store-card-body p {
flex: 1 1 auto;
overflow: hidden;
margin: 4px 0 8px;
display: -webkit-box;
color: var(--header-secondary);
font-size: 14px;
line-height: 18px;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
max-height: 52px;
}
.bd-card-tags {
display: flex;
max-width: 100%;
margin-bottom: 12px;
}
.bd-card-tags span {
margin-right: 5px;
border-radius: 2px;
background-color: var(--background-primary);
color: var(--interactive-normal);
padding: 4px 6px;
font-size: 10px;
line-height: 1.15;
text-transform: uppercase;
font-weight: 600;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
}
.theme-light .bd-card-tags span {
background-color: var(--background-secondary-alt);
}
.bd-addon-store .bd-store-card .bd-card-tags span.selected {
color: #fff;
background-color: var(--brand-experiment, hsl(235, calc(var(--saturation-factor, 1) * 85.6%), 64.7%));
}
.bd-card-tags span:last-child {
margin: 0;
}
.bd-store-card-footer {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.bd-store-card-stats,
.bd-store-card-stat {
display: flex;
align-items: center;
}
.bd-store-card-stat {
margin-right: 8px;
font-size: 12px;
line-height: 16px;
color: var(--header-secondary);
}
.bd-store-card-stat svg {
width: 18px;
height: 18px;
margin-right: 4px;
}
.bd-store-card-stat span {
max-width: 50px;
overflow: hidden;
text-overflow: ellipsis;
}
.bd-store-card-new-badge {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
text-transform: uppercase;
border-radius: 8px;
height: 16px;
padding: 0 4px;
margin-left: 6px;
background-color: var(--status-danger);
color: #fff;
text-align: center;
font-size: 12px;
line-height: 16px;
font-weight: 600;
}
/* Store Cards -- List View */
.bd-store-addons.list-view {
display: grid;
grid-gap: 0;
grid-template-columns: auto;
grid-auto-rows: min-content;
align-items: stretch;
}
.bd-store-addons.list-view .bd-store-card {
height: auto;
flex-direction: row;
border-radius: 0;
background-color: transparent;
box-shadow: none;
transform: none;
transition: none;
border-bottom: 1px solid var(--background-modifier-accent);
padding-bottom: 24px;
margin-bottom: 24px;
}
.bd-store-addons.list-view .bd-store-card-body {
padding: 0;
margin-left: 16px;
}
.bd-store-addons.list-view .bd-store-card-header {
flex: 0 0 auto;
margin: 0;
width: 240px;
height: 135px;
border-radius: 8px;
overflow: hidden;
}
.bd-store-addons.list-view .bd-store-card-icon {
display: none;
}
.bd-store-addons.list-view .bd-card-tags span {
background-color: var(--background-modifier-accent);
}
.bd-store-addons.list-view .bd-store-card:hover .bd-store-card-splash {
transform: scale(1.05);
}
.bd-store-addons.list-view .bd-store-card-body p {
-webkit-line-clamp: 2;
margin: 4px 0 12px;
}
.bd-store-addons.list-view .bd-store-card-body h5 {
font-family: var(--text-display);
}
/* Pagination */
.bd-page-control,
.bd-page-buttons {
display: flex;
align-items: center;
}
.bd-page-control {
justify-content: center;
max-width: 475px;
bottom: 0;
padding: 4px 0;
margin: 0 auto;
margin-top: 16px;
}
.bd-page-buttons {
max-width: 100%;
flex-grow: 0;
width: auto;
overflow: auto;
}
.bd-page-button {
cursor: pointer;
font-weight: 600;
box-sizing: border-box;
justify-content: center;
height: 38px;
min-width: 28px;
padding: 0 8px;
margin: 4px;
border-radius: 3px;
color: var(--header-primary);
background-color: transparent;
flex: 0 0 auto;
display: flex;
align-items: center;
width: min-content;
}
.bd-page-button svg {
display: inline-block;
width: 1em;
height: 1em;
}
.bd-page-button:hover {
color: var(--interactive-hover);
background-color: var(--background-secondary-alt);
}
.bd-page-button:focus-visible {
box-shadow: 0 0 0 4px var(--focus-primary);
}
.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 {
color: #fff;
background-color: var(--brand-experiment, hsl(235, calc(var(--saturation-factor, 1) * 85.6%), 64.7%));
}
/* Tags Bar */
.bd-store-tags {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
margin-top: 16px;
color: var(--text-muted);
font-weight: 600;
font-size: 14px;
}
.bd-store-tags-inner {
margin-bottom: -5px;
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
.bd-store-tags span {
padding: 6px 8px;
text-align: center;
cursor: pointer;
border-radius: 5px;
margin-right: 5px;
margin-bottom: 5px;
}
.bd-store-tags span:hover {
background-color: var(--background-modifier-hover);
}
.bd-store-tags span:active {
background-color: var(--background-modifier-active);
}
.theme-dark .bd-store-tags span.selected,
.theme-light .bd-store-tags span.selected {
background-color: var(--brand-experiment, hsl(235, calc(var(--saturation-factor, 1) * 85.6%), 64.7%));
color: #fff;
}
.bd-store-spinner {
margin-top: 96px;
}
/* Install Modal */
.bd-installation-header {
overflow: visible;
padding: 0;
position: relative;
height: 100px;
}
.bd-installation-close {
background: rgba(0,0,0, 0.6);
border-radius: 4px;
color: #fff;
height: 18px;
line-height: 0;
opacity: 1;
position: absolute;
right: 16px;
transition: background 0.1s;
top: 16px;
width: 18px;
}
.bd-installation-thumbnail {
border-radius: 5px 5px 0 0;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
object-position: 50%;
object-fit: cover;
}
.bd-installation-icon {
position: absolute;
left: 12px;
bottom: -26px;
width: 44px;
height: 44px;
background-color: var(--background-primary);
border-radius: 12px;
overflow: hidden;
transition: background-color 0.2s ease-out;
padding: 4px;
}
.bd-installation-content {
padding: 16px;
padding-top: 32px;
}
.bd-installation-name {
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: var(--header-primary);
}
.bd-installation-subtitle {
font-size: 14px;
line-height: 18px;
color: var(--header-secondary);
}
.bd-installation-info {
padding: 0 20px;
background-color: var(--background-secondary);
border-radius: 8px;
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;
display: flex;
align-items: center;
font-size: 14px;
line-height: 18px;
font-weight: 500;
color: var(--header-primary);
word-break: break-word;
}
.bd-installation-info li a:hover {
text-decoration: underline;
}
.bd-installation-info li .bd-info-icon {
display: inline-flex;
flex: 0 0 auto;
margin-right: 16px;
--bd-info-item-color: var(--channels-default);
}
.bd-installation-info svg {
width: 20px;
height: auto;
color: var(--bd-info-icon-color);
}
.bd-installation-info .bd-info-divider {
border-bottom: thin solid var(--background-modifier-accent);
}
#bd-info-description {
--bd-info-icon-color: var(--brand-experiment-360, hsl(235, calc(var(--saturation-factor, 1) * 86.1%), 77.5%));
}
#bd-info-version {
--bd-info-icon-color: #ff9a15;
}
#bd-info-source {
--bd-info-icon-color: #c373dd;
}
#bd-info-author {
--bd-info-icon-color: #fed648;
}
#bd-info-upload-date {
--bd-info-icon-color: #9bdd73;
}

View File

@ -1,9 +1,13 @@
.bd-tab-bar {
display: flex;
flex-direction: row;
list-style-type: none;
margin: 0;
padding: 0;
}
.bd-tab-item {
border: none;
justify-content: center;
align-items: center;
text-align: center;
@ -18,6 +22,7 @@
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
background-color: transparent;
color: var(--interactive-normal);
flex-shrink: 0;
font-weight: 500;
@ -25,7 +30,7 @@
line-height: 20px;
}
.bd-tab-item:last-child {
.bd-tab-item:last-of-type {
margin: 0;
}
@ -46,3 +51,13 @@
.bd-tab-item:active {
background-color: var(--background-modifier-active);
}
.bd-tab-item.disabled {
cursor: default;
background-color: transparent;
color: var(--interactive-muted);
}
.bd-tab-item:focus-visible {
box-shadow: 0 0 0 4px var(--focus-primary);
}

View File

@ -1,12 +1,11 @@
import {React, Strings, WebpackModules, DiscordClasses} from "modules";
import {React, Strings, WebpackModules, DiscordClasses, Utilities} from "modules";
import Extension from "./icons/extension";
import ThemeIcon from "./icons/theme";
import Divider from "./divider";
import TabBar from "./tabbar";
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
const joinClassNames = (...classNames) => classNames.filter(e => e).join(" ");
class AddonError extends React.Component {
constructor(props) {
super(props);
@ -29,8 +28,9 @@ class AddonError extends React.Component {
</div>;
}
render() {
const err = this.props.err;
return <div key={`${err.type}-${this.props.index}`} className={joinClassNames("bd-addon-error", (this.state.expanded) ? "expanded" : "collapsed")}>
const {err} = this.props;
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 />}
@ -105,9 +105,7 @@ export default class AddonErrorModal extends React.Component {
return <>
<div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
<h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
<div className="bd-tab-bar">
{tabs.map(tab => <div onClick={() => {this.switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
<TabBar items={tabs.map(({id, name}) => ({value: id, name}))} value={selectedTab.id} onChange={value => this.switchToTab(value)} />
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">

View File

@ -1,4 +1,4 @@
import {React, DiscordModules} from "modules";
import {React, Strings} from "modules";
import MagnifyingGlass from "../icons/magnifyingglass";
export default class NoResults extends React.Component {
@ -6,7 +6,7 @@ export default class NoResults extends React.Component {
return <div className={"bd-empty-results" + (this.props.className ? ` ${this.props.className}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
{this.props.text || Strings.Addons.noResults || ""}
</div>
</div>;
}

View File

@ -1,7 +1,7 @@
import {React, Strings} from "modules";
import Screen from "../../structs/screen";
import CloseButton from "../icons/close";
import CloseIcon from "../icons/close";
import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals";
@ -108,10 +108,10 @@ export default class FloatingWindow extends React.Component {
<span className="title">{this.props.title}</span>
<div className="floating-window-buttons">
<div className="button maximize-button" onClick={this.maximize}>
<MaximizeIcon size="18px" />
<MaximizeIcon size={16} />
</div>
<div className="button close-button" onClick={this.close}>
<CloseButton />
<CloseIcon size={14} />
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
import {React} from "modules";
export default class Author extends React.Component {
render() {
return <svg viewBox="0 0 24 24" width="24" height="24" {...this.props}>
<path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M14 8.00598C14 10.211 12.206 12.006 10 12.006C7.795 12.006 6 10.211 6 8.00598C6 5.80098 7.794 4.00598 10 4.00598C12.206 4.00598 14 5.80098 14 8.00598ZM2 19.006C2 15.473 5.29 13.006 10 13.006C14.711 13.006 18 15.473 18 19.006V20.006H2V19.006Z"></path>
<path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M14 8.00598C14 10.211 12.206 12.006 10 12.006C7.795 12.006 6 10.211 6 8.00598C6 5.80098 7.794 4.00598 10 4.00598C12.206 4.00598 14 5.80098 14 8.00598ZM2 19.006C2 15.473 5.29 13.006 10 13.006C14.711 13.006 18 15.473 18 19.006V20.006H2V19.006Z"></path>
<path fill="currentColor" d="M20.0001 20.006H22.0001V19.006C22.0001 16.4433 20.2697 14.4415 17.5213 13.5352C19.0621 14.9127 20.0001 16.8059 20.0001 19.006V20.006Z"></path>
<path fill="currentColor" d="M14.8834 11.9077C16.6657 11.5044 18.0001 9.9077 18.0001 8.00598C18.0001 5.96916 16.4693 4.28218 14.4971 4.0367C15.4322 5.09511 16.0001 6.48524 16.0001 8.00598C16.0001 9.44888 15.4889 10.7742 14.6378 11.8102C14.7203 11.8418 14.8022 11.8743 14.8834 11.9077Z"></path>
</svg>;
}
}

View File

@ -0,0 +1,10 @@
import {React} from "modules";
export default class Clock extends React.Component {
render() {
const size = this.props.size || 16;
return <svg {...this.props} aria-hidden="false" width={size} height={size} viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.4764 2 2 6.4764 2 12C2 17.5236 6.4764 22 12 22C17.5236 22 22 17.5236 22 12C22 6.4764 17.5236 2 12 2ZM12 5.6C12.4422 5.6 12.8 5.95781 12.8 6.4V11.5376L16.5625 13.7126C16.9453 13.9329 17.0703 14.4173 16.85 14.8001C16.6297 15.183 16.1453 15.3079 15.7625 15.0876L11.6873 12.7376C11.656 12.7251 11.6279 12.7048 11.5998 12.6876C11.3607 12.5486 11.1998 12.2954 11.1998 12.0001V6.4001C11.1998 5.9579 11.5578 5.6 12 5.6Z" />
</svg>;
}
}

View File

@ -2,12 +2,8 @@ import {React} from "modules";
export default class CloseButton extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 12 12" style={{width: size, height: size}}>
<g className="background" fill="none" fillRule="evenodd">
<path d="M0 0h12v12H0" />
<path className="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6" />
</g>
return <svg {...this.props} width={this.props.size || "24"} height={this.props.size || "24"} viewBox="0 0 24 24">
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z"></path>
</svg>;
}
}

View File

@ -0,0 +1,9 @@
import {React} from "modules";
export default class Description extends React.Component {
render() {
return <svg viewBox="0 0 12 12" width="24" height="24" {...this.props}>
<path d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z" fill="currentColor"></path>
</svg>;
}
}

View File

@ -2,9 +2,8 @@ import {React} from "modules";
export default class DownArrow extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="currentColor" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"/>
</svg>;
return <svg viewBox="0 0 24 24" width="16" height="16" {...this.props}>
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z" fill="currentColor"/>
</svg>;
}
}

View File

@ -0,0 +1,11 @@
import {React} from "modules";
export default class Download extends React.Component {
render() {
return <svg viewBox="0 0 24 24" width="24" height="24" {...this.props}>
<defs></defs>
<path d="M0 0h24v24H0z" fill="none"></path>
<path fill="currentColor" d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"></path>
</svg>;
}
}

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,10 +2,8 @@ import {React} from "modules";
export default class FullScreen extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={this.props.onClick}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
return <svg className={this.props.className || ""} fill="currentColor" width={this.props.size || "24px"} height={this.props.size || "24px"} viewBox="0 0 24 24">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>;
}
}

View File

@ -2,9 +2,9 @@ import {React} from "modules";
export default class GitHub extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="m12 .5c-6.63 0-12 5.28-12 11.792 0 5.211 3.438 9.63 8.205 11.188.6.111.82-.254.82-.567 0-.28-.01-1.022-.015-2.005-3.338.711-4.042-1.582-4.042-1.582-.546-1.361-1.335-1.725-1.335-1.725-1.087-.731.084-.716.084-.716 1.205.082 1.838 1.215 1.838 1.215 1.07 1.803 2.809 1.282 3.495.981.108-.763.417-1.282.76-1.577-2.665-.295-5.466-1.309-5.466-5.827 0-1.287.465-2.339 1.235-3.164-.135-.298-.54-1.497.105-3.121 0 0 1.005-.316 3.3 1.209.96-.262 1.98-.392 3-.398 1.02.006 2.04.136 3 .398 2.28-1.525 3.285-1.209 3.285-1.209.645 1.624.24 2.823.12 3.121.765.825 1.23 1.877 1.23 3.164 0 4.53-2.805 5.527-5.475 5.817.42.354.81 1.077.81 2.182 0 1.578-.015 2.846-.015 3.229 0 .309.21.678.825.56 4.801-1.548 8.236-5.97 8.236-11.173 0-6.512-5.373-11.792-12-11.792z" />
const size = this.props.size || "18";
return <svg viewBox="0 0 24 24" width={size} height={size} onClick={this.props.onClick}>
<path fill="currentColor" d="m12 .5c-6.63 0-12 5.28-12 11.792 0 5.211 3.438 9.63 8.205 11.188.6.111.82-.254.82-.567 0-.28-.01-1.022-.015-2.005-3.338.711-4.042-1.582-4.042-1.582-.546-1.361-1.335-1.725-1.335-1.725-1.087-.731.084-.716.084-.716 1.205.082 1.838 1.215 1.838 1.215 1.07 1.803 2.809 1.282 3.495.981.108-.763.417-1.282.76-1.577-2.665-.295-5.466-1.309-5.466-5.827 0-1.287.465-2.339 1.235-3.164-.135-.298-.54-1.497.105-3.121 0 0 1.005-.316 3.3 1.209.96-.262 1.98-.392 3-.398 1.02.006 2.04.136 3 .398 2.28-1.525 3.285-1.209 3.285-1.209.645 1.624.24 2.823.12 3.121.765.825 1.23 1.877 1.23 3.164 0 4.53-2.805 5.527-5.475 5.817.42.354.81 1.077.81 2.182 0 1.578-.015 2.846-.015 3.229 0 .309.21.678.825.56 4.801-1.548 8.236-5.97 8.236-11.173 0-6.512-5.373-11.792-12-11.792z" />
</svg>;
}
}

View File

@ -0,0 +1,9 @@
import {React} from "modules";
export default class Heart extends React.Component {
render() {
return <svg viewBox="0 0 24 24" width="24" height="24" {...this.props}>
<path d="M16 4.001C14.406 4.001 12.93 4.838 12 6.081C11.07 4.838 9.594 4.001 8 4.001C5.243 4.001 3 6.244 3 9.001C3 14.492 11.124 19.633 11.471 19.849C11.633 19.95 11.817 20.001 12 20.001C12.183 20.001 12.367 19.95 12.529 19.849C12.876 19.633 21 14.492 21 9.001C21 6.244 18.757 4.001 16 4.001V4.001Z" fill="currentColor"></path>
</svg>;
}
}

View File

@ -2,10 +2,11 @@ import {React} from "modules";
export default class ArrowRight extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M10 17l5-5-5-5v10z" />
<path d="M0 24V0h24v24H0z" fill="none" />
return <svg viewBox="0 0 24 24" width="24" height="24" {...this.props}>
<g fill="none" fillRule="evenodd">
<polygon fill="currentColor" fillRule="nonzero" points="8.47 2 6.12 4.35 13.753 12 6.12 19.65 8.47 22 18.47 12"></polygon>
<polygon points="0 0 24 0 24 24 0 24"></polygon>
</g>
</svg>;
}
}

View File

@ -2,10 +2,11 @@ import {React} from "modules";
export default class ArrowLeft extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M14 7l-5 5 5 5V7z" />
<path d="M24 0v24H0V0h24z" fill="none" />
return <svg viewBox="0 0 24 24" width="24" height="24" {...this.props}>
<g fill="none" fillRule="evenodd">
<polygon fill="currentColor" fillRule="nonzero" points="18.35 4.35 16 2 6 12 16 22 18.35 19.65 10.717 12"></polygon>
<polygon points="0 0 24 0 24 24 0 24"></polygon>
</g>
</svg>;
}
}

View File

@ -2,10 +2,8 @@ import {React} from "modules";
export default class Search extends React.Component {
render() {
const size = this.props.size || "16px";
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>;
return <svg {...this.props} width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M21.707 20.293L16.314 14.9C17.403 13.504 18 11.799 18 10C18 7.863 17.167 5.854 15.656 4.344C14.146 2.832 12.137 2 10 2C7.863 2 5.854 2.832 4.344 4.344C2.833 5.854 2 7.863 2 10C2 12.137 2.833 14.146 4.344 15.656C5.854 17.168 7.863 18 10 18C11.799 18 13.504 17.404 14.9 16.314L20.293 21.706L21.707 20.293ZM10 16C8.397 16 6.891 15.376 5.758 14.243C4.624 13.11 4 11.603 4 10C4 8.398 4.624 6.891 5.758 5.758C6.891 4.624 8.397 4 10 4C11.603 4 13.109 4.624 14.242 5.758C15.376 6.891 16 8.398 16 10C16 11.603 15.376 13.11 14.242 14.243C13.109 15.376 11.603 16 10 16Z"></path>
</svg>;
}
}

View File

@ -0,0 +1,9 @@
import {React} from "modules";
export default class Version extends React.Component {
render() {
return <svg viewBox="0 0 24 24" width="24" height="24" {...this.props}>
<path d="M21.707 13.293l-11-11C10.519 2.105 10.266 2 10 2H3c-.553 0-1 .447-1 1v7c0 .266.105.519.293.707l11 11c.195.195.451.293.707.293s.512-.098.707-.293l7-7c.391-.391.391-1.023 0-1.414zM7 9c-1.106 0-2-.896-2-2 0-1.106.894-2 2-2 1.104 0 2 .894 2 2 0 1.104-.896 2-2 2z" fill="currentColor"></path>
</svg>;
}
}

View File

@ -0,0 +1,117 @@
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";
import Author from "./icons/author";
import Description from "./icons/description";
import Clock from "./icons/clock";
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
};
}
async install(id, filename) {
this.setState({isInstalling: true});
await this.props.onInstall?.(id, filename, this.props.type);
this.setState({isInstalling: false});
this.props.onClose();
}
onKeyDown(event) {
const {key} = event;
if (key === "Escape" || key === "Enter" || key === " ") event.stopPropagation();
if (key === "Escape") this.props.onClose();
if (key === "Enter" || key === " ") {
this.install(this.props.id, this.props.filename);
}
}
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">
<Header className="bd-installation-header">
<img className="bd-installation-thumbnail" src={this.props.thumbnail} alt={`${name} thumbnail`}/>
<div ref={this.authorRef} className="bd-installation-icon">
<img alt={author.display_name} src={`https://github.com/${author.github_name}.png?size=44`} />
</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})}
</div>
<ul className="bd-installation-info">
<InfoItem icon={<Description aria-label={Strings.Addons.description} />} id="bd-info-description" label={Strings.Addons.description}>
{description}
</InfoItem>
<div className="bd-info-divider" role="separator"></div>
<InfoItem icon={<Version aria-label={Strings.Addons.version} />} id="bd-info-version" label={Strings.Addons.version}>
{version}
</InfoItem>
<div className="bd-info-divider" role="separator"></div>
<InfoItem icon={<Clock aria-label={Strings.Store.uploadDate.format({date: releaseDate.toLocaleString()})} />} id="bd-info-upload-date" label={Strings.Store.uploadDate.format({date: releaseDate.toLocaleString()})}>
{Strings.Store.uploadDate.format({date: releaseDate.toLocaleString()})}
</InfoItem>
<div className="bd-info-divider" role="separator"></div>
<InfoItem icon={<Github aria-label={Strings.Addons.source} />} id="bd-info-source" label={Strings.Addons.source}>
<a href={Web.ENDPOINTS.githubRedirect(id)} target="_blank" rel="noreferrer noopener">{filename}</a>
</InfoItem>
<div className="bd-info-divider" role="separator"></div>
<InfoItem icon={<Author aria-label={Strings.Addons.author} />} id="bd-info-author" label={Strings.Addons.uploaded}>
<a href={`${Web.PAGES.developer}/${author.display_name}`} target="_blank" rel="noreferrer noopener">{author.display_name}</a>
</InfoItem>
</ul>
</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>
</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}>
<div ref={this.iconRef} className="bd-info-icon">
{this.props.icon ? this.props.icon : <Support />}
</div>
<span>
{this.props.children}
</span>
</li>;
}
}

View File

@ -4,7 +4,7 @@ import {WebpackModules, React, ReactDOM, Settings, Strings, DOMManager, DiscordM
import FormattableString from "../structs/string";
import AddonErrorModal from "./addonerrormodal";
import ErrorBoundary from "./errorboundary";
import InstallationModal from "./installationmodal";
export default class Modals {
@ -93,7 +93,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");
@ -328,4 +328,11 @@ export default class Modals {
return React.createElement(ErrorBoundary, null, React.createElement(modal, props));
});
}
static showInstallationModal(options = {}) {
this.ModalActions.openModal(props => React.createElement(ErrorBoundary, null, React.createElement(InstallationModal, {
...props,
...options
})));
}
}

View File

@ -4,7 +4,7 @@ import SettingsTitle from "../settings/title";
import ServerCard from "./card";
import EmptyResults from "../blankslates/noresults";
import Connection from "../../structs/psconnection";
import Search from "../settings/components/search";
import SearchBar from "../settings/components/searchbar";
import Previous from "../icons/previous";
import Next from "../icons/next";
@ -155,7 +155,7 @@ export default class PublicServers extends React.Component {
}
get searchBox() {
return <Search onKeyDown={this.searchKeyDown} className="bd-server-search" placeholder={`${Strings.PublicServers.search}...`} value={this.state.query} />;
return <SearchBar onKeyDown={this.searchKeyDown} className="bd-server-search" placeholder={`${Strings.PublicServers.search}...`} value={this.state.query} />;
}
get title() {

View File

@ -54,11 +54,7 @@ export default new class SettingsRenderer {
}
getAddonPanel(title, addonList, addonState, options = {}) {
return React.createElement(AddonList, Object.assign({}, {
title: title,
addonList: addonList,
addonState: addonState
}, options));
return React.createElement(AddonList, Object.assign({}, {title, addonList, addonState}, options));
}
async patchSections() {

View File

@ -1,216 +0,0 @@
import Logger from "common/logger";
import {React, Strings, Events, WebpackModules, DataStore} from "modules";
import Modals from "../modals";
import SettingsTitle from "./title";
import AddonCard from "./addoncard";
import Dropdown from "./components/dropdown";
import Search from "./components/search";
import ErrorBoundary from "../errorboundary";
import ListIcon from "../icons/list";
import GridIcon from "../icons/grid";
import NoResults from "../blankslates/noresults";
import EmptyImage from "../blankslates/emptyimage";
const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
export default class AddonList extends React.Component {
constructor(props) {
super(props);
this.state = {query: "", sort: this.getControlState("sort", "name"), ascending: this.getControlState("ascending", true), view: this.getControlState("view", "list")};
this.sort = this.sort.bind(this);
this.reverse = this.reverse.bind(this);
this.search = this.search.bind(this);
this.update = this.update.bind(this);
this.listView = this.listView.bind(this);
this.gridView = this.gridView.bind(this);
this.openFolder = this.openFolder.bind(this);
}
componentDidMount() {
Events.on(`${this.props.prefix}-loaded`, this.update);
Events.on(`${this.props.prefix}-unloaded`, this.update);
}
componentWillUnmount() {
Events.off(`${this.props.prefix}-loaded`, this.update);
Events.off(`${this.props.prefix}-unloaded`, this.update);
}
onControlChange(control, value) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) addonlistControls[this.props.type] = {};
addonlistControls[this.props.type][control] = value;
DataStore.setBDData("addonlistControls", addonlistControls);
}
getControlState(control, defaultValue) {
const addonlistControls = DataStore.getBDData("addonlistControls") || {};
if (!addonlistControls[this.props.type]) return defaultValue;
if (!addonlistControls[this.props.type].hasOwnProperty(control)) return defaultValue;
return addonlistControls[this.props.type][control];
}
update() {
this.forceUpdate();
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
listView() {this.changeView("list");}
gridView() {this.changeView("grid");}
changeView(view) {
this.onControlChange("view", view);
this.setState({view});
}
reverse(value) {
this.onControlChange("ascending", value);
this.setState({ascending: value});
}
sort(value) {
this.onControlChange("sort", value);
this.setState({sort: value});
}
search(event) {
this.setState({query: event.target.value.toLocaleLowerCase()});
}
openFolder() {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(this.props.folder);
}
get sortOptions() {
return [
{label: Strings.Addons.name, value: "name"},
{label: Strings.Addons.author, value: "author"},
{label: Strings.Addons.version, value: "version"},
{label: Strings.Addons.added, value: "added"},
{label: Strings.Addons.modified, value: "modified"},
{label: Strings.Addons.isEnabled, value: "isEnabled"}
];
}
get directions() {
return [
{label: Strings.Sorting.ascending, value: true},
{label: Strings.Sorting.descending, value: false}
];
}
get emptyImage() {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${this.props.type}s`, type: this.props.type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type: this.props.type})} message={message}>
<button className="bd-button" onClick={this.openFolder}>{Strings.Addons.openFolder.format({type: this.props.type})}</button>
</EmptyImage>;
}
makeControlButton(title, children, action, selected = false) {
return <Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
}}
</Tooltip>;
}
render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props;
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: this.openFolder} : null;
let sortedAddons = addonList.sort((a, b) => {
const sortByEnabled = this.state.sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[this.state.sort];
const second = sortByEnabled ? addonState[b.id] : b[this.state.sort];
const stringSort = (str1, str2) => str1.toLocaleLowerCase().localeCompare(str2.toLocaleLowerCase());
if (typeof(first) == "string") return stringSort(first, second);
if (typeof(first) == "boolean") return (first === second) ? stringSort(a.name, b.name) : first ? -1 : 1;
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!this.state.ascending) sortedAddons.reverse();
if (this.state.query) {
sortedAddons = sortedAddons.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.author.toLocaleLowerCase().includes(this.state.query);
matches = matches || addon.description.toLocaleLowerCase().includes(this.state.query);
if (!matches) return false;
return true;
});
}
const renderedCards = sortedAddons.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
const hasAddonsInstalled = this.props.addonList.length !== 0;
const isSearching = !!this.state.query;
const hasResults = sortedAddons.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} />
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}:</label>
<Dropdown options={this.sortOptions} value={this.state.sort} onChange={this.sort} style="transparent" />
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}:</label>
<Dropdown options={this.directions} value={this.state.ascending} onChange={this.reverse} style="transparent" />
</div>
</div>
<div className="bd-addon-views">
{this.makeControlButton("List View", <ListIcon />, this.listView, this.state.view === "list")}
{this.makeControlButton("Grid View", <GridIcon />, this.gridView, this.state.view === "grid")}
</div>
</div>
</div>,
!hasAddonsInstalled && this.emptyImage,
isSearching && !hasResults && hasAddonsInstalled && <NoResults />,
hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (this.state.view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>
];
}
editAddon(id) {
if (this.props.editAddon) this.props.editAddon(id);
}
async deleteAddon(id) {
const addon = this.props.addonList.find(a => a.id == id);
const shouldDelete = await this.confirmDelete(addon);
if (!shouldDelete) return;
if (this.props.deleteAddon) this.props.deleteAddon(addon);
}
confirmDelete(addon) {
return new Promise(resolve => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.confirmDelete.format({name: addon.name}), {
danger: true,
confirmText: Strings.Addons.deleteAddon,
onConfirm: () => {resolve(true);},
onCancel: () => {resolve(false);}
});
});
}
}
const originalRender = AddonList.prototype.render;
Object.defineProperty(AddonList.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonList", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

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,
@ -132,10 +132,11 @@ export default class AddonCard extends React.Component {
}
get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
return <div className="bd-controls">
{this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})}
{this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)}
{this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})}
{this.props.confirmAddonDelete && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.confirmAddonDelete, {danger: true})}
</div>;
}

View File

@ -0,0 +1,297 @@
import {React, Settings, Strings, Events, Utilities, DataStore, DiscordClasses} from "modules";
import {Web} from "data";
import {shell} from "electron";
import Dropdown from "../components/dropdown";
import SearchBar from "../components/searchbar";
import TabBar from "../../tabbar";
import Divider from "../../divider";
import SettingsTitle from "../title";
import StorePage from "./store";
import InstalledPage from "./installed";
const CONTROLS = {
installed: {
sortOptions: [
{get label() {return Strings.Addons.name;}, value: "name"},
{get label() {return Strings.Addons.author;}, value: "author"},
{get label() {return Strings.Addons.version;}, value: "version"},
{get label() {return Strings.Addons.added;}, value: "added"},
{get label() {return Strings.Addons.modified;}, value: "modified"},
{get label() {return Strings.Addons.isEnabled;}, value: "isEnabled"}
],
directions: [
{get label() {return Strings.Sorting.ascending;}, value: true},
{get label() {return Strings.Sorting.descending;}, value: false}
],
viewOptions: [
{get label() {return Strings.Addons.list;}, value: "list"},
{get label() {return Strings.Addons.grid;}, value: "grid"}
]
},
store: {
sortOptions: [
{get label() {return Strings.Addons.name;}, value: "name"},
{get label() {return Strings.Store.Likes;}, value: "likes"},
{get label() {return Strings.Store.downloads;}, value: "downloads"},
{get label() {return Strings.Addons.added;}, value: "release_date"}
],
directions: [
{get label() {return Strings.Sorting.ascending;}, value: true},
{get label() {return Strings.Sorting.descending;}, value: false}
],
viewOptions: [
{get label() {return Strings.Addons.list;}, value: "list"},
{get label() {return Strings.Addons.grid;}, value: "grid"}
]
}
};
const PAGES = {
installed: {
get label() {return Strings.Addons.installed;},
component: InstalledPage,
defaults: {
sort: "name",
ascending: true,
view: "list"
}
},
store: {
get label() {return Strings.Addons.store;},
component: StorePage,
state: {
selectedTag: "all"
},
controls: ({setState, state, type}) => <div className="bd-store-tags">
<div className="bd-store-tags-inner">
{Web.TAGS[type].map(tag => {
return <span
onClick={() => setState({selectedTag: tag})}
className={Utilities.className({selected: state.selectedTag === tag})}
>{tag}</span>;
})}
</div>
</div>,
defaults: {
sort: "release_date",
ascending: false,
view: "grid"
}
}
};
export default class AddonList extends React.Component {
constructor(props) {
super(props);
this.update = this.update.bind(this);
this.reload = this.reload.bind(this);
this.editAddon = this.editAddon.bind(this);
this.events = [`${this.props.type}-loaded`, `${this.props.type}-unloaded`];
this.state = {
query: "",
selectedTag: "all",
viewStyle: this.viewStyle,
sortStyle: this.sortStyle,
ascending: this.ascending,
page: "installed",
controlsVisible: true
};
this.search = Utilities.debounce(value => {
this.setState({query: value});
}, 200);
}
componentDidMount() {
for (const event of this.events) Events.on(event, this.update);
}
componentWillUnmount() {
for (const event of this.events) Events.off(event, this.update);
}
get currentPage() {return this.state?.page || "installed";}
get defaults() {
const defaults = PAGES[this.currentPage]?.defaults;
return defaults || {
sort: "name",
ascending: true,
view: "list"
};
}
get sortStyle() {
return this.getControlState("sort", this.defaults.sort);
}
get ascending() {
return this.getControlState("ascending", this.defaults.ascending);
}
get viewStyle() {
return this.getControlState("view", this.defaults.view);
}
get pageControls() {
if (!PAGES[this.currentPage] || !PAGES[this.currentPage].controls || !this.state.controlsVisible) return null;
const {controls: Controls} = PAGES[this.currentPage];
return <Controls
key={`controls-${this.props.type}`}
setState={this.setState.bind(this)}
state={this.state}
type={this.props.type}
/>;
}
update() {this.forceUpdate();}
getControlState(control, defaultValue) {
const {type} = this.props;
const id = `${this.currentPage}ListControls`;
const controls = DataStore.getBDData(id, {});
if (!controls[type] || !controls[type][control]) {
this.setControlState(control, defaultValue);
return defaultValue;
}
return controls[type][control];
}
setControlState(control, value) {
const {type} = this.props;
const id = `${this.currentPage}ListControls`;
const controls = DataStore.getBDData(id, {});
if (!controls[type]) controls[type] = {};
controls[type][control] = value;
DataStore.setBDData(id, controls);
}
reload() {
if (this.props.refreshList) this.props.refreshList();
this.forceUpdate();
}
changeView(view) {
this.setControlState("view", view);
this.setState({viewStyle: view});
}
reverse(value) {
this.setControlState("ascending", value);
this.setState({ascending: value});
}
sort(value) {
this.setControlState("sort", value);
this.setState({sortStyle: value});
}
editAddon(id) {return this.props.editAddon(id);}
openFolder(folder) {
const open = shell.openItem ?? shell.openPath;
open(folder);
}
render() {
const storeEnabled = Settings.get("settings", "addons", "store");
const Page = PAGES[this.currentPage]?.component || (() => null);
return <React.Fragment>
<div className="bd-addon-list-title">
{storeEnabled
? <TabBar
value={this.state.page}
onChange={value => this.setState({page: value})}
items={Object.entries(PAGES).map(([id, props]) => ({
name: props.label,
value: id
}))}
>
</TabBar>
: <SettingsTitle key="title" text={this.props.title} />
}
<div className="bd-addon-list-filters">
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.sortBy}</label>
<Dropdown
key={`${this.props.type}-${this.currentPage}`}
options={CONTROLS[this.currentPage].sortOptions}
value={this.sortStyle}
onChange={value => this.sort(value)}
style="transparent"
/>
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Sorting.order}</label>
<Dropdown
key={`${this.props.type}-${this.currentPage}`}
options={CONTROLS[this.currentPage].directions}
value={this.ascending}
onChange={value => this.reverse(value)}
style="transparent"
/>
</div>
<div className="bd-select-wrapper">
<label className="bd-label">{Strings.Addons.view}</label>
<Dropdown
key={`${this.props.type}-${this.currentPage}`}
options={CONTROLS[this.currentPage].viewOptions}
value={this.viewStyle}
onChange={value => this.changeView(value)}
style="transparent"
/>
</div>
</div>
</div>
<div className="bd-addon-list-controls">
<SearchBar
key={this.props.type + "-search"}
size={SearchBar.Sizes.MEDIUM}
onChange={this.search}
value={this.state.query}
placeholder={Strings.Addons.search.format({type: this.props.title})}
/>
<button
className="bd-button size-small"
onClick={() => this.openFolder(this.props.folder)}
>{Strings.Addons.openFolder.format({type: this.props.type})}</button>
</div>
{this.pageControls}
<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)}
type={this.props.type}
title={this.props.title}
addonState={this.props.addonState}
addonList={this.props.addonList}
folder={this.props.folder}
reload={this.props.reload}
onChange={this.props.onChange}
refreshList={this.props.refreshList}
isLoaded={this.props.isLoaded}
deleteAddon={this.props.deleteAddon}
installAddon={this.props.installAddon}
editAddon={this.editAddon}
confirmAddonDelete={this.props.confirmAddonDelete}
view={this.viewStyle}
sort={this.sortStyle}
query={this.state.query}
ascending={this.ascending}
setControlsVisible={(value) => this.setState({controlsVisible: value})}
/>
</React.Fragment>;
}
}

View File

@ -0,0 +1,101 @@
import Logger from "common/logger";
import {React, Strings, Events} from "modules";
import {Web} from "data";
import AddonCard from "./addoncard";
import ErrorBoundary from "../../errorboundary";
import NoResults from "../../blankslates/noresults";
import EmptyImage from "../../blankslates/emptyimage";
export default class InstalledPage extends React.Component {
constructor() {
super();
this.update = this.update.bind(this);
}
componentDidMount() {
Events.on(`${this.props.type}-loaded`, this.update);
Events.on(`${this.props.type}-unloaded`, this.update);
}
componentWillUnmount() {
Events.off(`${this.props.type}-loaded`, this.update);
Events.off(`${this.props.type}-unloaded`, this.update);
}
update() {
this.forceUpdate();
}
get emptyImage() {
const message = Strings.Addons.blankSlateMessage.format({link: `${Web.PAGES[`${this.props.type}s`]}`, type: this.props.type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type: this.props.type})} message={message} />;
}
render() {
const containerState = this.props.state;
const {addonList, addonState, onChange, reload} = this.props;
let sortedAddons = addonList.sort((a, b) => {
const sortByEnabled = this.props.sort === "isEnabled";
const first = sortByEnabled ? addonState[a.id] : a[this.props.sort];
const second = sortByEnabled ? addonState[b.id] : b[this.props.sort];
const stringSort = (str1, str2) => str1.toLocaleLowerCase().localeCompare(str2.toLocaleLowerCase());
if (typeof(first) == "string") return stringSort(first, second);
if (typeof(first) == "boolean") return (first === second) ? stringSort(a.name, b.name) : first ? -1 : 1;
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!this.props.ascending) sortedAddons.reverse();
if (this.props.query) {
sortedAddons = sortedAddons.filter(addon => {
let matches = addon.name.toLocaleLowerCase().includes(this.props.query);
matches = matches || addon.author.toLocaleLowerCase().includes(this.props.query);
matches = matches || addon.description.toLocaleLowerCase().includes(this.props.query);
if (!matches) return false;
return true;
});
}
const renderedCards = sortedAddons.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary>
<AddonCard
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}
onChange={onChange}
reload={reload}
hasSettings={hasSettings}
getSettingsPanel={getSettings}
/>
</ErrorBoundary>;
});
const hasAddonsInstalled = this.props.addonList.length !== 0;
const isSearching = !!containerState.query;
const hasResults = sortedAddons.length !== 0;
return <React.Fragment>
{!hasAddonsInstalled && this.emptyImage}
{isSearching && !hasResults && hasAddonsInstalled && <NoResults />}
{hasAddonsInstalled && <div key="addonList" className={"bd-addon-list" + (this.props.view == "grid" ? " bd-grid-view" : "")}>{renderedCards}</div>}
</React.Fragment>;
}
}
const originalRender = InstalledPage.prototype.render;
Object.defineProperty(InstalledPage.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonList", "Addon guideline for plugins #3 https://docs.betterdiscord.app/plugins/introduction/guidelines/#scope");},
get: () => originalRender
});

View File

@ -0,0 +1,162 @@
import {React, Strings, Utilities, DiscordClasses, WebAPI} from "modules";
import {Web} from "data";
import Spinner from "../../spinner";
import Next from "../../icons/next";
import Previous from "../../icons/previous";
import NoResults from "../../blankslates/noresults";
import StoreCard from "./storecard";
import Modals from "../../modals";
import Toasts from "../../toasts";
export default class StorePage extends React.Component {
constructor(props) {
super(props);
this.matchAddon = this.matchAddon.bind(this);
this.filterTags = this.filterTags.bind(this);
this.state = {
isLoaded: false,
addons: null,
selectedPage: 0
};
}
componentDidMount() {
this.connect();
}
async connect() {
try {
const data = await WebAPI.getAddons(`${this.props.type}s`);
this.setState({
isLoaded: true,
addons: data
});
}
catch (error) {
Modals.showConfirmationModal(Strings.Store.connectionError, Strings.Store.connectionErrorMessage, {
cancelText: Strings.Modals.close,
confirmText: Strings.Modals.retry,
onConfirm: () => this.connect()
});
}
}
async install(id, filename) {
try {
const contents = await WebAPI.getAddonContents(id);
this.props.installAddon(contents, filename);
}
catch (error) {
Toasts.error(Strings.Store.downloadError.format({type: this.props.type}));
}
}
matchAddon(addon, query) {
let matches = ~addon.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
matches = matches || ~addon.author.display_name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
matches = matches || ~addon.description.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
return matches;
}
filterTags(addon) {
const matches = this.matchAddon(addon, this.props.query);
const tagMatches = this.props.state.selectedTag === "all" || addon.tags.some(tag => tag === this.props.state.selectedTag);
return tagMatches && matches;
}
get addons() {
if (!this.state.isLoaded) return null;
const {sort} = this.props;
const final = this.state.addons
.flat(10)
.filter(this.filterTags)
.sort((a, b) => {
const first = a[sort];
const second = b[sort];
if (typeof(first) == "string") return first.toLocaleLowerCase().localeCompare(second.toLocaleLowerCase());
if (first > second) return 1;
if (second > first) return -1;
return 0;
});
if (!this.props.ascending) final.reverse();
return Utilities.splitArray(final, 16);
}
render() {
const containerState = this.props.state;
if (this.props.query !== this.latestSearchQuery || containerState.selectedTag !== this.latestSelectedTag) this.setState({selectedPage: 0});
this.latestSearchQuery = this.props.query;
this.latestSelectedTag = containerState.selectedTag;
const addons = this.addons;
const canGoForward = addons && (addons.length > 1 && this.state.selectedPage < addons.length - 1);
const canGoBackward = this.state.selectedPage > 0;
const handleSelect = getState => () => {
this.setState({selectedPage: getState(this.state.selectedPage)});
const element = document.getElementsByClassName("bd-addon-list-title")[0]?.parentElement?.parentElement;
element?.scrollTo(0, 0);
};
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.className("bd-store-addons", this.props.view + "-view")}>
{addons[this.state.selectedPage].map(addon => {
const thumbnail = Web.ENDPOINTS.thumbnail(addon.thumbnail_url);
return <StoreCard
{...addon}
key={addon.id}
filename={addon.file_name}
releaseDate={new Date(addon.release_date)}
thumbnail={thumbnail}
selectedTag={containerState.selectedTag}
isInstalled={this.props.isLoaded(addon.file_name)}
onInstall={() => Modals.showInstallationModal({
...addon,
thumbnail,
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.props.confirmAddonDelete(addon.file_name)}
onForceDelete={() => this.props.deleteAddon(addon.file_name)}
/>;
})}
</div>
: this.state.isLoaded && <NoResults />
}
{this.state.isLoaded && addons.length > 1 && <nav className="bd-page-control">
<button className="bd-page-button" onClick={handleSelect(s => s - 1)} disabled={!canGoBackward}>
<Previous />
{Strings.Store.back}
</button>
<div className={`bd-page-buttons ${DiscordClasses.Scrollers.thin}`}>
{addons.length
? addons.map((_, index) => <div
role="button"
aria-label={`Page ${index + 1}`}
aria-current={index === this.state.selectedPage ? "page" : undefined}
tabIndex="0"
className={Utilities.className("bd-page-item bd-page-button", {selected: index === this.state.selectedPage})}
onClick={handleSelect(() => index)}
>
<span>{index + 1}</span>
</div>)
: null
}
</div>
<button className="bd-page-button" onClick={handleSelect(s => s + 1)} disabled={!canGoForward}>
{Strings.Store.next}
<Next />
</button>
</nav>}
</div>;
}
}

View File

@ -0,0 +1,100 @@
import {React, Strings, Utilities} from "modules";
import Heart from "../../icons/heart";
import Download from "../../icons/download";
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";
if (n >= 1e6 && n < 1e9) return +(n / 1e6).toFixed(1) + "M";
}
monthsAgo(date) {
const current = new Date();
const release = new Date(date);
const months = (((current.getFullYear() - release.getFullYear()) * 12) - release.getMonth()) + current.getMonth();
return Math.max(months, 0);
}
async onButtonClick(event) {
event.stopPropagation();
event.preventDefault();
if (this.props.isInstalled) {
if (event.shiftKey) this.props.onForceDelete?.();
else this.props.onDelete?.();
}
else {
if (event.shiftKey) this.props.onForceInstall?.();
else this.props.onInstall?.();
}
}
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;
return <div className={"bd-store-card" + (className ? ` ${className}` : "")} data-addon-name={name}>
<div className="bd-store-card-header">
<div className="bd-store-card-splash">
<img
key={thumbnail}
alt={name}
src={thumbnail}
/>
</div>
<div className="bd-store-card-icon">
<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 && <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.className({selected: tag === selectedTag})}>{tag}</span>)}
</div>
<div className="bd-store-card-footer">
<div className="bd-store-card-stats">
<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
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>
</div>
</div>
</div>;
}
}

View File

@ -1,22 +0,0 @@
import {React} from "modules";
import SearchIcon from "../../icons/search";
export default class Search extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(e);
}
render() {
return <div className={"bd-search-wrapper" + (this.props.className ? ` ${this.props.className}` : "")}>
<input onChange={this.onChange} onKeyDown={this.props.onKeyDown} type="text" className="bd-search" placeholder={this.props.placeholder} maxLength="50" value={this.state.value} />
<SearchIcon />
</div>;
}
}

View File

@ -0,0 +1,41 @@
import {React, Utilities} from "modules";
import Close from "../../icons/close";
import Search from "../../icons/search";
const Sizes = {
SMALL: "small",
LARGE: "large",
MEDIUM: "medium"
};
export default class SearchBar extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.state = {
hasContent: !!props.value,
value: props.value || ""
};
}
static get Sizes() {return Sizes;}
onChange({target: {value}}) {
this.setState({value, hasContent: !!value});
if (typeof(this.props.onChange) === "function") this.props.onChange(value);
}
render() {
const {className, size = Sizes.SMALL, placeholder, disabled = false} = this.props;
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.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

@ -10,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

@ -0,0 +1,33 @@
import {React, Utilities} from "modules";
export const Type = {
WANDERING_CUBES: "wandering-cubes",
CHASING_DOTS: "chasing-dots",
PULSING_ELLIPSIS: "pulsing-ellipsis",
SPINNING_CIRCLE: "spinning-circle",
LOW_MOTION: "low-motion"
};
export default class Spinner extends React.Component {
static get Type() {return Type;}
renderItems(type) {
if (type !== Type.SPINNING_CIRCLE) {
const itemAmount = type === Type.LOW_MOTION || type === Type.PULSING_ELLIPSIS ? 3 : 2;
return Array.from({length: itemAmount}).map(() => <span className="bd-spinner-item" />);
}
return <svg className="bd-spinner-circular" viewBox="25 25 50 50">
{Array.from({length: 3}).map(() => <circle cx="50" cy="50" r="20" />)}
</svg>;
}
render() {
const {className, type = Type.WANDERING_CUBES, ...props} = this.props;
return <div className={Utilities.className("bd-spinner", `bd-spinner-${type}`, className)} {...props}>
<span className="bd-spinner-inner">
{this.renderItems(type)}
</span>
</div>;
}
}

View File

@ -0,0 +1,64 @@
import {React, Utilities} from "modules";
export default class TabBar extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.listRef = React.createRef();
this.state = {
selected: this.props.items.find(item => item.value === this.props.value)
};
}
// getDeriveredStateFromProps(props, state) {
// if (props.value !== state.selected) {
// return {selected: props.value};
// }
// return null;
// }
onChange(item) {
this.setState({selected: item});
if (typeof(this.props.onChange) === "function") this.props.onChange(item.value);
}
onKeyDown(event) {
const children = this.listRef.current.children;
const keyMap = {
ArrowRight: Array.from(children).indexOf(event.currentTarget) + 1,
ArrowLeft: Array.from(children).indexOf(event.currentTarget) - 1,
Home: 0,
End: children.length - 1
};
if (keyMap.hasOwnProperty(event.key)) {
event.preventDefault();
children[keyMap[event.key]]?.focus();
}
}
render() {
const {selected} = this.state;
return <ul role="tablist" className="bd-tab-bar" aria-orientation="horizontal" ref={this.listRef}>
{this.props.items.map(item => (
<button
role="tab"
key={item.value}
tabIndex={((item.value === selected.value) && !item.disabled) ? "0" : "-1"}
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}
aria-selected={item.value === selected.value}
>
{item.name}
</button>
))}
{this.props.children}
</ul>;
}
}