Merge pull request #1661 from BetterDiscord/development

Merge changes from development
This commit is contained in:
Zerebos 2024-03-04 15:15:58 -05:00 committed by GitHub
commit d390a6966e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 4810 additions and 3907 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
auto-install-peers=false

View File

@ -2,6 +2,93 @@
This changelog starts with the restructured 1.0.0 release that happened after context isolation changes. The changelogs here should more-or-less mirror the ones that get shown in the client but probably with less formatting and pizzazz.
## 1.10.0
### Added
- Enable/disable all buttons for both AddonLists
- BDContextMenu has been integrated
- Ability to reset to default settings of any collection
- Several design components like Flex, Button, Text, and all Modal components
- Added a backup modal stack and backdrop in case we lose Discord's again
- Added a wanring for large debug logs
- `onClose` for modal APIs
- New debug information in user settings
### Removed
- Several unused "known modules"
- `DiscordClasses` module since it was seldom used
- `ClassName` and similar constructs
- Legacy emote assets
### Changed
- Search bars now auto focus
- Opening folder on Windows now occur in the foreground
- Search fields are now clearable
- AddonList pages now show totals and results of searches
- Custom CSS live update is now debounced and using a proper switch not a checkbox
- ConfirmationModal and ChangelogModal are now using custom components
- Updater panel now uses icons rather than text buttons
- AddonList panels have been rearranged to accomodate the new buttons
- The anonymous BD patch function is now named `BDPatcher`
- Modal APIs now use internal ConfirmationModal component
- Markdown areas now consistenly allow for embedded links
- Lazy loaded modules are returned to the original source
### Fixed
- Fixed grabbing the wrong module for accepting invites
- Fixed more strings that were not translatable
## 1.9.8
### Added
### Removed
### Changed
### Fixed
- Removed `ipc.sendTo` for electron 28
- Fixed core updater not using the semver comparator
## 1.9.7
### Added
- Support for Spanish (LATAM) locale
### Removed
### Changed
- Updated translations
- Ignore relative requires (This is because when favoriting a GIF and other UI actions, Discord repeatedly attempts to load relative requires on accident causing our code to make tons of FS requests causing lag.)
### Fixed
- Fixed locale not falling back to English properly
## 1.9.6
### Added
- All HTTP request options for bd-fetch
### Removed
### Changed
- Updated translations
### Fixed
- Fixed race conditions for notices
- Fixed options not being sent to fetch
## 1.9.5
### Added
### Removed
### Changed
### Fixed
- Fixed the webpack patch for the new loader
## 1.9.4
### Added

View File

@ -1,6 +1,6 @@
<div align="center">
[![BetterDiscord Logo](https://media.discordapp.net/attachments/341062370627682315/1053477174566465567/logo_large_ish.png)](https://betterdiscord.app/)
<a href="https://betterdiscord.app"><img src="https://betterdiscord.app/resources/branding/logo_large.svg" alt="BetterDiscord Logo" style="width: 70%"/></a>
[![CI Status][build-badge]][build-link] [![GitHub Releases][downloads-badge]][downloads-link] [![Discord][discord-badge]][discord-link] [![Website][website-badge]][website-link] [![Docs][docs-badge]][docs-link]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
module.exports = {
blocklist: require("./blocklist.json"),
BTTV: require("./bttv.json"),
FrankerFaceZ: require("./frankerfacez.json"),
TwitchGlobal: require("./twitchglobal.json"),
TwitchSubscriber: require("./twitchsubscriber.json")
};

View File

@ -1 +0,0 @@
{"JKanStyle":15,"OptimizePrime":16,"StoneLightning":17,"TheRinger":18,"RedCoat":22,"Kappa":25,"JonCarnage":26,"MrDestructoid":28,"BCWarrior":30,"GingerPower":32,"DansGame":33,"SwiftRage":34,"PJSalt":36,"KevinTurtle":40,"Kreygasm":41,"SSSsss":46,"PunchTrees":47,"ArsonNoSexy":50,"SMOrc":52,"FrankerZ":65,"OneHand":66,"HassanChop":68,"BloodTrail":69,"DBstyle":73,"AsianGlow":74,"BibleThump":86,"ShazBotstix":87,"PogChamp":88,"PMSTwin":92,"FUNgineer":244,"ResidentSleeper":245,"4Head":354,"HotPokket":357,"FailFish":360,"DAESuppy":973,"WholeWheat":1896,"ThunBeast":1898,"TF2John":1899,"RalpherZ":1900,"Kippa":1901,"Keepo":1902,"BigBrother":1904,"SoBayed":1906,"PeoplesChamp":3412,"GrammarKing":3632,"PanicVis":3668,"ANELE":3792,"BrokeBack":4057,"PipeHype":4240,"YouWHY":4337,"RitzMitz":4338,"EleGiggle":4339,"TheThing":7427,"HassaanChop":20225,"BabyRage":22639,"panicBasket":22998,"PermaSmug":27509,"BuddhaBar":27602,"WutFace":28087,"PRChase":28328,"Mau5":30134,"HeyGuys":30259,"NotATK":34875,"mcaT":35063,"TTours":38436,"PraiseIt":38586,"HumbleLife":46881,"CorgiDerp":49106,"ArgieB8":51838,"ShadyLulu":52492,"KappaPride":55338,"CoolCat":58127,"DendiFace":58135,"NotLikeThis":58765,"riPepperonis":62833,"duDudu":62834,"bleedPurple":62835,"twitchRaid":62836,"SeemsGood":64138,"MingLee":68856,"KappaRoss":70433,"KappaClaus":74510,"OhMyDog":81103,"OSFrog":81248,"SeriousSloth":81249,"KomodoHype":81273,"VoHiYo":81274,"MikeHogu":81636,"KappaWealth":81997,"cmonBruh":84608,"SmoocherZ":89945,"NomNom":90075,"StinkyCheese":90076,"ChefFrank":90129,"FutureMan":98562,"OpieOP":100590,"DoritosChip":102242,"PJSugar":102556,"VoteYea":106293,"VoteNay":106294,"RuleFive":107030,"DxCat":110734,"DrinkPurple":110785,"TinyFace":111119,"PicoMause":111300,"TheTarFu":111351,"DatSheffy":111700,"UnSane":111792,"copyThis":112288,"pastaThat":112289,"imGlitch":112290,"GivePLZ":112291,"TakeNRG":112292,"BlargNaut":114738,"DogFace":114835,"Jebaited":114836,"TooSpicy":114846,"WTRuck":114847,"UncleNox":114856,"RaccAttack":114870,"StrawBeary":114876,"PrimeMe":115075,"BrainSlug":115233,"BatChest":115234,"CurseLit":116625,"Poooound":117484,"FreakinStinkin":117701,"SuperVinlin":118772,"TriHard":120232,"CoolStoryBob":123171,"ItsBoshyTime":133468,"KAPOW":133537,"YouDontSay":134254,"UWot":134255,"RlyTho":134256,"PartyTime":135393,"NinjaGrumpy":138325,"MVGame":142140,"TBAngel":143490,"TheIlluminati":145315,"BlessRNG":153556,"MorphinTime":156787,"ThankEgg":160392,"ArigatoNas":160393,"BegWan":160394,"BigPhish":160395,"InuyoFace":160396,"Kappu":160397,"KonCha":160400,"PunOko":160401,"SabaPing":160402,"TearGlove":160403,"TehePelo":160404,"TwitchLit":166263,"CarlSmile":166266,"CrreamAwk":191313,"Squid1":191762,"Squid2":191763,"Squid3":191764,"Squid4":191767,"TwitchUnity":196892,"TPcrunchyroll":323914,"EntropyWins":376765,"PowerUpR":425671,"PowerUpL":425688,"HSCheers":444572,"HSWP":446979,"DarkMode":461298,"TwitchVotes":479745,"TPFufun":508650,"RedTeam":530888,"GreenTeam":530890,"PurpleStar":624501,"FBtouchdown":626795,"PopCorn":724216,"TombRaid":864205,"EarthDay":959018,"PartyHat":965738,"MercyWing1":1003187,"MercyWing2":1003189,"PinkMercy":1003190,"BisexualPride":1064987,"LesbianPride":1064988,"GayPride":1064991,"TransgenderPride":1064995,"AsexualPride":1130348,"PansexualPride":1130349,"TwitchRPG":1220086,"IntersexPride":1221184,"MaxLOL":1290325,"NonBinaryPride":1297279,"GenderFluidPride":1297281,"FBRun":1441261,"FBPass":1441271,"FBSpiral":1441273,"FBBlock":1441276,"FBCatch":1441281,"FBChallenge":1441285,"FBPenalty":1441289,"PixelBob":1547903,"GunRun":1584743,"HolidayCookie":1713813,"HolidayLog":1713816,"HolidayOrnament":1713818,"HolidayPresent":1713819,"HolidaySanta":1713822,"HolidayTree":1713825,"SoonerLater":2113050,"TwitchSings":300116344,"SingsMic":300116349,"SingsNote":300116350}

File diff suppressed because one or more lines are too long

View File

@ -162,7 +162,14 @@
"version": "Version",
"added": "Date Added",
"modified": "Date Modified",
"isEnabled": "Enabled",
"listView": "List View",
"gridView": "Grid View",
"enableAll": "Enable All",
"disableAll": "Disable All",
"enableAllWarning": "Enabling all {{type}} can cause temporary lag and unexpected errors.\n\n(Hold shift while clicking to skip this prompt!)",
"search": "Search {{type}}",
"results": "{{count}} Results",
"editAddon": "Edit",
"deleteAddon": "Delete",
"confirmDelete": "Are you sure you want to delete {{name}}?",
@ -186,14 +193,7 @@
"wasUnloaded": "{{name}} was unloaded.",
"blankSlateHeader": "You don't have any {{type}}s!",
"blankSlateMessage": "Grab some from [this website]({{link}}) and add them to your {{type}} folder.",
"isEnabled": "Enabled",
"wasLoaded": "{{name}} v{{version}} was loaded.",
"listView": "List View",
"gridView": "Grid View",
"enableAll": "Enable All",
"disableAll": "Disable All",
"results": "{{count}} Results",
"enableAllWarning": "Enabling all {{type}} can cause temporary lag and unexpected errors.\n\n(Hold shift while clicking to skip this prompt!)"
"wasLoaded": "{{name}} v{{version}} was loaded."
},
"CustomCSS": {
"confirmationText": "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",
@ -255,5 +255,14 @@
"versionAvailable": "Version {{version}} now available!",
"upToDateBlankslate": "All of your {{type}} seem to be up to date!",
"updateButton": "Update!"
},
"Settings": {
"customColor": "Custom Color",
"resetSettings": "Reset Settings",
"resetSettingsWarning": "Resetting your settings returns them all to the original state, this cannot be undone."
},
"Socials": {
"twitter": "Follow Us!",
"github": "Star Us!"
}
}

View File

@ -11,6 +11,6 @@
"lint": "eslint --ext .js src/"
},
"devDependencies": {
"webpack": "^5.73.0"
"webpack": "^5.90.3"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "betterdiscord",
"version": "1.9.7",
"version": "1.10.0",
"description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js",
"scripts": {
@ -17,20 +17,26 @@
"translations": "node -r dotenv/config scripts/translations.js"
},
"devDependencies": {
"@babel/core": "^7.16.12",
"@babel/preset-env": "^7.16.11",
"@babel/register": "^7.16.9",
"asar": "^3.2.0",
"dotenv": "^16.0.3",
"eslint": "^8.23.0",
"eslint-plugin-react": "^7.31.6",
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/register": "^7.23.7",
"@electron/asar": "^3.2.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"mocha": "^10.0.0",
"webpack": "^5.74.0",
"mocha": "^10.3.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.90.3",
"webpack-cli": "^4.10.0"
},
"engines": {
"node": ">=14",
"pnpm": ">=7"
},
"pnpm": {
"overrides": {
"json5@<1.0.2": ">=1.0.2"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,6 @@
"lint": "eslint --ext .js src/"
},
"devDependencies": {
"webpack": "^5.73.0"
"webpack": "^5.90.3"
}
}

View File

@ -16,22 +16,22 @@
"test-prod": "npm run test -- --reporter min"
},
"devDependencies": {
"@babel/core": "^7.16.12",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/register": "^7.16.9",
"babel-loader": "^8.2.3",
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@babel/register": "^7.23.7",
"babel-loader": "^8.3.0",
"babel-plugin-module-resolver": "^4.1.0",
"circular-dependency-plugin": "^5.2.2",
"css-loader": "^6.5.1",
"postcss": "^8.4.5",
"css-loader": "^6.10.0",
"postcss": "^8.4.35",
"postcss-cli": "^9.1.0",
"postcss-csso": "^6.0.0",
"postcss-csso": "^6.0.1",
"postcss-easy-import": "^4.0.0",
"postcss-loader": "^6.2.1",
"stylelint": "^14.3.0",
"stylelint": "^14.16.1",
"stylelint-config-standard": "^24.0.0",
"webpack": "^5.73.0"
"webpack": "^5.90.3"
},
"dependencies": {
"buffer": "^6.0.3"

View File

@ -134,7 +134,7 @@ export default new class CustomCSS extends Builtin {
save: this.saveCSS.bind(this),
update: this.insertCSS.bind(this),
openNative: this.openNative.bind(this),
onChange: this.onChange.bind(this)
onChange: Utilities.debounce(this.onChange.bind(this), 500)
});
FloatingWindows.open({

View File

@ -69,6 +69,8 @@ export default new class DebugLogs extends Builtin {
async checkFilesize() {
try {
// Not been created yet, no need to check filesize
if (!fs.existsSync(this.logFile)) return;
const stats = fs.statSync(this.logFile);
const mb = stats.size / (1024 * 1024);
if (mb < 100) return; // Under 100MB, all good

View File

@ -7,6 +7,8 @@ import Webpack from "@modules/webpackmodules";
import ContextMenuPatcher from "@modules/api/contextmenu";
import pluginManager from "@modules/pluginmanager";
import themeManager from "@modules/thememanager";
import Utilities from "@modules/utilities";
import React from "@modules/react";
const ContextMenu = new ContextMenuPatcher();
@ -31,25 +33,49 @@ export default new class BDContextMenu extends Builtin {
}
callback(retVal) {
const target = Utilities.findInTree(retVal, b => Array.isArray(b) && b.some(e => e?.key?.toLowerCase() === "my_account"), {walkable: ["props", "children"]});
if (!target) return;
// Prevent conflict with plugin until its eradicated
if (target.some(e => e.props.label.toLowerCase() === "betterdiscord")) return;
// BetterDiscord Settings
const items = Settings.collections.map(c => this.buildCollectionMenu(c));
items.push({label: Strings.panels.updates, action: () => {this.openCategory("updates");}});
if (Settings.get("settings", "customcss", "customcss")) items.push({label: Strings.panels.customcss, action: () => {this.openCategory("customcss");}});
items.push(this.buildAddonMenu(Strings.panels.plugins, pluginManager));
items.push(this.buildAddonMenu(Strings.panels.themes, themeManager));
retVal?.props?.children?.props?.children?.[0].push(ContextMenu.buildItem({type: "separator"}));
retVal?.props?.children?.props?.children?.[0].push(ContextMenu.buildItem({type: "submenu", label: "BetterDiscord", items: items}));
// Updater
items.push({
label: Strings.Panels.updates,
action: () => this.openCategory("updates")
});
// Custom CSS
if (Settings.get("settings", "customcss", "customcss")) {
items.push({
label: Strings.Panels.customcss,
action: () => this.openCategory("customcss")
});
}
// Plugins & Themes
items.push(this.buildAddonMenu(Strings.Panels.plugins, pluginManager));
items.push(this.buildAddonMenu(Strings.Panels.themes, themeManager));
// Parent SubMenu
const bdSubMenu = ContextMenu.buildItem({type: "submenu", label: "BetterDiscord", items: items});
const bdGroup = React.createElement(ContextMenu.Group, null, [bdSubMenu]);
target.push(bdGroup);
}
buildCollectionMenu(collection) {
return {
type: "submenu",
label: collection.name,
action: () => {this.openCategory(collection.name);},
action: () => this.openCategory(collection.name),
items: collection.settings.map(category => {
return {
type: "submenu",
label: category.name,
action: () => {this.openCategory(collection.name);},
action: () => this.openCategory(collection.name),
items: category.settings.filter(s => s.type === "switch" && !s.hidden && s.id !== this.id).map(setting => {
return {
type: "toggle",
@ -65,7 +91,7 @@ export default new class BDContextMenu extends Builtin {
}
/**
*
* TODO: Can this be done better now that it's integrated?
* @param {string} label
* @param {import("../../modules/addonmanager").default} manager
* @returns
@ -75,14 +101,14 @@ export default new class BDContextMenu extends Builtin {
return {
type: "submenu",
label: label,
action: () => {this.openCategory(label.toLowerCase());},
action: () => this.openCategory(label.toLowerCase()),
items: names.map(name => {
return {
type: "toggle",
label: name,
disabled: manager.getAddon(name)?.partial ?? false,
active: manager.isEnabled(name),
action: () => {manager.toggleAddon(name);}
action: () => manager.toggleAddon(name)
};
})
};

View File

@ -1,16 +1,56 @@
// fixed, improved, added, progress
export default {
description: "This is a small but very important update to fix some key issues!",
video: "https://www.youtube.com/embed/evyvq9eQTqA?si=opmzjGjUArT4VLrj&vq=hd720p&hd=1&rel=0&showinfo=0&mute=1&loop=1&autohide=1",
description: "This update brings a lot of small features that have been requested for a long time. For a quick demo of what's included in this update, take a look at the video above! If you want more details, keep reading on below!",
changes: [
{
title: "What's Fixed?",
title: "Cool New Features",
type: "added",
description: "All of these features can be seen demo'd in the video up top!",
items: [
"**Enable/disable all buttons** for plugins and themes is finally here! With it comes a revamped set of controls on the plugins and themes pages. You'll find the search field has shifted up top while the open folder button has moved down and next to the new buttons.",
"The **[BDContextMenu](https://betterdiscord.app/plugin/BDContextMenu) plugin has now been integrated** directly into BetterDiscord! If you're using it, *go ahead and delete it*. If you're not, try and right click the cog menu near your avatar at the bottom left.",
"You can now **reset to default settings!** This helps when trying to debug why individuals pieces of BetterDiscord aren't working or figuring out if a problem you're having is from a plugin or not.",
"BetterDiscord is now using a more **consistent design components!** Not only does this make the UI more __visually consistent for you,__ but it also makes developing easier for us as we add new features!",
"Next time you need support, there is now **new debug information** at the bottom left of your settings page. Click it to copy advanced info and give it to the support team to help you out!",
]
},
{
title: "Quality Of Life Improvements",
type: "progress",
items: [
"The search bar on the plugins and themes pages will now auto focus upon entering. (Thanks [@flatypus](https://github.com/flatypus))",
"Opening the plugin or theme folder will now happen in the foreground. (Thanks [@pschaub](https://github.com/pschaub))",
"Users of the debug logging option will be notified when their log becomes overly large with a prompt to delete.",
"All search fields are now clearable! The search icon will turn into an `x` when you input a query",
"The search field on the plugins and theme pages will now show the total number installed.",
"Searching for plugins and theme will display the number of results in the title.",
"The Updater panel now uses proper icons for buttons instead of 3 different sized buttons with text.",
"Live updating in Custom CSS is now smoother due to a new debouncer. This setting also now uses the common switch rather than a basic checkbox.",
"This Changelog you're reading right now is now made using custom components. That means it should keep working even if Discord updates and changes things again!"
]
},
{
title: "Bugs Squashed",
type: "fixed",
items: [
"Spanish (LATAM) is now properly supported.",
"Future cases of unrecognized locales as well as locale fallback now works as intended and shouldn't cause loading issues.",
"Updated translations for Vietnamese locale.",
"Fixed an issue where certain actions (such as favoriting GIFs) caused unexpected lag.",
"Fixed some issues with general client lag."
"BetterDiscord should now be compatible with Electron 28 which is currently used in Canary/PTB.",
"Clicking the support server button on plugin/theme cards should work again! (Thanks [@Huderon](https://github.com/Huderon))",
"More untranslated strings were added to the translatable list.",
"Lazy loaded modules are now restored to their original source. (Thanks [@Skamt](https://github.com/Skamt))",
"Markdown used by BetterDiscord will now behave more consistently and embed links.",
"Fixed some issues with general client lag.",
]
},
{
title: "Developer Notes",
type: "improved",
items: [
"Modal APIs now support `onClose` which will be called when the modal is closed regardless of the reason.",
"The anonymous patch function from `Patcher` is no longer anonymous, it will show up as `BDPatcher` to make it clearer what the function is.",
"Internally, BetterDiscord has moved to consistently using new UI components `Flex`, `Button`, and `Text`. These will become available to developers once they are more tested.",
"BetterDiscord has also started using custom modal components including `ModalHeader`, `ModalContent`, `ModalFooter` `ModalRoot`, `ModalStack`, `CloseButton`, and `Backdrop`. ",
"The Modal APIs now use our custom `ConfirmationModal` component built from the components above. This should prove more consistent through Discord updates. Access to the component itself will come in a future update.",
]
}
]

View File

@ -1,51 +0,0 @@
import ClassName from "@structs/classname";
import Utilities from "./utilities";
import WebpackModules from "./webpackmodules";
const combineClasses = function (...props) {
return Object.assign({}, ...props.map(prop => WebpackModules.getByProps(...prop)));
};
const DiscordClassModules = Utilities.memoizeObject({
get Text() {
return combineClasses(
["size20", "size12"],
["selectable", "colorMuted"]
);
},
get Titles() {
return combineClasses(
["wrapper", "base"],
["defaultColor", "h4"]
);
},
get EmptyImage() {return WebpackModules.getByProps("emptyImage", "emptyHeader");},
get Modal() {return WebpackModules.getByProps("content", "root", "header", "close");},
get Scrollers() {return WebpackModules.getByProps("thin", "scrollerBase", "content");},
get Margins() {return WebpackModules.getByProps("marginXSmall", "marginBottom8");},
get Integrations() {return WebpackModules.getByProps("secondaryHeader", "detailsWrapper");},
get Card() {return WebpackModules.getByProps("card", "topDivider", "description");},
});
const emptyClassModule = new Proxy({}, {
get() {return "";}
});
const DiscordClasses = new Proxy(DiscordClassModules, {
get(list, item) {
if (list[item] === undefined) return emptyClassModule;
if (typeof(list[item]) === "string") return list[item];
return new Proxy(list[item], {
get(obj, prop) {
if (!(prop in obj)) return "";
return new ClassName(obj[prop]);
}
});
}
});
export default DiscordClasses;

View File

@ -12,151 +12,13 @@ import WebpackModules, {Filters} from "./webpackmodules";
export default Utilities.memoizeObject({
get React() {return WebpackModules.getByProps("createElement", "cloneElement");},
get ReactDOM() {return WebpackModules.getByProps("render", "findDOMNode");},
get Flux() {return WebpackModules.getByProps("connectStores");},
get Events() {return WebpackModules.getByPrototypes("setMaxListeners", "emit");},
/* Guild Info, Stores, and Utilities */
get GuildStore() {return WebpackModules.getByProps("getGuild");},
get SortedGuildStore() {return WebpackModules.getByProps("getSortedGuilds");},
get SelectedGuildStore() {return WebpackModules.getByProps("getLastSelectedGuildId");},
get GuildSync() {return WebpackModules.getByProps("getSyncedGuilds");},
get GuildInfo() {return WebpackModules.getByProps("getAcronym");},
get GuildChannelsStore() {return WebpackModules.getByProps("getChannels", "getDefaultChannel");},
get GuildMemberStore() {return WebpackModules.getByProps("getMember");},
get MemberCountStore() {return WebpackModules.getByProps("getMemberCounts");},
get GuildEmojiStore() {return WebpackModules.getByProps("getEmojis");},
get GuildActions() {return WebpackModules.getByProps("markGuildAsRead");},
get GuildPermissions() {return WebpackModules.getByProps("getGuildPermissions");},
/* Channel Store & Actions */
get ChannelStore() {return WebpackModules.getByProps("getChannel", "getDMFromUserId");},
get SelectedChannelStore() {return WebpackModules.getByProps("getLastSelectedChannelId");},
get ChannelActions() {return WebpackModules.getByProps("selectChannel");},
get PrivateChannelActions() {return WebpackModules.getByProps("openPrivateChannel");},
get ChannelSelector() {return WebpackModules.getByProps("selectGuild", "selectChannel");},
/* Current User Info, State and Settings */
get UserInfoStore() {return WebpackModules.getByProps("getToken");},
get LocaleStore() {return WebpackModules.getByProps("locale", "initialize");},
get ThemeStore() {return WebpackModules.getByProps("theme", "initialize");},
get AccountManager() {return WebpackModules.getByProps("register", "login");},
get UserSettingsUpdater() {return WebpackModules.getByProps("updateRemoteSettings");},
get OnlineWatcher() {return WebpackModules.getByProps("isOnline");},
get CurrentUserIdle() {return WebpackModules.getByProps("getIdleTime");},
get RelationshipStore() {return WebpackModules.getByProps("isBlocked", "getFriendIDs");},
get RelationshipManager() {return WebpackModules.getByProps("addRelationship");},
get MentionStore() {return WebpackModules.getByProps("getMentions");},
/* User Stores and Utils */
get UserStore() {return WebpackModules.getByProps("getCurrentUser", "getUser");},
get UserStatusStore() {return WebpackModules.getByProps("getStatus", "getState");},
get UserTypingStore() {return WebpackModules.getByProps("isTyping");},
get UserActivityStore() {return WebpackModules.getByProps("getActivity");},
get UserNameResolver() {return WebpackModules.getByProps("getName");},
get UserNoteStore() {return WebpackModules.getByProps("getNote");},
get UserNoteActions() {return WebpackModules.getByProps("updateNote");},
/* Emoji Store and Utils */
get EmojiInfo() {return WebpackModules.getByProps("isEmojiDisabled");},
get EmojiUtils() {return WebpackModules.getByProps("getGuildEmoji");},
get EmojiStore() {return WebpackModules.getByProps("getByCategory", "EMOJI_NAME_RE");},
/* Invite Store and Utils */
get InviteStore() {return WebpackModules.getByProps("getInvites");},
get InviteResolver() {return WebpackModules.getByProps("findInvite");},
get InviteActions() {return WebpackModules.getByProps("createInvite");},
/* Discord Objects & Utils */
get DiscordConstants() {return WebpackModules.getByProps("Permissions", "ActivityTypes", "StatusTypes");},
get DiscordPermissions() {return WebpackModules.getByProps("Permissions", "ActivityTypes", "StatusTypes").Permissions;},
get PermissionUtils() {return WebpackModules.getByProps("getHighestRole");},
get ColorConverter() {return WebpackModules.getByProps("hex2int");},
get ColorShader() {return WebpackModules.getByProps("darken");},
get TinyColor() {return WebpackModules.getByPrototypes("toRgb");},
get ClassResolver() {return WebpackModules.getByProps("getClass");},
get ButtonData() {return WebpackModules.getByProps("ButtonSizes");},
get IconNames() {return WebpackModules.getByProps("IconNames");},
get NavigationUtils() {return WebpackModules.getByProps("transitionTo", "replaceWith", "getHistory");},
/* Discord Messages */
get MessageStore() {return WebpackModules.getByProps("getMessages");},
get MessageActions() {return WebpackModules.getByProps("jumpToMessage", "_sendMessage");},
get MessageQueue() {return WebpackModules.getByProps("enqueue");},
get MessageParser() {return WebpackModules.getByProps("createMessage", "parse", "unparse");},
/* Text Processing */
get hljs() {return WebpackModules.getByProps("highlight", "highlightBlock");},
get SimpleMarkdown() {return WebpackModules.getByProps("parseBlock", "parseInline", "defaultOutput");},
/* Experiments */
get ExperimentStore() {return WebpackModules.getByProps("getExperimentOverrides");},
get ExperimentsManager() {return WebpackModules.getByProps("isDeveloper");},
get CurrentExperiment() {return WebpackModules.getByProps("getExperimentId");},
/* Images, Avatars and Utils */
get ImageResolver() {return WebpackModules.getByProps("getUserAvatarURL", "getGuildIconURL");},
get ImageUtils() {return WebpackModules.getByProps("getSizedImageSrc");},
get AvatarDefaults() {return WebpackModules.getByProps("getUserAvatarURL", "DEFAULT_AVATARS");},
/* Window, DOM, HTML */
get WindowInfo() {return WebpackModules.getByProps("isFocused", "windowSize");},
get TagInfo() {return WebpackModules.getByProps("VALID_TAG_NAMES");},
get DOMInfo() {return WebpackModules.getByProps("canUseDOM");},
/* Locale/Location and Time */
get LocaleManager() {return WebpackModules.getByProps("setLocale");},
get Moment() {return WebpackModules.getByProps("parseZone");},
get LocationManager() {return WebpackModules.getByProps("createLocation");},
get Timestamps() {return WebpackModules.getByProps("fromTimestamp");},
get TimeFormatter() {return WebpackModules.getByProps("dateFormat");},
/* Strings and Utils */
get Strings() {return WebpackModules.getByProps("Messages").Messages;},
get StringFormats() {return WebpackModules.getByProps("a", "z");},
get StringUtils() {return WebpackModules.getByProps("toASCII");},
/* URLs and Utils */
get URLParser() {return WebpackModules.getByProps("Url", "parse");},
get ExtraURLs() {return WebpackModules.getByProps("getArticleURL");},
/* Drag & Drop */
get DNDActions() {return WebpackModules.getByProps("beginDrag");},
get DNDSources() {return WebpackModules.getByProps("addTarget");},
get DNDObjects() {return WebpackModules.getByProps("DragSource");},
/* Media Stuff (Audio/Video) */
get MediaDeviceInfo() {return WebpackModules.getByProps("Codecs", "SUPPORTED_BROWSERS");},
get MediaInfo() {return WebpackModules.getByProps("getOutputVolume");},
get MediaEngineInfo() {return WebpackModules.getByProps("MediaEngineFeatures");},
get VoiceInfo() {return WebpackModules.getByProps("EchoCancellation");},
get VideoStream() {return WebpackModules.getByProps("getVideoStream");},
get SoundModule() {return WebpackModules.getByProps("playSound");},
/* Electron & Other Internals with Utils*/
get ElectronModule() {return WebpackModules.getByProps("setBadge");},
get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");},
get PathUtils() {return WebpackModules.getByProps("hasBasename");},
get NotificationModule() {return WebpackModules.getByProps("showNotification");},
get RouterModule() {return WebpackModules.getByProps("Router");},
get APIModule() {return WebpackModules.getByProps("getAPIBaseURL");},
get AnalyticEvents() {return WebpackModules.getByProps("AnalyticEventConfigs");},
get KeyGenerator() {return WebpackModules.getByRegex(/"binary"/);},
get Buffers() {return WebpackModules.getByProps("INSPECT_MAX_BYTES", "kMaxLength");},
get DeviceStore() {return WebpackModules.getByProps("getDevices");},
get SoftwareInfo() {return WebpackModules.getByProps("os");},
get CurrentContext() {return WebpackModules.getByProps("setTagsContext");},
/* Commonly Used Classes */
get GuildClasses() {
const guildsWrapper = WebpackModules.getByProps("base", "guilds");
const guilds = WebpackModules.getByProps("wrapper", "acronym");
const pill = WebpackModules.getByProps("circleIconButton");
const listItem = WebpackModules.getModule(m => m.listItem && !m.pill && !m.sidebar);
return Object.assign({}, guildsWrapper, guilds, pill, listItem);
},
get LayerStack() {return WebpackModules.getByProps("pushLayer");},
get Tooltip() {
// Make fallback component just pass children, so it can at least render that.
const fallback = props => props.children?.({}) ?? null;

View File

@ -73,12 +73,16 @@ export default new class SettingsManager {
if (!this.state[collection.id]) this.state[collection.id] = {};
for (let cc = 0; cc < categories.length; cc++) {
const category = categories[cc];
if (category.type != "category") {if (!this.state[collection.id].hasOwnProperty(category.id)) this.state[collection.id][category.id] = category.value;}
if (category.type != "category") {
if (!this.state[collection.id].hasOwnProperty(category.id)) this.state[collection.id][category.id] = category.value;
category.defaultValue = category.value;
}
else {
if (!this.state[collection.id].hasOwnProperty(category.id)) this.state[collection.id][category.id] = {};
for (let s = 0; s < category.settings.length; s++) {
const setting = category.settings[s];
if (!this.state[collection.id][category.id].hasOwnProperty(setting.id)) this.state[collection.id][category.id][setting.id] = setting.value;
setting.defaultValue = setting.value;
if (setting.hasOwnProperty("disabled")) continue;
if (!setting.enableWith && !setting.disableWith) continue;
const pathString = setting.enableWith || setting.disableWith;
@ -131,6 +135,28 @@ export default new class SettingsManager {
this.saveCollection(id); // in case new things were added
}
resetCollection(id) {
const collection = this.collections.find(c => c.id == id);
if (!collection) return;
const categories = collection.settings;
for (let cc = 0; cc < categories.length; cc++) {
const category = categories[cc];
if (category.type != "category") {
// console.log("cat", collection.id, category.id, this.get(collection.id, category.id), category.value);
if (this.get(collection.id, category.id) == category.defaultValue) continue;
this.set(collection.id, category.id, category.defaultValue);
}
else {
for (let s = 0; s < category.settings.length; s++) {
const setting = category.settings[s];
// console.log("setting", collection.id, category.id, setting.id, this.get(collection.id, category.id, setting.id), setting.defaultValue);
if (this.get(collection.id, category.id, setting.id) == setting.defaultValue) continue;
this.set(collection.id, category.id, setting.id, setting.defaultValue);
}
}
}
}
onSettingChange(collection, category, id, value) {
this.state[collection][category][id] = value;
Events.dispatch("setting-updated", collection, category, id, value);

View File

@ -87,7 +87,7 @@ export class CoreUpdater {
const data = await resp.json();
this.apiData = data;
const remoteVersion = data.tag_name.startsWith("v") ? data.tag_name.slice(1) : data.tag_name;
this.hasUpdate = remoteVersion > Config.version;
this.hasUpdate = semverComparator(Config.version, remoteVersion) > 0;
this.remoteVersion = remoteVersion;
if (!this.hasUpdate || !showNotice) return;

View File

@ -524,7 +524,7 @@ export default class WebpackModules {
catch (error) {
Logger.stacktrace("WebpackModules", "Could not patch pushed module", error);
}
finally{
finally {
require.m[moduleId] = originalModule;
}
};

View File

@ -1,50 +0,0 @@
// import Selector from "./selector";
/**
* Representation of a Class Name
**/
class ClassName {
/**
*
* @param {string} name - name of the class to represent
*/
constructor(name) {
this.value = name;
}
/**
* Concatenates new class names to the current one using spaces.
* @param {string} classNames - list of class names to add to this class name
* @returns {ClassName} returns self to allow chaining
*/
add(...classNames) {
for (let i = 0; i < classNames.length; i++) this.value += " " + classNames[i];
return this;
}
/**
* Returns the raw class name, this is how native function get the value.
* @returns {string} raw class name.
*/
toString() {
return this.value;
}
/**
* Returns the raw class name, this is how native function get the value.
* @returns {string} raw class name.
*/
valueOf() {
return this.value;
}
get single() {
return this.value.split(" ")[0];
}
get first() {
return this.value.split(" ")[0];
}
}
export default ClassName;

View File

@ -3,9 +3,9 @@ import Utilities from "@modules/utilities";
export default class SimpleMarkdownExt {
static parseToReact(str) {
static parseToReact(str, inline = true) {
if (!this._parser) this._initialize();
return this._renderer(this._parse(str, {inline: true}));
return this._renderer(this._parse(str, {inline}));
}
static _initialize() {

View File

@ -55,19 +55,8 @@
align-items: center;
}
.controls-section .checkbox-inner {
width: 14px;
height: 14px;
}
.controls-section .checkbox-inner .checkbox:checked + span::after {
left: 2px;
top: -2px;
}
.controls-section .checkbox-label {
font-size: 14px;
font-weight: 500;
.controls-section.controls-right {
gap: 15px;
}
.monaco-editor:not(.rename-box),

View File

@ -1,4 +1,4 @@
.bd-button {
/* .bd-button {
display: inline-flex;
justify-content: center;
align-items: center;
@ -59,4 +59,542 @@
.bd-button-disabled:hover {
cursor: not-allowed;
}
} */
/* Generic Button Styles */
.bd-button {
position: relative;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
background: none;
border: none;
border-radius: 3px;
font-size: 14px;
font-weight: 500;
line-height: 16px;
padding: 2px 16px;
user-select: none;
}
.bd-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-button .bd-button-content {
--button--underline-color: transparent;
background-image: linear-gradient(0deg, transparent, transparent 1px, var(--button--underline-color) 0, var(--button--underline-color) 2px, transparent 0);
display: flex;
justify-content: center;
align-items: center;
}
.bd-button:disabled .bd-button-content {
background-image: none !important;
}
.bd-button-outlined:disabled {
background-color: transparent !important;
}
/* Button Sizes */
.bd-button-tiny {
width: 52px;
height: 24px;
min-width: 52px;
min-height: 24px;
}
.bd-button-small {
width: 60px;
height: 32px;
min-width: 60px;
min-height: 32px;
}
.bd-button-medium {
width: 96px;
height: 38px;
min-width: 96px;
min-height: 38px;
}
.bd-button-large {
width: 130px;
height: 44px;
min-width: 130px;
min-height: 44px;
}
.bd-button-xlarge {
width: 148px;
height: 50px;
min-width: 148px;
min-height: 50px;
font-size: 16px;
line-height: normal;
padding: 2px 20px;
}
.bd-button-icon {
height: auto;
padding: 4px;
}
.bd-button-grow,
.bd-button-icon {
width: auto
}
/* Button Looks */
.bd-button-filled {
-webkit-transition: background-color .17s ease, color .17s ease;
transition: background-color .17s ease, color .17s ease
}
.bd-button-outlined {
-webkit-transition: color .17s ease, background-color .17s ease, border-color .17s ease;
transition: color .17s ease, background-color .17s ease, border-color .17s ease;
border-width: 1px;
border-style: solid
}
.bd-button-blank {
background: transparent;
color: currentColor;
border: 0;
/* padding: 0; */
margin: 0
}
.bd-button-filled .bd-button-content,
.bd-button-link .bd-button-content,
.bd-button-outlined .bd-button-content {
margin: 0 auto;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
/* COLORS */
/* Color BD Brand */
.bd-button-filled.bd-button-color-brand {
color: var(--white-500);
background-color: #3E82E5; /* BD Blue */
}
.bd-button-filled.bd-button-color-brand:hover {
background-color: #3875CE;
}
.bd-button-filled.bd-button-color-brand:active {
background-color: #3268B7;
}
.bd-button-filled.bd-button-color-brand:disabled {
background-color: #3E82E5; /* BD Blue */
opacity: 0.4;
}
.bd-button-outlined.bd-button-color-brand {
color: var(--button-outline-brand-text);
border-color: var(--button-outline-brand-border);
}
.bd-button-outlined.bd-button-color-brand:hover {
background-color: var(--button-outline-brand-background-hover);
border-color: var(--button-outline-brand-border-hover);
color: var(--button-outline-brand-text-hover);
}
.bd-button-outlined.bd-button-color-brand:active {
background-color: var(--button-outline-brand-background-active);
border-color: var(--button-outline-brand-border-active);
color: var(--button-outline-brand-text-active);
}
.bd-button-link.bd-button-color-brand {
color: #3E82E5; /* BD Blue */
}
.bd-button-link.bd-button-color-brand:hover .bd-button-content {
--button--underline-color: #3E82E5; /* BD Blue */
}
/* Color Blurple */
.bd-button-filled.bd-button-color-blurple {
color: var(--white-500);
background-color: var(--brand-experiment)
}
.bd-button-filled.bd-button-color-blurple:hover {
background-color: var(--brand-experiment-560)
}
.bd-button-filled.bd-button-color-blurple:active {
background-color: var(--brand-experiment-600)
}
.bd-button-filled.bd-button-color-blurple:disabled {
background-color: var(--brand-experiment)
}
.bd-button-outlined.bd-button-color-blurple {
color: var(--button-outline-brand-text);
border-color: var(--button-outline-brand-border)
}
.bd-button-outlined.bd-button-color-blurple:hover {
background-color: var(--button-outline-brand-background-hover);
border-color: var(--button-outline-brand-border-hover);
color: var(--button-outline-brand-text-hover)
}
.bd-button-outlined.bd-button-color-blurple:active {
background-color: var(--button-outline-brand-background-active);
border-color: var(--button-outline-brand-border-active);
color: var(--button-outline-brand-text-active)
}
.bd-button-link.bd-button-color-blurple {
color: var(--brand-experiment)
}
.bd-button-link.bd-button-color-blurple:hover .bd-button-content {
--button--underline-color: var(--brand-experiment)
}
/* Color Yellow/Warn */
.bd-button-filled.bd-button-color-yellow {
color: var(--white-500);
background-color: var(--status-warning)
}
.bd-button-filled.bd-button-color-yellow:active,
.bd-button-filled.bd-button-color-yellow:hover {
background-color: null;
}
.bd-button-filled.bd-button-color-yellow:disabled {
background-color: var(--status-warning)
}
.bd-button-outlined.bd-button-color-yellow {
color: var(--status-warning);
border-color: var(--status-warning)
}
.bd-button-outlined.bd-button-color-yellow:active {
background-color: hsl(var(--yellow-300-hsl)/.1)
}
.bd-button-link.bd-button-color-yellow {
color: var(--status-warning)
}
.bd-button-link.bd-button-color-yellow:hover .bd-button-content {
--button--underline-color: var(--status-warning)
}
/* Color Link */
.bd-button-filled.bd-button-color-link {
color: var(--white-500);
background-color: var(--text-link)
}
.bd-button-filled.bd-button-color-link:active,
.bd-button-filled.bd-button-color-link:hover {
background-color: null
}
.bd-button-filled.bd-button-color-link:disabled {
background-color: var(--text-link)
}
.bd-button-outlined.bd-button-color-link {
color: var(--text-link);
border-color: var(--text-link)
}
.bd-button-outlined.bd-button-color-link:active {
background-color: hsl(var(--text-link-hsl)/.1)
}
.bd-button-link.bd-button-color-link {
color: var(--text-link)
}
.bd-button-link.bd-button-color-link:hover .bd-button-content {
--button--underline-color: var(--text-link)
}
/* Color White */
.bd-button-filled.bd-button-color-white {
color: var(--primary-500);
background-color: var(--white-500)
}
.bd-button-filled.bd-button-color-white:active,
.bd-button-filled.bd-button-color-white:hover {
background-color: null
}
.bd-button-filled.bd-button-color-white:disabled {
background-color: var(--white-500)
}
.bd-button-outlined.bd-button-color-white {
color: var(--white-500);
border-color: var(--white-500)
}
.bd-button-outlined.bd-button-color-white:active {
background-color: hsl(var(--white-500-hsl)/.1)
}
.bd-button-link.bd-button-color-white {
color: var(--white-500)
}
.bd-button-link.bd-button-color-white:hover .bd-button-content {
--button--underline-color: var(--white-500)
}
/* Color Red/Danger/Error */
.bd-button-filled.bd-button-color-red {
color: var(--white-500);
background-color: var(--button-danger-background)
}
.bd-button-filled.bd-button-color-red:hover {
background-color: var(--button-danger-background-hover)
}
.bd-button-filled.bd-button-color-red:active {
background-color: var(--button-danger-background-active)
}
.bd-button-filled.bd-button-color-red:disabled {
background-color: var(--button-danger-background-disabled)
}
.bd-button-outlined.bd-button-color-red {
color: var(--button-outline-danger-text);
border-color: var(--button-outline-danger-border)
}
.bd-button-outlined.bd-button-color-red:hover {
background-color: var(--button-outline-danger-background-hover);
border-color: var(--button-outline-danger-border-hover);
color: var(--button-outline-danger-text-hover)
}
.bd-button-outlined.bd-button-color-red:active {
background-color: var(--button-outline-danger-background-active);
border-color: var(--button-outline-danger-border-active);
color: var(--button-outline-danger-text-active)
}
.bd-button-link.bd-button-color-red {
color: var(--text-danger)
}
.bd-button-link.bd-button-color-red:hover .bd-button-content {
--button--underline-color: var(--text-danger)
}
/* Color Green Success */
.bd-button-filled.bd-button-color-green {
color: var(--white-500);
background-color: var(--button-positive-background)
}
.bd-button-filled.bd-button-color-green:hover {
background-color: var(--button-positive-background-hover)
}
.bd-button-filled.bd-button-color-green:active {
background-color: var(--button-positive-background-active)
}
.bd-button-filled.bd-button-color-green:disabled {
background-color: var(--button-positive-background-disabled)
}
.bd-button-outlined.bd-button-color-green {
color: var(--button-outline-positive-text);
border-color: var(--button-outline-positive-border)
}
.bd-button-outlined.bd-button-color-green:hover {
background-color: var(--button-outline-positive-background-hover);
border-color: var(--button-outline-positive-border-hover);
color: var(--button-outline-positive-text-hover)
}
.bd-button-outlined.bd-button-color-green:active {
background-color: var(--button-outline-positive-background-active);
border-color: var(--button-outline-positive-border-active);
color: var(--button-outline-positive-text-active)
}
.bd-button-link.bd-button-color-green {
color: var(--green-360)
}
.bd-button-link.bd-button-color-green:hover .bd-button-content {
--button--underline-color: var(--green-360)
}
/* Color Primary/Grey */
.bd-button-outlined.bd-button-color-primary {
color: var(--button-outline-primary-text);
border-color: var(--button-outline-primary-border)
}
.bd-button-outlined.bd-button-color-primary:hover {
background-color: var(--button-outline-primary-background-hover);
border-color: var(--button-outline-primary-border-hover);
color: var(--button-outline-primary-text-hover)
}
.bd-button-outlined.bd-button-color-primary:active {
background-color: var(--button-outline-primary-background-active);
border-color: var(--button-outline-primary-border-active);
color: var(--button-outline-primary-text-active)
}
.bd-button-filled.bd-button-color-primary {
color: var(--white-500);
background-color: var(--button-secondary-background)
}
.bd-button-filled.bd-button-color-primary:hover {
background-color: var(--button-secondary-background-hover)
}
.bd-button-filled.bd-button-color-primary:active {
background-color: var(--button-secondary-background-active)
}
.bd-button-filled.bd-button-color-primary:disabled {
background-color: var(--button-secondary-background-disabled)
}
.theme-dark .bd-button-link.bd-button-color-primary {
color: var(--white-500)
}
.theme-dark .bd-button-link.bd-button-color-primary:hover .bd-button-content {
--button--underline-color: var(--white-500)
}
.theme-light .bd-button-link.bd-button-color-primary {
color: var(--primary-400)
}
.theme-light .bd-button-link.bd-button-color-primary:hover .bd-button-content {
--button--underline-color: var(--primary-400)
}
/* Color Transparent */
.theme-dark .bd-button-filled.bd-button-color-transparent {
color: var(--primary-100);
background-color: hsl(var(--white-500-hsl)/.1)
}
.theme-dark .bd-button-filled.bd-button-color-transparent:hover {
background-color: hsl(var(--white-500-hsl)/.05)
}
.theme-dark .bd-button-filled.bd-button-color-transparent:active {
background-color: hsl(var(--white-500-hsl)/.01)
}
.theme-dark .bd-button-filled.bd-button-color-transparent:disabled {
background-color: hsl(var(--white-500-hsl)/.1)
}
.theme-dark .bd-button-outlined.bd-button-color-transparent {
color: var(--primary-200);
border-color: var(--primary-200)
}
.theme-dark .bd-button-outlined.bd-button-color-transparent:active {
background-color: hsl(var(--primary-200-hsl)/.1)
}
.theme-dark .bd-button-link.bd-button-color-transparent {
color: var(--primary-200)
}
.theme-dark .bd-button-link.bd-button-color-transparent:hover .bd-button-content {
--button--underline-color: var(--primary-200)
}
.theme-light .bd-button-filled.bd-button-color-transparent {
color: var(--primary-400);
background-color: hsl(var(--primary-400-hsl)/.01)
}
.theme-light .bd-button-filled.bd-button-color-transparent:hover {
background-color: hsl(var(--primary-400-hsl)/.2)
}
.theme-light .bd-button-filled.bd-button-color-transparent:active {
background-color: hsl(var(--primary-400-hsl)/.25)
}
.theme-light .bd-button-filled.bd-button-color-transparent:disabled {
background-color: hsl(var(--primary-400-hsl)/.01)
}
.theme-light .bd-button-outlined.bd-button-color-transparent {
color: var(--primary-400);
border-color: var(--primary-400)
}
.theme-light .bd-button-outlined.bd-button-color-transparent:active {
background-color: hsl(var(--primary-400-hsl)/.1)
}
.theme-light .bd-button-link.bd-button-color-transparent {
color: var(--primary-400)
}
.theme-light .bd-button-link.bd-button-color-transparent:hover .bd-button-content {
--button--underline-color: var(--primary-400)
}

View File

@ -6,6 +6,7 @@
@import "./buttons.css";
@import "./spinner.css";
@import "./search.css";
@import "./text.css";
.bd-chat-badge {
vertical-align: bottom;
@ -19,23 +20,6 @@
margin-left: 4px;
}
.bd-changelog-modal video,
.bd-changelog-modal img {
width: 100%;
border-radius: 5px;
outline: none;
}
.bd-changelog-modal code.inline {
padding: 0.2em;
margin: -0.2em 0;
border-radius: 3px;
font-size: 85%;
line-height: 1.125rem;
white-space: pre-wrap;
background: var(--background-secondary);
}
.bd-link {
text-decoration: none;
}

View File

@ -0,0 +1,86 @@
.bd-text-normal {
color: var(--text-normal);
}
.bd-text-muted {
color: var(--text-muted);
}
.bd-text-error {
color: var(--red-400);
}
.bd-text-brand {
color: var(--text-brand);
}
.bd-text-link {
color: var(--text-link);
}
.bd-header-primary {
color: var(--header-primary);
}
.bd-header-secondary {
color: var(--header-secondary);
}
.bd-text-yellow {
color: var(--text-warning);
}
.bd-text-green {
color: var(--text-positive);
}
.bd-text-red {
color: var(--status-danger);
}
.bd-text-white {
color: var(--white-500);
}
.bd-text-10 {
font-size: 10px;
line-height: 12px;
}
.bd-text-12 {
font-size: 12px;
line-height: 16px;
}
.bd-text-14 {
font-size: 14px;
line-height: 18px;
}
.bd-text-16 {
font-size: 16px;
line-height: 20px;
}
.bd-text-20 {
font-size: 20px;
line-height: 24px;
}
.bd-text-24 {
font-size: 24px;
line-height: 30px;
}
.bd-text-32 {
font-size: 32px;
line-height: 40px;
}
.bd-text-strong {
font-weight: 600;
}
.bd-selectable {
user-select: text;
}

View File

@ -17,6 +17,7 @@
}
.bd-controls,
.bd-controls-basic,
.bd-controls-advanced {
display: flex;
}
@ -27,7 +28,7 @@
.bd-addon-list.bd-grid-view {
display: grid;
grid-template-columns: auto auto;
grid-template-columns: 1fr 1fr;
column-gap: 10px;
row-gap: 10px;
}

View File

@ -168,6 +168,7 @@
display: flex;
justify-content: space-between;
color: var(--header-primary, #FFFFFF);
display: flex;
font-weight: 600;
cursor: default;
flex: 1;

View File

@ -0,0 +1,239 @@
.bd-changelog-modal iframe,
.bd-changelog-modal video,
.bd-changelog-modal img {
width: 100%;
border-radius: 5px;
outline: none;
}
.bd-changelog-modal code.inline {
padding: 0.2em;
margin: -0.2em 0;
border-radius: 3px;
font-size: 85%;
line-height: 1.125rem;
white-space: pre-wrap;
background: var(--background-secondary);
}
.bd-changelog-modal .bd-modal-content {
font-size: 16px;
line-height: 20px;
padding-bottom: 20px;
}
.bd-changelog-modal .bd-modal-content .emoji {
object-fit: contain;
width: 22px;
height: 22px;
}
.bd-changelog-modal .bd-modal-content h1 {
line-height: 20px;
font-size: 16px;
}
.bd-changelog-modal .bd-modal-content h1,
.bd-changelog-modal .bd-modal-content h2,
.bd-changelog-modal .bd-modal-content strong {
font-weight: 700;
}
.bd-changelog-modal .bd-modal-content em,
.bd-changelog-modal .bd-modal-content i {
font-style: italic;
}
.bd-changelog-modal .bd-modal-content p + p {
margin-top: 10px;
}
.bd-changelog-modal .bd-modal-content ol {
margin: 16px 0 16px 16px;
}
.bd-changelog-modal .bd-modal-content ol li {
list-style-type: decimal;
margin-bottom: 8px;
margin-left: 20px;
}
.bd-changelog-modal .bd-modal-content ul {
margin: 20px 0 8px 20px;
}
.bd-changelog-modal .bd-modal-content ul ul {
margin-top: 8px;
}
.bd-changelog-modal .bd-modal-content ul li {
position: relative;
list-style: none;
margin-bottom: 8px;
user-select: text;
}
.bd-changelog-modal .bd-modal-content ul li:last-child {
margin-bottom: 0;
}
.bd-changelog-modal .bd-modal-content ul li::before {
content: "";
position: absolute;
top: 10px;
left: -15px;
width: 6px;
height: 6px;
margin-top: -4px;
margin-left: -3px;
border-radius: 50%;
opacity: 0.3;
}
.bd-changelog-modal .bd-modal-content ul li li::before {
top: 12px;
height: 2px;
border-radius: 0;
}
.bd-changelog-modal .bd-modal-content img,
.bd-changelog-modal .bd-modal-content video,
.bd-changelog-modal .bd-modal-content iframe {
width: 100%;
}
.bd-changelog-modal .bd-modal-content iframe {
height: 346px;
}
.bd-changelog-modal .bd-modal-content a {
color: hsl(200, calc(var(--saturation-factor, 1) * 100%), 49.4%);
transition: 0.05s;
text-decoration: none;
}
.bd-changelog-modal .bd-modal-content a:hover {
text-decoration: underline;
}
.theme-dark .bd-changelog-modal .bd-modal-content ol,
.theme-dark .bd-changelog-modal .bd-modal-content p,
.theme-dark .bd-changelog-modal .bd-modal-content ul li {
color: hsl(210, calc(var(--saturation-factor, 1) * 9.3%), 78.8%);
}
.theme-dark .bd-changelog-modal .bd-modal-content ul li::before {
background-color: hsl(216, calc(var(--saturation-factor, 1) * 9.8%), 90%);
}
.theme-light .bd-changelog-modal .bd-modal-content ol,
.theme-light .bd-changelog-modal .bd-modal-content p,
.theme-light .bd-changelog-modal .bd-modal-content ul li {
color: hsl(223, calc(var(--saturation-factor, 1) * 5.8%), 52.9%);
}
.theme-light .bd-changelog-modal .bd-modal-content ul li::before {
background-color: hsl(223, calc(var(--saturation-factor, 1) * 5.8%), 52.9%);
}
.bd-changelog-title {
font-weight: 700;
font-size: 16px;
line-height: 20px;
text-transform: uppercase;
}
.bd-changelog-title {
display: flex;
align-items: center;
margin-top: 40px;
}
.bd-changelog-title.bd-changelog-first {
margin-top: 20px;
}
.bd-changelog-title::after {
content: "";
height: 1px;
flex: 1 1 auto;
margin-left: 4px;
opacity: .6;
}
.bd-changelog-added {
color: var(--text-positive);
}
.bd-changelog-added::after {
background-color: var(--info-positive-foreground);
}
.bd-changelog-fixed {
color: hsl(359, calc(var(--saturation-factor, 1)*87.3%), 59.8%);
}
.bd-changelog-fixed::after {
background-color: hsl(359, calc(var(--saturation-factor, 1)*87.3%), 59.8%);
}
.bd-changelog-progress {
color: var(--text-warning);
}
.bd-changelog-progress::after {
background-color: var(--info-warning-foreground);
}
.bd-changelog-improved {
color: hsl(235, calc(var(--saturation-factor, 1)*85.6%), 64.7%);
}
.bd-changelog-improved::after {
background-color: hsl(235, calc(var(--saturation-factor, 1)*85.6%), 64.7%);
}
.theme-dark .bd-changelog-improved {
color: hsl(235, calc(var(--saturation-factor, 1)*86.1%), 77.5%);
}
.theme-dark .bd-changelog-improved::after {
background-color: hsl(235, calc(var(--saturation-factor, 1)*86.1%), 77.5%);
}
.theme-dark .bd-changelog-modal iframe,
.theme-dark .bd-changelog-modal video,
.theme-dark .bd-changelog-modal img {
box-shadow: 0 2px 10px 0 hsl(var(0, calc(var(--saturation-factor, 1)*0%), 0%-hsl)/.2);
}
.theme-light .bd-changelog-modal iframe,
.theme-light .bd-changelog-modal video,
.theme-light .bd-changelog-modal img {
box-shadow: 0 2px 10px 0 hsl(var(0, calc(var(--saturation-factor, 1)*0%), 0%-hsl)/.1);
}
/* .socialLink-1qjJIk {
margin-right: 16px;
}
.theme-light .socialLink-1qjJIk {
color: hsl(228, calc(var(--saturation-factor, 1)*6%), 32.5%);
}
.theme-dark .socialLink-1qjJIk {
color: hsl(210, calc(var(--saturation-factor, 1)*9.3%), 78.8%);
} */
.bd-social {
opacity: 0.7;
transition: 0.17s ease opacity;
}
.bd-social:hover {
opacity: 1;
}
.bd-social + .bd-social {
margin-left: 10px;
}

View File

@ -78,4 +78,16 @@
scrollbar-color: var(--background-tertiary) var(--background-secondary);
background: var(--background-secondary);
border: 1px solid var(--background-tertiary);
}
.bd-addon-error-details {
display: flex;
flex-grow: 0;
justify-content: flex-start;
margin-top: 4px;
}
.bd-addon-error-details-icon {
margin-right: 4px;
color: var(--interactive-normal);
}

View File

@ -0,0 +1,124 @@
.bd-flex {
display: flex;
}
.bd-flex-align-start {
align-items: flex-start;
}
.bd-flex-align-end {
align-items: flex-end;
}
.bd-flex-align-center {
align-items: center;
}
.bd-flex-align-stretch {
align-items: stretch;
}
.bd-flex-align-baseline {
align-items: baseline
}
.bd-flex-justify-start {
justify-content: flex-start;
}
.bd-flex-justify-end {
justify-content: flex-end;
}
.bd-flex-justify-center {
justify-content: center;
}
.bd-flex-justify-around {
justify-content: space-around;
}
.bd-flex-justify-between {
justify-content: space-between;
}
.bd-flex-no-wrap {
flex-wrap: nowrap;
}
.bd-flex-wrap {
flex-wrap: wrap;
}
.bd-flex-wrap-reverse {
flex-wrap: wrap-reverse;
}
.bd-flex-horizontal {
flex-direction: row;
}
.bd-flex-reverse {
flex-direction: row-reverse;
}
.bd-flex-vertical {
flex-direction: column;
}
.spacer-2upayl {
flex: 1;
overflow: hidden;
}
.bd-flex-vertical {}
.bd-flex-horizontal {}
.bd-flex-reverse {}
.bd-flex-horizontal > .spacer-2upayl,
.bd-flex-reverse > .spacer-2upayl,
.bd-flex-vertical > .spacer-2upayl {
min-height: 1px;
}
.flexCenter-1Mwsxg {}
.bd-flex {}
.bd-flex-horizontal {}
.bd-flex-reverse {}
.bd-flex-horizontal > .bd-flex,
.bd-flex-horizontal > .bd-flex-child {
margin-left: 10px;
margin-right: 10px;
}
.bd-flex-horizontal > .bd-flex:first-child,
.bd-flex-horizontal > .bd-flex-child:first-child {
margin-left: 0;
}
.bd-flex-horizontal > .bd-flex:last-child,
.bd-flex-horizontal > .bd-flex-child:last-child {
margin-right: 0;
}
.bd-flex-reverse > .bd-flex,
.bd-flex-reverse > .bd-flex-child {
margin-left: 10px;
margin-right: 10px;
}
.bd-flex-reverse > .bd-flex:first-child,
.bd-flex-reverse > .bd-flex-child:first-child {
margin-right: 0;
}
.bd-flex-reverse > .bd-flex:last-child,
.bd-flex-reverse > .bd-flex-child:last-child {
margin-left: 0;
}

View File

@ -35,9 +35,15 @@
top: 3px;
display: flex;
align-items: center;
gap: 5px;
}
.bd-keybind-controls .bd-keybind-record {
padding: 4px 8px;
}
.bd-keybind-clear {
margin-left: 5px;
background: none!important;
opacity: 0.5;
padding-right: 4px!important;

View File

@ -71,3 +71,143 @@
@keyframes bd-modal-open {
from {transform: scale(0.7);}
}
.bd-modal-root {
display: flex;
flex-direction: column;
background-color: var(--modal-background);
border-radius: 4px;
margin: 0 auto;
pointer-events: all;
position: relative;
max-height: 100%;
}
.bd-close-button {
height: 26px;
padding: 4px;
transition: opacity 0.2s ease-in-out;
opacity: 0.5;
cursor: pointer;
border-radius: 3px;
color: var(--interactive-normal);
box-sizing: content-box;
}
.bd-close-button:hover {
opacity: 1;
color: var(--interactive-hover);
}
.bd-modal-small {
width: 440px;
max-height: 720px;
min-height: 200px;
}
.bd-modal-standard {
font-size: 13px;
white-space: pre-wrap;
word-wrap: break-word;
width: 490px;
max-height: 800px;
}
.bd-modal-medium {
width: 600px;
max-height: 800px;
min-height: 400px;
}
.bd-modal-large {
width: 800px;
max-height: 960px;
min-height: 400px;
}
.bd-modal-header,
.bd-modal-footer {
position: relative;
flex: 0 0 auto;
padding: 16px;
z-index: 1;
overflow-x: hidden;
}
.bd-modal-header {
border-radius: 4px 4px 0 0;
transition: box-shadow 0.1s ease-out;
word-wrap: break-word;
}
.bd-modal-footer {
border-radius: 0 0 5px 5px;
background-color: var(--modal-footer-background);
overflow: hidden;
box-shadow: inset 0 1px 0 hsl(var(--primary-630-hsl)/0.6);
}
.bd-modal-content {
position: relative;
z-index: 0;
border-radius: 5px 5px 0 0;
padding-left: 16px;
/* padding-right: 16px; */
overflow-x: hidden;
font-size: 16px;
line-height: 20px;
padding-bottom: 20px;
overflow: hidden scroll;
padding-right: 8px;
}
.bd-modal-backdrop {
position: fixed;
top: 0;
right: var(--devtools-sidebar-width,0);
bottom: 0;
left: 0;
-webkit-transform: translateZ(0);
transform: translateZ(0);
pointer-events: all;
}
#bd-modal-container {
position: absolute;
top: 0;
left: 0;
right: var(--devtools-sidebar-width,0);
bottom: 0;
background: none!important;
pointer-events: none;
z-index: 1002;
}
.bd-modal-layer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.bd-modal-layer {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
min-height: 0;
padding-top: 40px;
padding-bottom: 40px;
}

View File

@ -0,0 +1,34 @@
.bd-scroller-base {
position: relative;
box-sizing: border-box;
min-height: 0;
flex: 1 1 auto;
}
.bd-scroller-thin {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thin-thumb) var(--scrollbar-thin-track);
}
.bd-scroller-thin::-webkit-scrollbar-track {
border-color: var(--scrollbar-thin-track);
background-color: var(--scrollbar-thin-track);
border: 2px solid var(--scrollbar-thin-track);
}
.bd-scroller-thin::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.bd-scroller-thin::-webkit-scrollbar-corner {
background-color: transparent;
}
.bd-scroller-thin::-webkit-scrollbar-thumb {
background-clip: padding-box;
border: 2px solid transparent;
border-radius: 4px;
background-color: var(--scrollbar-thin-thumb);
min-height: 40px;
}

View File

@ -21,6 +21,7 @@
.bd-sidebar-header .bd-changelog-button {
height: 16px;
padding: 0;
}
.bd-sidebar-header .bd-icon {
@ -28,6 +29,6 @@
fill: var(--interactive-normal);
}
.bd-sidebar-header .bd-icon:hover {
.bd-sidebar-header .bd-changelog-button:hover .bd-icon {
fill: var(--interactive-hover);
}

View File

@ -5,6 +5,7 @@
justify-content: center;
align-items: center;
padding: 3px;
margin-right: 4px; /* Align with other icons */
}
.bd-empty-updates {
@ -20,4 +21,37 @@
.bd-empty-updates svg {
fill: #43B581;
margin-bottom: 20px;
}
.bd-button.bd-button-icon svg {
fill: var(--interactive-normal);
}
.bd-button.bd-button-icon:hover svg {
fill: var(--interactive-hover);
}
.bd-button.bd-button-icon.animate {
animation: 500ms linear infinite bd-update-spin;
}
.bd-button.bd-button-icon.no-animate {
animation: none !important;
}
.bd-settings-group-title .bd-button-icon {
margin-left: 3px;
margin-right: -8px;
}
@keyframes bd-update-spin {
0% {
transform: rotate(0);
}
50% {
transform: rotate(180deg)
}
100% {
transform: rotate(360deg)
}
}

View File

@ -0,0 +1,104 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
// S.Looks = y;
// S.Colors = I;
// S.BorderColors = O;
// S.Hovers = T;
// S.Sizes = v;
const {useCallback} = React;
export const Looks = Object.freeze({
FILLED: "bd-button-filled",
OUTLINED: "bd-button-outlined",
LINK: "bd-button-link",
BLANK: "bd-button-blank"
});
export const Colors = Object.freeze({
BRAND: "bd-button-color-brand",
BLURPLE: "bd-button-color-blurple",
RED: "bd-button-color-red",
GREEN: "bd-button-color-green",
YELLOW: "bd-button-color-yellow",
PRIMARY: "bd-button-color-primary",
LINK: "bd-button-color-link",
WHITE: "bd-button-color-white",
TRANSPARENT: "bd-button-color-transparent",
CUSTOM: ""
});
export const Sizes = Object.freeze({
NONE: "",
TINY: "bd-button-tiny",
SMALL: "bd-button-small",
MEDIUM: "bd-button-medium",
LARGE: "bd-button-large",
ICON: "bd-button-icon"
});
export default function Button({
className,
children,
onClick,
onKeyDown,
buttonRef,
disabled = false,
type = "button",
look = Looks.FILLED,
color = Colors.BRAND,
size = Sizes.MEDIUM,
grow = true,
...others
}) {
const handleClick = useCallback(event => {
event.preventDefault();
event.stopPropagation();
onClick?.(event);
}, [onClick]);
return <button {...others} className={
Utilities.className(
"bd-button",
className,
look,
color,
size,
grow ? "bd-button-grow" : ""
)}
ref={buttonRef}
type={type === "button" ? null : type}
onClick={disabled ? () => {} : handleClick}
onKeyDown={disabled ? () => {} : onKeyDown}
>
<div className="bd-button-content">{children}</div>
</button>;
}
Button.Looks = Looks;
Button.Colors = Colors;
Button.Sizes = Sizes;
// window.BDButton = Button;
// (() => {
// const buttons = [];
// for (const look in window.BDButton.Looks) {
// if (!window.BDButton.Looks[look] || look === "BLANK") continue;
// for (const color in window.BDButton.Colors) {
// if (!window.BDButton.Colors[color]) continue;
// for (const size in window.BDButton.Sizes) {
// if (!window.BDButton.Sizes[size]) continue;
// buttons.push(window.BdApi.React.createElement(window.BDButton, {
// look: window.BDButton.Looks[look],
// color: window.BDButton.Colors[color],
// size: window.BDButton.Sizes[size]
// }, "Hello World!"));
// buttons.push(window.BdApi.React.createElement("br"));
// }
// }
// }
// window.BdApi.showConfirmationModal("Buttons", buttons);
// })();

View File

@ -0,0 +1,76 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
export const Direction = Object.freeze({
VERTICAL: "bd-flex-vertical",
HORIZONTAL: "bd-flex-horizontal",
HORIZONTAL_REVERSE: "bd-flex-reverse"
});
export const Justify = Object.freeze({
START: "bd-flex-justify-start",
END: "bd-flex-justify-end",
CENTER: "bd-flex-justify-center",
BETWEEN: "bd-flex-justify-between",
AROUND: "bd-flex-justify-around"
});
export const Align = Object.freeze({
START: "bd-flex-align-start",
END: "bd-flex-align-end",
CENTER: "bd-flex-align-center",
STRETCH: "bd-flex-align-stretch",
BASELINE: "bd-flex-align-baseline"
});
export const Wrap = Object.freeze({
NO_WRAP: "bd-flex-no-wrap",
WRAP: "bd-flex-wrap",
WRAP_REVERSE: "bd-flex-wrap-reverse"
});
export function Child(props) {
if (!props.className) props.className = "";
props.className = Utilities.className(props.className, "bd-flex-child");
return <Flex {...props} />;
}
export default function Flex({
children,
className,
style,
shrink = 1,
grow = 1,
basis = "auto",
direction = Direction.HORIZONTAL,
align = Align.STRETCH,
justify = Justify.START,
wrap = Wrap.NO_WRAP
}) {
return <div
className={Utilities.className(
"bd-flex",
direction,
justify,
align,
wrap,
className
)}
style={Object.assign({
flexShrink: shrink,
flexGrow: grow,
flexBasis: basis
}, style)}
>
{children}
</div>;
}
Flex.Child = Child;
Flex.Direction = Direction;
Flex.Align = Align;
Flex.Justify = Justify;
Flex.Wrap = Wrap;

View File

@ -0,0 +1,39 @@
import React from "@modules/react";
import WebpackModules from "@modules/webpackmodules";
import DiscordModules from "@modules/discordmodules";
let DiscordMarkdown, rules;
function setupMarkdown() {
DiscordMarkdown = WebpackModules.find(m => m?.prototype?.render && m.rules);
rules = {};
if (DiscordMarkdown) {
rules = {
...DiscordMarkdown.rules,
link: DiscordModules.SimpleMarkdown.defaultRules.link
};
const originalLink = rules.link.react;
rules.link.react = function() {
const original = Reflect.apply(originalLink, undefined, arguments);
original.props.className = "bd-link";
original.props.target = "_blank";
original.props.rel = "noopener noreferrer";
return original;
};
}
}
export default function Markdown({className, children}) {
if (!DiscordMarkdown && !rules) setupMarkdown();
if (!DiscordMarkdown) return <div className="bd-markdown-fallback">{children}</div>;
return <DiscordMarkdown
className={className}
parser={DiscordModules.SimpleMarkdown.parserFor(rules)}
output={DiscordModules.SimpleMarkdown.reactFor(DiscordModules.SimpleMarkdown.ruleOutput(rules, "react"))}
>
{children}
</DiscordMarkdown>;
}

View File

@ -0,0 +1,55 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
export const Colors = Object.freeze({
STANDARD: "bd-text-normal",
MUTED: "bd-text-muted",
ERROR: "bd-text-error",
BRAND: "bd-text-brand",
LINK: "bd-text-link",
HEADER_PRIMARY: "bd-header-primary",
HEADER_SECONDARY: "bd-header-secondary",
STATUS_YELLOW: "bd-text-yellow",
STATUS_GREEN: "bd-text-green",
STATUS_RED: "bd-text-red",
ALWAYS_WHITE: "bd-text-white",
CUSTOM: null
});
export const Sizes = Object.freeze({
SIZE_10: "bd-text-10",
SIZE_12: "bd-text-12",
SIZE_14: "bd-text-14",
SIZE_16: "bd-text-16",
SIZE_20: "bd-text-20",
SIZE_24: "bd-text-24",
SIZE_32: "bd-text-32"
});
export default function Text({tag: Tag = "div", className, children, color = Colors.STANDARD, size = Sizes.SIZE_14, selectable, strong, style}) {
return <Tag
className={
Utilities.className(
color, size, className,
{
"bd-selectable": selectable,
"bd-text-strong": strong
}
)}
style={style}
>
{children}
</Tag>;
}
Text.Colors = Colors;
Text.Sizes = Sizes;
// te = WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors)
// foo = []
// for (const color in te.Colors) foo.push(BdApi.React.createElement(te, {color: te.Colors[color]}, color))
// for (const size in te.Sizes) foo.push(BdApi.React.createElement(te, {size: te.Sizes[size]}, size))
// BdApi.showConfirmationModal("Text Elements", foo)

View File

@ -1,13 +1,15 @@
import SimpleMarkdown from "@structs/markdown";
import React from "@modules/react";
import DiscordClasses from "@modules/discordclasses";
import WebpackModules from "@modules/webpackmodules";
const EmptyImageClasses = WebpackModules.getByProps("emptyImage", "emptyHeader") ?? {emptyContainer: "emptyContainer-poti7J", emptyImage: "emptyImage-2pCD2j", emptyHeader: "emptyHeader-2cxTFP"};
export default function EmptyImage(props) {
return <div className={`bd-empty-image-container ${DiscordClasses.EmptyImage.emptyContainer}` + (props.className ? ` ${props.className}` : "")}>
<div className={`bd-empty-image ${DiscordClasses.EmptyImage.emptyImage}`}></div>
<div className={`bd-empty-image-header ${DiscordClasses.EmptyImage.emptyHeader}`}>
return <div className={`bd-empty-image-container ${EmptyImageClasses.emptyContainer}` + (props.className ? ` ${props.className}` : "")}>
<div className={`bd-empty-image ${EmptyImageClasses.emptyImage}`}></div>
<div className={`bd-empty-image-header ${EmptyImageClasses.emptyHeader}`}>
{props.title || "You don't have anything!"}
</div>
<div className={`bd-empty-image-message`}>

View File

@ -1,23 +0,0 @@
import React from "@modules/react";
const {useState, useCallback} = React;
export default function Checkbox({checked: initialState, text, onChange: notifyParent}) {
const [checked, setChecked] = useState(initialState);
const onClick = useCallback(() => {
notifyParent?.(!checked);
setChecked(!checked);
}, [notifyParent, checked]);
return <div className="checkbox-item">
<div className="checkbox-label label-JWQiNe da-label">{text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={onClick}>
<div className="checkbox-inner checkboxInner-3yjcPe da-checkboxInner">
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={checked} type="checkbox" />
<span></span>
</div>
<span></span>
</div>
</div>;
}

View File

@ -61,7 +61,7 @@ export default forwardRef(function CssEditor({css, openNative, update, save, onC
{label: <Refresh size="18px" />, tooltip: Strings.CustomCSS.update, onClick: updateCss},
{label: <Save size="18px" />, tooltip: Strings.CustomCSS.save, onClick: saveCss},
{label: <Edit size="18px" />, tooltip: Strings.CustomCSS.openNative, onClick: popoutNative},
{label: Strings.Collections.settings.customcss.liveUpdate.name, type: "checkbox", onChange: toggleLiveUpdate, checked: Settings.get("settings", "customcss", "liveUpdate"), side: "right"},
{label: Strings.Collections.settings.customcss.liveUpdate.name, type: "boolean", onChange: toggleLiveUpdate, checked: Settings.get("settings", "customcss", "liveUpdate"), side: "right"},
openDetached && {label: <Detach size="18px" />, tooltip: Strings.CustomCSS.openDetached, onClick: popout, side: "right"}
].filter(c => c)}
value={css}

View File

@ -2,7 +2,10 @@ import React from "@modules/react";
import DiscordModules from "@modules/discordmodules";
import Settings from "@modules/settingsmanager";
import Checkbox from "./checkbox";
import Button from "../base/button";
import Flex from "../base/flex";
import Switch from "../settings/components/switch";
import Text from "@ui/base/text";
const {useState, useCallback, useEffect, forwardRef, useMemo, useImperativeHandle} = React;
const ThemeStore = DiscordModules.ThemeStore;
@ -13,17 +16,20 @@ const languages = ["abap", "abc", "actionscript", "ada", "apache_conf", "asciido
function makeButton(button, value) {
return <DiscordModules.Tooltip color="primary" position="top" text={button.tooltip}>
{props => {
return <button {...props} className="btn btn-primary" onClick={(event) => {button.onClick(event, value?.());}}>{button.label}</button>;
return <Button {...props} size={Button.Sizes.ICON} look={Button.Looks.BLANK} onClick={(event) => {button.onClick(event, value?.());}}>{button.label}</Button>;
}}
</DiscordModules.Tooltip>;
}
function makeCheckbox(checkbox) {
return <Checkbox text={checkbox.label} onChange={checkbox.onChange} checked={checkbox.checked} />;
// <Switch disabled={disabled} checked={isEnabled} onChange={onChange} />
function makeSwitch(control) {
return <Flex align={Flex.Align.CENTER} style={{gap: "10px"}}>
<Text>{control.label}</Text>
<Switch onChange={control.onChange} checked={control.checked} />
</Flex>;
}
function buildControl(value, control) {
if (control.type == "checkbox") return makeCheckbox(control);
if (control.type == "boolean") return makeSwitch(control);
return makeButton(control, value);
}

View File

@ -0,0 +1,9 @@
import React from "@modules/react";
export default function Download(props) {
const size = props.size || "24px";
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>;
}

View File

@ -1,6 +1,6 @@
import React from "@modules/react";
export default function FullScreen(props) {
export default function Folder(props) {
const size = props.size || "20px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>

View File

@ -0,0 +1,9 @@
import React from "@modules/react";
export default function Restore(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z"/>
</svg>;
}

View File

@ -0,0 +1,9 @@
import React from "@modules/react";
export default function Sync(props) {
const size = props.size || "24px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
</svg>;
}

View File

@ -1,8 +0,0 @@
import React from "@modules/react";
export default function Twitch(props) {
const size = props.size || "18px";
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" style={{width: size, height: size}}>
<path fill="currentColor" d="M40.1 32L10 108.9v314.3h107V480h60.2l56.8-56.8h87l117-117V32H40.1zm357.8 254.1L331 353H224l-56.8 56.8V353H76.9V72.1h321v214zM331 149v116.9h-40.1V149H331zm-107 0v116.9h-40.1V149H224z"/>
</svg>;
}

View File

@ -0,0 +1,8 @@
import React from "@modules/react";
export default function Twitter(props) {
const size = props.size || "18px";
return <svg xmlns="http://www.w3.org/2000/svg" fill="#26a7de" viewBox="0 0 24 24" style={{width: size, height: size}}>
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>;
}

View File

@ -7,37 +7,38 @@ import React from "@modules/react";
import ReactDOM from "@modules/reactdom";
import Strings from "@modules/strings";
import Settings from "@modules/settingsmanager";
import DiscordModules from "@modules/discordmodules";
import Events from "@modules/emitter";
// import DiscordModules from "@modules/discordmodules";
import WebpackModules from "@modules/webpackmodules";
import DiscordClasses from "@modules/discordclasses";
import DOMManager from "@modules/dommanager";
import AddonErrorModal from "./addonerrormodal";
import AddonErrorModal from "./modals/addonerrormodal";
import ErrorBoundary from "./errorboundary";
import TextElement from "./base/text";
import ModalRoot from "./modals/root";
// import ModalHeader from "./modals/header";
// import ModalContent from "./modals/content";
// import ModalFooter from "./modals/footer";
import ConfirmationModal from "./modals/confirmation";
// import Button from "./base/button";
import CustomMarkdown from "./base/markdown";
import ChangelogModal from "./modals/changelog";
import ModalStack, {generateKey} from "./modals/stack";
export default class Modals {
static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");}
static get ModalActions() {
return this._ModalActions ??= WebpackModules.getByProps("openModal", "closeModal");
}
static get ModalStack() {return this._ModalStack ??= WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get ModalComponents() {return this._ModalComponents ??= WebpackModules.getByProps("Header", "Footer");}
static get ModalRoot() {return this._ModalRoot ??= WebpackModules.getModule(m => m?.toString?.()?.includes("ENTERING") && m?.toString?.()?.includes("headerId"), {searchExports: true});}
static get ModalClasses() {return this._ModalClasses ??= WebpackModules.getByProps("modal", "content");}
static get FlexElements() {return this._FlexElements ??= WebpackModules.getByProps("Child", "Align");}
static get TextElement() {return this._TextElement ??= WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);}
static get ConfirmationModal() {return this._ConfirmationModal ??= WebpackModules.getByProps("ConfirmModal").ConfirmModal;}
static get Markdown() {return this._Markdown ??= WebpackModules.find(m => m?.prototype?.render && m.rules);}
static get Buttons() {return this._Buttons ??= WebpackModules.getModule(m => m.BorderColors, {searchExports: true});}
static get ModalQueue() {return this._ModalQueue ??= [];}
static get hasModalOpen() {return !!document.getElementsByClassName("bd-modal").length;}
static get ModalActions() {
return this._ModalActions ??= WebpackModules.getByProps("openModal", "closeModal", "updateModal");
}
static get ModalQueue() {return this._ModalQueue ??= [];}
static async initialize() {
const names = ["ConfirmationModal", "ModalActions", "Markdown", "ModalRoot", "ModalComponents", "Buttons", "TextElement", "FlexElements"];
const names = ["ModalActions"];
for (const name of names) {
let value = this[name];
@ -162,8 +163,6 @@ export default class Modals {
* @returns {string} - the key used for this modal
*/
static showConfirmationModal(title, content, options = {}) {
const Markdown = this.Markdown;
const ConfirmationModal = this.ConfirmationModal;
const ModalActions = this.ModalActions;
if (content instanceof FormattableString) content = content.toString();
@ -171,7 +170,7 @@ export default class Modals {
const emptyFunction = () => {};
const {onClose = emptyFunction, onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options;
if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) {
if (!this.ModalActions) {
return this.default(title, content, [
confirmText && {label: confirmText, action: onConfirm},
cancelText && {label: cancelText, action: onCancel, danger}
@ -179,9 +178,9 @@ export default class Modals {
}
if (!Array.isArray(content)) content = [content];
content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c);
content = content.map(c => typeof(c) === "string" ? React.createElement(CustomMarkdown, null, c) : c);
const modalKey = ModalActions.openModal(props => {
const modalKey = this.openModal(props => {
return React.createElement(ErrorBoundary, {
onError: () => {
setTimeout(() => {
@ -194,13 +193,13 @@ export default class Modals {
}
}, React.createElement(ConfirmationModal, Object.assign({
header: title,
confirmButtonColor: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND,
danger: danger,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel,
onCloseCallback: () => {
if (props?.transitionState === 1) onClose?.();
if (props?.transitionState === 2) onClose?.();
}
}, props), React.createElement(ErrorBoundary, {}, content)));
}, {modalKey: key});
@ -210,87 +209,22 @@ export default class Modals {
static showAddonErrors({plugins: pluginErrors = [], themes: themeErrors = []}) {
if (!pluginErrors || !themeErrors || !this.shouldShowAddonErrors) return;
if (!pluginErrors.length && !themeErrors.length) return;
if (this.addonErrorsRef && this.addonErrorsRef.current) {
return this.addonErrorsRef.current.refreshTabs(Array.isArray(pluginErrors) ? pluginErrors : [], Array.isArray(themeErrors) ? themeErrors : []);
}
this.addonErrorsRef = React.createRef();
this.ModalActions.openModal(props => React.createElement(ErrorBoundary, null, React.createElement(this.ModalRoot, Object.assign(props, {
size: "medium",
className: "bd-error-modal",
children: [
React.createElement(AddonErrorModal, {
ref: this.addonErrorsRef,
pluginErrors: Array.isArray(pluginErrors) ? pluginErrors : [],
themeErrors: Array.isArray(themeErrors) ? themeErrors : [],
onClose: props.onClose
}),
React.createElement(this.ModalComponents.Footer, {
className: "bd-error-modal-footer",
}, React.createElement(this.Buttons, {
onClick: props.onClose,
className: "bd-button"
}, Strings.Modals.okay))
]
}))));
const options = {
ref: this.addonErrorsRef,
pluginErrors: Array.isArray(pluginErrors) ? pluginErrors : [],
themeErrors: Array.isArray(themeErrors) ? themeErrors : []
};
this.openModal(props => {
return React.createElement(ErrorBoundary, null, React.createElement(AddonErrorModal, Object.assign(options, props)));
});
}
static showChangelogModal(options = {}) {
const OriginalModalClasses = WebpackModules.getByProps("hideOnFullscreen", "root");
const ChangelogModalClasses = WebpackModules.getModule(m => typeof(m) === "object" && Object.keys(m).length === 2 && m.modal && m.content);
const ChangelogClasses = WebpackModules.getByProps("fixed", "improved") ?? {added: "added_dc4118 title_cb085d", container: "container_bd6694", date: "date__21306", fixed: "fixed__61d41 title_cb085d", footer: "footer__55d42", image: "image__988dc", improved: "improved_df526b title_cb085d", lead: "lead__8e4f4", marginTop: "marginTop__89620", premiumBanner: "premiumBanner__6597f", premiumIcon: "premiumIcon__9fe69", progress: "progress_be0b9b title_cb085d", socialLink: "socialLink_f11d71", title: "title_cb085d", video: "video__562f3"};
const TextElement = this.TextElement;
const FlexChild = this.FlexElements;
const MarkdownParser = WebpackModules.getByProps("defaultRules", "parse");
options = Object.assign({image: "https://i.imgur.com/wuh5yMK.png", description: "", changes: [], title: "BetterDiscord", subtitle: `v${Config.version}`}, options);
if (!OriginalModalClasses || !ChangelogModalClasses || !ChangelogClasses || !TextElement || !FlexChild || !MarkdownParser) return Logger.warn("Modals", "showChangelogModal missing modules");
const {image = "https://i.imgur.com/wuh5yMK.png", description = "", changes = [], title = "BetterDiscord", subtitle = `v${Config.version}`, footer} = options;
const ce = React.createElement;
const changelogItems = [options.video ? ce("video", {src: options.video, poster: options.poster, controls: true, className: ChangelogClasses.video}) : ce("img", {src: image})];
if (description) changelogItems.push(ce("p", null, MarkdownParser.parse(description)));
for (let c = 0; c < changes.length; c++) {
const entry = changes[c];
const type = ChangelogClasses[entry.type] ? ChangelogClasses[entry.type] : ChangelogClasses.added;
const margin = c == 0 ? ChangelogClasses.marginTop : "";
changelogItems.push(ce("h1", {className: `${type} ${margin}`,}, entry.title));
if (entry.description) changelogItems.push(ce("p", null, MarkdownParser.parse(entry.description)));
const list = ce("ul", null, entry.items.map(i => ce("li", null, MarkdownParser.parse(i))));
changelogItems.push(list);
}
const renderHeader = function() {
return ce(FlexChild, {className: OriginalModalClasses.header, grow: 0, shrink: 0, direction: FlexChild.Direction.VERTICAL},
ce(TextElement, {tag: "h1", size: TextElement.Sizes.SIZE_20, strong: true}, title),
ce(TextElement, {size: TextElement.Sizes.SIZE_12, color: TextElement.Colors.STANDARD, className: ChangelogClasses.date}, subtitle)
);
};
const renderFooter = () => {
const AnchorClasses = WebpackModules.getByProps("anchorUnderlineOnHover") || {anchor: "anchor-3Z-8Bb", anchorUnderlineOnHover: "anchorUnderlineOnHover-2ESHQB"};
const joinSupportServer = (click) => {
click.preventDefault();
click.stopPropagation();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel({inviteKey: "0Tmfo5ZbORCRqbAd"});
};
const supportLink = ce("a", {className: `${AnchorClasses.anchor} ${AnchorClasses.anchorUnderlineOnHover}`, onClick: joinSupportServer}, "Join our Discord Server.");
const defaultFooter = ce(TextElement, {size: TextElement.Sizes.SIZE_12, color: TextElement.Colors.STANDARD}, "Need support? ", supportLink);
return ce(FlexChild, {className: OriginalModalClasses.footer + " " + OriginalModalClasses.footerSeparator},
ce(FlexChild.Child, {grow: 1, shrink: 1}, footer ? footer : defaultFooter)
);
};
const body = ce("div", {
className: `${OriginalModalClasses.content} ${ChangelogClasses.container} ${ChangelogModalClasses.content} ${DiscordClasses.Scrollers.thin}`
}, changelogItems);
const key = this.ModalActions.openModal(props => {
return React.createElement(ErrorBoundary, null, React.createElement(this.ModalRoot, Object.assign({
className: `bd-changelog-modal ${OriginalModalClasses.root} ${OriginalModalClasses.small} ${ChangelogModalClasses.modal}`,
selectable: true,
onScroll: _ => _,
onClose: _ => _,
}, props), renderHeader(), body, renderFooter()));
const key = this.openModal(props => {
return React.createElement(ErrorBoundary, null, React.createElement(ChangelogModal, Object.assign(options, props)));
});
return key;
}
@ -316,7 +250,7 @@ export default class Modals {
}
render() {
if (this.state.hasError) return null;
if (this.state.hasError) return React.createElement(TextElement, {color: TextElement.Colors.STATUS_RED}, Strings.Addons.settingsError);
const props = {
className: "bd-addon-settings-wrap",
ref: this.elementRef
@ -328,23 +262,36 @@ export default class Modals {
}
if (typeof(child) === "function") child = React.createElement(child);
const mc = this.ModalComponents;
const modal = props => {
return React.createElement(ErrorBoundary, {}, React.createElement(this.ModalRoot, Object.assign({size: mc.Sizes.MEDIUM, className: "bd-addon-modal" + " " + mc.Sizes.MEDIUM}, props),
React.createElement(mc.Header, {separator: false, className: "bd-addon-modal-header"},
React.createElement(this.TextElement, {tag: "h1", size: this.TextElement.Sizes.SIZE_20, strong: true}, `${name} Settings`)
),
React.createElement(mc.Content, {className: "bd-addon-modal-settings"},
React.createElement(ErrorBoundary, {}, child)
),
React.createElement(mc.Footer, {className: "bd-addon-modal-footer"},
React.createElement(this.Buttons, {onClick: props.onClose, className: "bd-button"}, Strings.Modals.done)
)
));
const options = {
className: "bd-addon-modal",
size: ModalRoot.Sizes.MEDIUM,
header: `${name} Settings`,
cancelText: null,
confirmText: Strings.Modals.done
};
return this.ModalActions.openModal(props => {
return React.createElement(ErrorBoundary, null, React.createElement(modal, props));
return this.openModal(props => {
return React.createElement(ErrorBoundary, null, React.createElement(ConfirmationModal, Object.assign(options, props), child));
});
}
static makeStack() {
const div = DOMManager.parseHTML(`<div id="bd-modal-container">`);
DOMManager.bdBody.append(div);
ReactDOM.render(<ModalStack />, div);
this.hasInitialized = true;
}
static openModal(render, options = {}) {
if (typeof(this.ModalActions.openModal) === "function") return this.ModalActions.openModal(render);
if (!this.hasInitialized) this.makeStack();
options.modalKey = generateKey(options.modalKey);
Events.emit("open-modal", render, options);
return options.modalKey;
}
}
Modals.makeStack();

View File

@ -1,13 +1,21 @@
import React from "@modules/react";
import Strings from "@modules/strings";
import DiscordClasses from "@modules/discordclasses";
import WebpackModules from "@modules/webpackmodules";
import Text from "@ui/base/text";
import Button from "@ui/base/button";
import Flex from "@ui/base/flex";
import Extension from "@ui/icons/extension";
import ThemeIcon from "@ui/icons/theme";
import Divider from "@ui/divider";
import Header from "./header";
import Content from "./content";
import ModalRoot from "./root";
import Footer from "./footer";
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
const {useState, useCallback, useMemo} = React;
@ -35,12 +43,12 @@ function AddonError({err, index}) {
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
<div className="bd-addon-error-header-inner">
<h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
<div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<Text tag="h3" size={Text.Sizes.SIZE_16} color={Text.Colors.HEADER_PRIMARY} strong={true}>{err.name}</Text>
<div className="bd-addon-error-details">
<svg className="bd-addon-error-details-icon" aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" 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"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
<Text color={Text.Colors.HEADER_SECONDARY} size={Text.Sizes.SIZE_12}>{err.message}</Text>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
@ -56,7 +64,7 @@ function generateTab(id, errors) {
return {id, errors, name: Strings.Panels[id]};
}
export default function AddonErrorModal({pluginErrors, themeErrors}) {
export default function AddonErrorModal({transitionState, onClose, pluginErrors, themeErrors}) {
const tabs = useMemo(() => {
return [
pluginErrors.length && generateTab("plugins", pluginErrors),
@ -68,17 +76,22 @@ export default function AddonErrorModal({pluginErrors, themeErrors}) {
const switchToTab = useCallback((id) => setTab(id), []);
const selectedTab = tabs.find(e => e.id === tabId);
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={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</div>
</>;
return <ModalRoot transitionState={transitionState} className="bd-error-modal" size={ModalRoot.Sizes.MEDIUM}>
<Header className="bd-error-modal-header">
<Flex direction={Flex.Direction.VERTICAL}>
<Text tag="h1" size={Text.Sizes.SIZE_14} color={Text.Colors.HEADER_PRIMARY} strong={true} style={{"text-transform": "uppercase", "margin-bottom": "8px"}}>{Strings.Modals.addonErrors}</Text>
<div className="bd-tab-bar">
{tabs.map(tab => <div onClick={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
</Flex>
</Header>
<Content className="bd-error-modal-content">
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</Content>
<Footer className="bd-error-modal-footer">
<Button onClick={onClose}>{Strings.Modals.okay}</Button>
</Footer>
</ModalRoot>;
}

View File

@ -0,0 +1,35 @@
import React from "@modules/react";
import WebpackModules from "@modules/webpackmodules";
import Utilities from "@modules/utilities";
const Spring = WebpackModules.getByProps("useSpring", "animated");
export default function Backdrop({isVisible, className, onClick}) {
const transition = Spring.useTransition(isVisible, {
keys: e => e ? "backdrop" : "empty",
config: {duration: 300},
from: {
opacity: 0,
background: "var(--black-500)"
},
enter: {
opacity: 0.85,
background: "var(--black-500)"
},
leave: {
opacity: 0,
background: "var(--black-500)"
}
});
return transition((styles, visible) => {
if (!visible) return null;
return <Spring.animated.div
className={Utilities.className("bd-modal-backdrop", className)}
style={styles}
onClick={onClick}
/>;
});
}

View File

@ -0,0 +1,100 @@
import React from "@modules/react";
import WebpackModules from "@modules/webpackmodules";
import DiscordModules from "@modules/discordmodules";
import Strings from "@modules/strings";
import Root from "./root";
import Header from "./header";
import Footer from "./footer";
import Content from "./content";
import Flex from "../base/flex";
import Text from "../base/text";
import CloseButton from "./close";
import SimpleMarkdownExt from "@structs/markdown";
import Twitter from "@ui/icons/twitter";
import GitHub from "@ui/icons/github";
const {useMemo} = React;
const AnchorClasses = WebpackModules.getByProps("anchorUnderlineOnHover") || {anchor: "anchor-3Z-8Bb", anchorUnderlineOnHover: "anchorUnderlineOnHover-2ESHQB"};
const joinSupportServer = (click) => {
click.preventDefault();
click.stopPropagation();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel({inviteKey: "0Tmfo5ZbORCRqbAd"});
};
const supportLink = <a className={`${AnchorClasses.anchor} ${AnchorClasses.anchorUnderlineOnHover}`} onClick={joinSupportServer}>Join our Discord Server.</a>;
const defaultFooter = <Text>Need support? {supportLink}</Text>;
const twitter = <DiscordModules.Tooltip color="primary" position="top" text={Strings.Socials.twitter}>
{p => <a {...p} className="bd-social" href="https://x.com/_BetterDiscord_" rel="noopener noreferrer" target="_blank">
<Twitter />
</a>}
</DiscordModules.Tooltip>;
const github = <DiscordModules.Tooltip color="primary" position="top" text={Strings.Socials.github}>
{p => <a {...p} className="bd-social" href="https://github.com/BetterDiscord/BetterDiscord" rel="noopener noreferrer" target="_blank">
<GitHub />
</a>}
</DiscordModules.Tooltip>;
function YoutubeEmbed({src}) {
return <iframe
src={src}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>;
}
function Video({src, poster}) {
if (src.toLowerCase().includes("youtube.com")) return <YoutubeEmbed src={src} />;
return <video src={src} poster={poster} controls={true} className="bd-changelog-poster" />;
}
export default function ChangelogModal({transitionState, footer, title, subtitle, onClose, video, poster, image, description, changes}) {
const ChangelogHeader = useMemo(() => <Header justify={Flex.Justify.BETWEEN}>
<Flex direction={Flex.Direction.VERTICAL}>
<Text tag="h1" size={Text.Sizes.SIZE_20} strong={true}>{title}</Text>
<Text size={Text.Sizes.SIZE_12} color={Text.Colors.HEADER_SECONDARY}>{subtitle}</Text>
</Flex>
<CloseButton onClick={onClose} />
</Header>, [title, subtitle, onClose]);
const ChangelogFooter = useMemo(() => <Footer justify={Flex.Justify.BETWEEN} direction={Flex.Direction.HORIZONTAL}>
<Flex.Child grow="1" shrink="1">
{footer ? footer : defaultFooter}
</Flex.Child>
{!footer && <Flex.Child grow="0" shrink="0">
{twitter}
{github}
</Flex.Child>}
</Footer>, [footer]);
const changelogItems = useMemo(() => {
const items = [video ? <Video src={video} poster={poster} /> : <img src={image} className="bd-changelog-poster" />];
if (description) items.push(<p>{SimpleMarkdownExt.parseToReact(description)}</p>);
for (let c = 0; c < changes.length; c++) {
const entry = changes[c];
const type = "bd-changelog-" + entry.type;
const margin = c == 0 ? " bd-changelog-first" : "";
items.push(<h1 className={`bd-changelog-title ${type}${margin}`}>{entry.title}</h1>);
if (entry.description) items.push(<p>{SimpleMarkdownExt.parseToReact(entry.description)}</p>);
const list = <ul>{entry.items.map(i => <li>{SimpleMarkdownExt.parseToReact(i)}</li>)}</ul>;
items.push(list);
}
return items;
}, [description, video, image, poster, changes]);
return <Root className="bd-changelog-modal" transitionState={transitionState} size={Root.Sizes.MEDIUM} style={Root.Styles.STANDARD}>
{ChangelogHeader}
<Content>{changelogItems}</Content>
{ChangelogFooter}
</Root>;
}

View File

@ -0,0 +1,17 @@
import React from "@modules/react";
import Button from "../base/button";
import Close from "../icons/close";
export default function CloseButton({onClick}) {
return <Button
className="bd-close-button"
size={Button.Sizes.ICON}
look={Button.Looks.BLANK}
color={Button.Colors.TRANSPARENT}
onClick={onClick}
>
<Close size="24px" />
</Button>;
}

View File

@ -0,0 +1,57 @@
import React from "@modules/react";
import Strings from "@modules/strings";
import Root from "./root";
import Header from "./header";
import Footer from "./footer";
import Content from "./content";
import Text from "../base/text";
import Button from "../base/button";
const {useRef, useEffect, useLayoutEffect} = React;
export default function ConfirmationModal({transitionState, onClose, onCloseCallback, className, size = Root.Sizes.SMALL, header, children, danger = false, onCancel = () => {}, onConfirm = () => {}, cancelText = Strings.Modals.cancel, confirmText = Strings.Modals.okay}) {
const buttonRef = useRef(null);
useEffect(() => {
setTimeout(() => buttonRef?.current?.focus?.(), 0);
}, []);
useLayoutEffect(() => {
onCloseCallback?.();
}, [onCloseCallback]);
return <Root transitionState={transitionState} size={size} className={className}>
<Header>
<Text tag="h1" size={Text.Sizes.SIZE_20} color={Text.Colors.HEADER_PRIMARY} strong={true}>{header}</Text>
</Header>
<Content>{children}</Content>
<Footer>
{confirmText && <Button
type="submit"
buttonRef={buttonRef}
color={danger ? Button.Colors.RED : Button.Colors.BRAND}
onClick={() => {
onConfirm?.();
onClose();
}}
>
{confirmText}
</Button>}
{cancelText && <Button
type="button"
look={Button.Looks.LINK}
color={Button.Colors.PRIMARY}
onClick={() => {
onCancel?.();
onClose();
}}
>
{cancelText}
</Button>}
</Footer>
</Root>;
}

View File

@ -0,0 +1,9 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
export default function Content({id, className, children, scroller = true}) {
return <div id={id} className={Utilities.className("bd-modal-content", {"bd-scroller-base bd-scroller-thin": scroller}, className)}>
{children}
</div>;
}

View File

@ -0,0 +1,20 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
import Flex from "../base/flex";
export default function Footer({id, className, children, justify, direction, align, wrap}) {
return <Flex
id={id}
className={Utilities.className("bd-modal-footer", className)}
grow={0}
shrink={0}
direction={direction ?? Flex.Direction.HORIZONTAL_REVERSE}
justify={justify ?? Flex.Justify.START}
align={align ?? Flex.Align.STRETCH}
wrap={wrap ?? Flex.Wrap.NO_WRAP}
>
{children}
</Flex>;
}

View File

@ -0,0 +1,20 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
import Flex from "../base/flex";
export default function Header({id, className, children}) {
return <Flex
id={id}
className={Utilities.className("bd-modal-header", className)}
grow={0}
shrink={0}
direction={Flex.Direction.HORIZONTAL}
justify={Flex.Justify.START}
align={Flex.Align.CENTER}
wrap={Flex.Wrap.NO_WRAP}
>
{children}
</Flex>;
}

View File

@ -0,0 +1,96 @@
import React from "@modules/react";
import Utilities from "@modules/utilities";
import WebpackModules from "@modules/webpackmodules";
const Spring = WebpackModules.getByProps("useSpring", "animated");
const Anims = WebpackModules.getByProps("Easing");
export const Sizes = Object.freeze({
SMALL: "bd-modal-small",
MEDIUM: "bd-modal-medium",
LARGE: "bd-modal-large",
DYNAMIC: ""
});
export const Styles = Object.freeze({
STANDARD: "bd-modal-standard",
CUSTOM: ""
});
const AccessibilityContext = WebpackModules.getModule(m => m?._currentValue?.reducedMotion, {searchExports: true});
const FocusLock = WebpackModules.getModule(m => m?.render?.toString().includes("impressionProperties") && m?.render?.toString().includes(".Provider"), {searchExports: true}) ?? React.Fragment;
export default function ModalRoot({className, transitionState, children, size = Sizes.DYNAMIC, style = Styles.CUSTOM}) {
const visible = transitionState == 0 || transitionState == 1; // 300 ms
const preferences = React.useContext(AccessibilityContext ?? {});
const reducedMotion = preferences?.reducedMotion?.enabled ?? document.documentElement?.classList.contains("reduce-motion");
const springStyles = Spring.useSpring({
opacity: visible ? 1 : 0,
transform: visible || reducedMotion ? "scale(1)" : "scale(0.7)",
config: {
duration: visible ? 300 : 100,
easing: visible ? Anims.Easing.inOut(Anims.Easing.back()) : Anims.Easing.quad,
clamp: true
}
});
return <FocusLock disableTrack={true}>
<Spring.animated.div
className={Utilities.className("bd-modal-root", size, className, style)}
style={springStyles}
>
{children}
</Spring.animated.div>
</FocusLock>;
// const [visible, setVisible] = React.useState(true);
// const visible = transitionState < 2;
// const springTransition = Spring.useTransition(transitionState, {
// keys: e => e ? "backdrop" : "empty",
// from: {
// opacity: 0,
// transform: "scale(0.7)"
// },
// enter: {
// opacity: 1,
// transform: "scale(1)"
// },
// leave: {
// opacity: 0,
// transform: "scale(0.7)"
// },
// // config: (a, b, c, d) => {
// // console.log({a, b, c, d});
// // return {
// // duration: a ? 300 : 100,
// // easing: a ? Anims.Easing.inOut(Anims.Easing.back()) : Anims.Easing.quad,
// // clamp: true
// // };
// // }
// config: (a, b, c) => {
// console.log({a, b, c});
// return {
// duration: true ? 300 : 100,
// easing: true ? Anims.Easing.inOut(Anims.Easing.back()) : Anims.Easing.quad,
// clamp: true
// };
// }
// });
// return springTransition((styles, isVisible) => {
// if (!isVisible) console.log("not visible");
// return <Spring.animated.div
// className={Utilities.className("bd-modal-root", size, className, style)}
// style={styles}
// >
// {children}
// </Spring.animated.div>;
// });
}
ModalRoot.Sizes = Sizes;
ModalRoot.Styles = Styles;

View File

@ -0,0 +1,76 @@
import React from "@modules/react";
import Events from "@modules/emitter";
import WebpackModules from "@modules/webpackmodules";
import Backdrop from "./backdrop";
const {Fragment, useState, useCallback, useEffect} = React;
const Transitions = WebpackModules.getModule(m => m?.defaultProps?.transitionAppear);
// const Transitions = WebpackModules.getByProps("TransitionGroup").TransitionGroup;
class ModalLayer extends React.Component {
constructor(props) {
super(props);
this.state = {transitionState: null};
}
componentWillEnter(finish) {
this.setState({transitionState: 0});
setTimeout(() => {
this.setState({transitionState: 1});
finish();
}, 300);
}
componentWillLeave(finish) {
this.setState({transitionState: 2});
setTimeout(() => {
this.setState({transitionState: 3});
finish();
}, 300);
}
render() {
return React.createElement("div", {className: "bd-modal-layer"}, this.props.render({
transitionState: this.state.transitionState,
onClose: this.props.onClose
}));
}
}
// ENTERING 0
// ENTERED 1
// EXITING 2
// EXITED 3
// HIDDEN 4
let modalKey = 0;
export const generateKey = key => key ? `${key}-${modalKey++}` : modalKey++;
export default function ModalStack() {
const [modals, setModals] = useState([]);
const addModal = useCallback((render, props = {}) => {
setModals(m => [...m, {...props, render}]);
}, []);
const removeModal = useCallback((key) => {
setModals(mods => mods.filter(m => {
if (m.modalKey === key && m.onClose) m.onClose();
return m.modalKey !== key;
}));
}, []);
useEffect(() => {
Events.on("open-modal", addModal);
return () => {
Events.off("open-modal", addModal);
};
}, [addModal]);
return <Transitions component={Fragment}>
<Backdrop isVisible={!!modals.length} onClick={() => removeModal(modals[modals.length - 1].modalKey)} />
{modals.length && <ModalLayer key={modals[modals.length - 1].modalKey} {...modals[modals.length - 1]} onClose={() => removeModal(modals[modals.length - 1].modalKey)} />}
</Transitions>;
}

View File

@ -1,20 +1,87 @@
import Config from "@data/config";
import React from "@modules/react";
import Strings from "@modules/strings";
import Utilities from "@modules/utilities";
import Events from "@modules/emitter";
import Settings from "@modules/settingsmanager";
import DataStore from "@modules/datastore";
import WebpackModules, {Filters} from "@modules/webpackmodules";
import Patcher from "@modules/patcher";
import DiscordModules from "@modules/discordmodules";
import PluginManager from "@modules/pluginmanager";
import ThemeManager from "@modules/thememanager";
import AddonList from "./settings/addonlist";
import SettingsGroup from "./settings/group";
import SettingsTitle from "./settings/title";
import Header from "./settings/sidebarheader";
import ReactUtils from "@modules/api/reactutils";
import Button from "@ui/base/button";
import Modals from "@ui/modals";
import AddonList from "@ui/settings/addonlist";
import SettingsGroup from "@ui/settings/group";
import SettingsTitle from "@ui/settings/title";
import Header from "@ui/settings/sidebarheader";
import Restore from "./icons/restore";
import Text from "./base/text";
// import SettingsPanel from "./settings/panel";
function makeResetButton(collectionId, refresh) {
const action = confirmReset(() => {
Settings.resetCollection(collectionId);
refresh?.();
});
return <DiscordModules.Tooltip color="primary" position="top" text={Strings.Settings.resetSettings}>
{(props) =>
<Button {...props} size={Button.Sizes.ICON} look={Button.Looks.BLANK} color={Button.Colors.TRANSPARENT} onClick={action}>
<Restore />
</Button>
}
</DiscordModules.Tooltip>;
}
/**
* @param {function} action
* @returns
*/
function confirmReset(action) {
return () => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Settings.resetSettingsWarning, {
confirmText: Strings.Modals.okay,
cancelText: Strings.Modals.cancel,
danger: true,
onConfirm: action,
});
};
}
function getDebugInfo(discordInfo, pluginsEnabled, themesEnabled) {
const lines = ["```md", `## Discord Info\n${discordInfo}\n`];
lines.push(`## BetterDiscord`);
lines.push(`stable ${Config.version}\n`);
lines.push(`### Plugins (${pluginsEnabled} Enabled):\n${PluginManager.addonList.map(a => `- ${a.name}${PluginManager.isEnabled(a.id) ? " (Enabled)" : ""}`).join("\n")}\n`);
lines.push(`### Themes (${themesEnabled} Enabled):\n${ThemeManager.addonList.map(a => `- ${a.name}${ThemeManager.isEnabled(a.id) ? " (Enabled)" : ""}`).join("\n")}`);
lines.push("```");
return lines.join("\n");
}
/**
*
* @param {string} type plugin or theme
* @returns {{total: number, enabled: number}}
*/
function getAddonCount(type) {
if (type === "theme") return {total: ThemeManager.addonList.length, enabled: ThemeManager.addonList.filter(p => ThemeManager.isEnabled(p.id)).length};
if (type === "plugin") return {total: PluginManager.addonList.length, enabled: PluginManager.addonList.filter(p => PluginManager.isEnabled(p.id)).length};
return {total: 0, enabled: 0};
}
export default new class SettingsRenderer {
constructor() {
this.patchSections();
this.patchVersionInformation();
Events.on("strings-updated", this.forceUpdate);
}
@ -34,22 +101,32 @@ export default new class SettingsRenderer {
onChange(onChange) {
return (collection, category, id) => {
const before = Settings.collections.length + Settings.panels.length;
onChange(collection, category, id);
const after = Settings.collections.length + Settings.panels.length;
if (before != after) setTimeout(this.forceUpdate.bind(this), 50);
// Delay until after switch animation
// TODO: lift settings state to SettingsPanel
// to prevent the need for this.
setTimeout(this.forceUpdate.bind(this), 250);
};
}
buildSettingsPanel(id, title, config, state, onChange, button = null) {
buildSettingsPanel(id, title, config, state, onChange) {
config.forEach(section => {
section.settings.forEach(item => item.value = state[section.id][item.id]);
});
return this.getSettingsPanel(id, title, config, this.onChange(onChange), button);
return this.getSettingsPanel(id, title, config, this.onChange(onChange));
}
getSettingsPanel(id, title, groups, onChange, button = null) {
return [React.createElement(SettingsTitle, {text: title, button: button}), groups.map(section => {
getSettingsPanel(id, title, groups, onChange) {
// return <SettingsPanel
// id={id}
// title={title}
// groups={groups}
// onChange={onChange}
// onDrawerToggle={(...args) => this.onDrawerToggle(...args)}
// getDrawerState={(...args) => this.getDrawerState(...args)}
// />;
return [React.createElement(SettingsTitle, {text: title}, makeResetButton(id, this.forceUpdate.bind(this))), groups.map(section => {
return React.createElement(SettingsGroup, Object.assign({}, section, {
onChange: onChange,
onDrawerToggle: state => this.onDrawerToggle(id, section.id, state),
@ -85,7 +162,7 @@ export default new class SettingsRenderer {
section: collection.name,
label: collection.name.toString(),
className: `bd-${collection.id}-tab`,
element: () => this.buildSettingsPanel(collection.id, collection.name, collection.settings, Settings.state[collection.id], Settings.onSettingChange.bind(Settings, collection.id), collection.button ? collection.button : null)
element: () => this.buildSettingsPanel(collection.id, collection.name, collection.settings, Settings.state[collection.id], Settings.onSettingChange.bind(Settings, collection.id))
});
}
for (const panel of Settings.panels.sort((a,b) => a.order > b.order ? 1 : -1)) {
@ -97,11 +174,59 @@ export default new class SettingsRenderer {
});
}
async patchVersionInformation() {
const versionDisplayModule = await WebpackModules.getLazy(Filters.byStrings("RELEASE_CHANNEL", "COPY_VERSION"), {defaultExport: false});
if (!versionDisplayModule?.default) return;
Patcher.after("SettingsManager", versionDisplayModule, "default", (_, __, reactTree) => {
const currentCopy = reactTree?.props?.copyValue;
const target = reactTree?.props?.children?.props?.children;
// Do some sanity checking to make sure this is both the right component
// and that it's in the format we expect
if (!Array.isArray(target) || !currentCopy) return;
const [pluginCount, setPluginCount] = React.useState(getAddonCount("plugin"));
const [themeCount, setThemeCount] = React.useState(getAddonCount("theme"));
React.useEffect(() => {
const hooks = [setPluginCount, setThemeCount];
const handlers = {};
const types = ["plugin", "theme"];
const events = ["enabled", "disabled", "loaded", "unloaded"];
// Set handlers and add event listeners
for (let t = 0; t < types.length; t++) {
handlers[types[t]] = () => hooks[t](getAddonCount(types[t]));
for (let e = 0; e < events.length; e++) {
Events.on(`${types[t]}-${events[e]}`, handlers[types[t]]);
}
}
return () => {
// Remove event listeners
for (let t = 0; t < types.length; t++) {
for (let e = 0; e < events.length; e++) {
Events.off(`${types[t]}-${events[e]}`, handlers[types[t]]);
}
}
};
}, []);
Object.assign(reactTree.props, {get copyValue() {return getDebugInfo(currentCopy, pluginCount.enabled, themeCount.enabled);}});
target.push(<Text color={Text.Colors.MUTED} size={Text.Sizes.SIZE_12}>BetterDiscord {Config.version}</Text>);
target.push(<Text color={Text.Colors.MUTED} size={Text.Sizes.SIZE_12}>{Strings.Panels.plugins} {pluginCount.total} ({pluginCount.enabled} {Strings.Addons.isEnabled})</Text>);
target.push(<Text color={Text.Colors.MUTED} size={Text.Sizes.SIZE_12}>{Strings.Panels.themes} {themeCount.total} ({themeCount.enabled} {Strings.Addons.isEnabled})</Text>);
});
}
forceUpdate() {
const viewClass = WebpackModules.getByProps("standardSidebarView")?.standardSidebarView.split(" ")[0];
const node = document.querySelector(`.${viewClass}`);
if (!node) return;
const stateNode = Utilities.findInTree(node?.__reactFiber$, m => m && m.getPredicateSections, {walkable: ["return", "stateNode"]});
const stateNode = Utilities.findInTree(ReactUtils.getInternalInstance(node), m => m && m.getPredicateSections, {walkable: ["return", "stateNode"]});
if (stateNode) stateNode.forceUpdate();
}
};

View File

@ -66,7 +66,7 @@ function makeButton(title, children, action, {isControl = false, danger = false,
const ButtonType = isControl ? "button" : "div";
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <ButtonType {...props} className={(isControl ? "bd-button bd-addon-button" : "bd-addon-button") + (danger ? " bd-button-danger" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action}>{children}</ButtonType>;
return <ButtonType {...props} className={(isControl ? "bd-button bd-button-filled bd-addon-button" : "bd-addon-button") + (danger ? " bd-button-color-red" : isControl ? " bd-button-color-brand" : "") + (disabled ? " bd-button-disabled" : "")} onClick={action} disabled={disabled}>{children}</ButtonType>;
}}
</DiscordModules.Tooltip>;
}

View File

@ -5,6 +5,7 @@ import DataStore from "@modules/datastore";
import DiscordModules from "@modules/discordmodules";
import ipc from "@modules/ipc";
import Button from "../base/button";
import SettingsTitle from "./title";
import AddonCard from "./addoncard";
import Dropdown from "./components/dropdown";
@ -47,20 +48,20 @@ function openFolder(folder) {
function blankslate(type, onClick) {
const message = Strings.Addons.blankSlateMessage.format({link: `https://betterdiscord.app/${type}s`, type}).toString();
return <EmptyImage title={Strings.Addons.blankSlateHeader.format({type})} message={message}>
<button className="bd-button" onClick={onClick}>{Strings.Addons.openFolder.format({type})}</button>
<Button size={Button.Sizes.LARGE} onClick={onClick}>{Strings.Addons.openFolder.format({type})}</Button>
</EmptyImage>;
}
function makeBasicButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => <button {...props} className="bd-button" onClick={action}>{children}</button>}
{(props) => <Button {...props} size={Button.Sizes.NONE} look={Button.Looks.BLANK} className="bd-button" onClick={action}>{children}</Button>}
</DiscordModules.Tooltip>;
}
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
return <button {...props} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</button>;
return <Button {...props} size={Button.Sizes.NONE} look={Button.Looks.BLANK} className={"bd-button bd-view-button" + (selected ? " selected" : "")} onClick={action}>{children}</Button>;
}}
</DiscordModules.Tooltip>;
}

View File

@ -1,5 +1,6 @@
import React from "@modules/react";
import DiscordModules from "@modules/discordmodules";
import Strings from "@modules/strings";
const {useState, useCallback} = React;
@ -76,7 +77,7 @@ export default function Color({value: initialValue, onChange, colors = defaultCo
</div>
)}
</DiscordModules.Tooltip>
<DiscordModules.Tooltip text="Custom Color" position="bottom">
<DiscordModules.Tooltip text={Strings.Settings.customColor} position="bottom">
{props => (
<div className="bd-color-picker-custom">
<Dropper color={getContrastColor(resolveColor(value, true))} />

View File

@ -1,5 +1,6 @@
import React from "@modules/react";
import Button from "../../base/button";
import Keyboard from "@ui/icons/keyboard";
import Close from "@ui/icons/close";
@ -45,8 +46,8 @@ export default function Keybind({value: initialValue, onChange, max = 2, clearab
return <div className={"bd-keybind-wrap" + (state.isRecording ? " recording" : "")} onClick={onClick}>
<input readOnly={true} type="text" className="bd-keybind-input" value={displayValue} />
<div className="bd-keybind-controls">
<button className={"bd-button bd-keybind-record" + (state.isRecording ? " bd-button-danger" : "")}><Keyboard size="24px" /></button>
{clearable && <button onClick={clearKeybind} className="bd-button bd-keybind-clear"><Close size="24px" /></button>}
<Button size={Button.Sizes.ICON} look={Button.Looks.FILLED} color={state.isRecording ? Button.Colors.RED : Button.Colors.BRAND} className="bd-keybind-record" onClick={onClick}><Keyboard size="24px" /></Button>
{clearable && <Button size={Button.Sizes.ICON} look={Button.Looks.BLANK} onClick={clearKeybind} className="bd-keybind-clear"><Close size="24px" /></Button>}
</div>
</div>;
}

View File

@ -1,4 +1,5 @@
import React from "@modules/react";
import Button from "@ui/base/button";
import Close from "@ui/icons/close";
import SearchIcon from "@ui/icons/search";
@ -23,12 +24,14 @@ export default function Search({onChange, className, onKeyDown, placeholder}) {
const reset = useCallback(() => {
onChange?.({target: {value: ""}});
setValue("");
}, [onChange]);
if (!input.current) return;
input.current.focus();
}, [onChange, input]);
return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} ref={input}/>
{!value && <SearchIcon />}
{value && <button className="bd-button" onClick={reset}><Close size="16px" /></button>}
{value && <Button look={Button.Looks.BLANK} color={Button.Colors.TRANSPARENT} size={Button.Sizes.NONE} onClick={reset}><Close size="16px" /></Button>}
</div>;
}

View File

@ -9,7 +9,7 @@ const {useState, useCallback, useRef} = React;
const baseClassName = "bd-settings-group";
export default function Drawer({name, collapsible, shown = true, showDivider, children, button, onDrawerToggle}) {
export default function Drawer({name, collapsible, shown = true, showDivider, children, titleChildren, onDrawerToggle}) {
const container = useRef(null);
const [collapsed, setCollapsed] = useState(collapsible && !shown);
const toggleCollapse = useCallback(() => {
@ -26,17 +26,13 @@ export default function Drawer({name, collapsible, shown = true, showDivider, ch
}, [collapsed, onDrawerToggle]);
const onClick = useCallback((event) => {
event.stopPropagation();
button?.onClick(...arguments);
}, [button]);
const collapseClass = collapsible ? `collapsible ${collapsed ? "collapsed" : "expanded"}` : "";
const groupClass = `${baseClassName} ${collapseClass}`;
return <div className={groupClass}>
<Title text={name} collapsible={collapsible} onClick={toggleCollapse} button={button ? {...button, onClick} : null} isGroup={true} />
<Title text={name} collapsible={collapsible} onClick={toggleCollapse} isGroup={true}>
{titleChildren}
</Title>
<div className="bd-settings-container" ref={container}>
{children}
</div>

View File

@ -0,0 +1,68 @@
import React from "@modules/react";
import Strings from "@modules/strings";
import Utilities from "@modules/utilities";
import Events from "@modules/emitter";
import Settings from "@modules/settingsmanager";
import DataStore from "@modules/datastore";
import WebpackModules, {Filters} from "@modules/webpackmodules";
import Patcher from "@modules/patcher";
import DiscordModules from "@modules/discordmodules";
import Button from "@ui/base/button";
import Modals from "@ui/modals";
import AddonList from "@ui/settings/addonlist";
import SettingsGroup from "@ui/settings/group";
import SettingsTitle from "@ui/settings/title";
import Header from "@ui/settings/sidebarheader";
import Restore from "@ui/icons/restore";
const {useCallback, useEffect, useReducer} = React;
function makeResetButton(collectionId) {
const action = confirmReset(() => Settings.resetCollection(collectionId));
return <DiscordModules.Tooltip color="primary" position="top" text={Strings.Settings.resetSettings}>
{(props) =>
<Button {...props} size={Button.Sizes.ICON} look={Button.Looks.BLANK} color={Button.Colors.TRANSPARENT} onClick={action}>
<Restore />
</Button>
}
</DiscordModules.Tooltip>;
}
/**
* @param {function} action
* @returns
*/
function confirmReset(action) {
return () => {
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Settings.resetSettingsWarning, {
confirmText: Strings.Modals.okay,
cancelText: Strings.Modals.cancel,
danger: true,
onConfirm: action,
});
};
}
export default function SettingsPanel({id, title, groups, onChange, onDrawerToggle, getDrawerState}) {
// TODO: add onChange here to lift and manage state here
return <>
<SettingsTitle text={title}>
{makeResetButton(id)}
</SettingsTitle>,
{groups.map(section => {
const props = Object.assign({}, section, {
onChange,
onDrawerToggle: state => onDrawerToggle(id, section.id, state),
shown: getDrawerState(id, section.id, section.hasOwnProperty("shown") ? section.shown : true)
});
return <SettingsGroup {...props} />;
})}
</>;
}

View File

@ -7,6 +7,7 @@ import Strings from "@modules/strings";
import HistoryIcon from "@ui/icons/history";
import Modals from "@ui/modals";
import Button from "@ui/base/button";
export default function SettingsTitle() {
@ -14,9 +15,9 @@ export default function SettingsTitle() {
<h2 className="bd-sidebar-header-label">BetterDiscord</h2>
<DiscordModules.Tooltip color="primary" position="top" text={Strings.Modals.changelog}>
{props =>
<div {...props} className="bd-changelog-button" onClick={() => Modals.showChangelogModal(Changelog)}>
<Button {...props} className="bd-changelog-button" look={Button.Looks.BLANK} color={Button.Colors.TRANSPARENT} size={Button.Sizes.NONE} onClick={() => Modals.showChangelogModal(Changelog)}>
<HistoryIcon className="bd-icon" size="16px" />
</div>
</Button>
}
</DiscordModules.Tooltip>
</div>;

View File

@ -1,5 +1,7 @@
import React from "@modules/react";
import Button from "../base/button";
const {useCallback} = React;
@ -18,7 +20,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text
const titleClass = className ? `${baseClass} ${className}` : baseClass;
return <h2 className={titleClass} onClick={() => {onClick?.();}}>
{text}
{button && <button className="bd-button bd-button-title" onClick={click}>{button.title}</button>}
{button && <Button className="bd-button-title" onClick={click} size={Button.Sizes.NONE}>{button.title}</Button>}
{children}
</h2>;

View File

@ -3,21 +3,45 @@ import Config from "@data/config";
import React from "@modules/react";
import Strings from "@modules/strings";
import Events from "@modules/emitter";
import DiscordModules from "@modules/discordmodules";
import Drawer from "./settings/drawer";
import SettingItem from "./settings/components/item";
import SettingsTitle from "./settings/title";
import Toasts from "./toasts";
import Button from "@ui/base/button";
import Drawer from "@ui/settings/drawer";
import SettingItem from "@ui/settings/components/item";
import SettingsTitle from "@ui/settings/title";
import Toasts from "@ui/toasts";
import Checkmark from "@ui/icons/check";
import Download from "@ui/icons/download";
import Reload from "@ui/icons/reload";
import Sync from "@ui/icons/sync";
const {useState, useCallback, useEffect} = React;
function makeButton(tooltip, children, action, options = {}) {
const {size = Button.Sizes.ICON, look = Button.Looks.BLANK, color = Button.Colors.TRANSPARENT, className = "", stopAnimation = false} = options;
const onClick = async (event) => {
const button = event.target.closest("button");
button.classList.add("animate");
await action();
if (!stopAnimation) return;
await new Promise(r => setTimeout(r, 500)); // Allow animation to complete at least once.
button?.classList?.remove("animate"); // Stop animation if it hasn't been removed from the DOM
};
return <DiscordModules.Tooltip color="primary" position="top" text={tooltip}>
{(props) => <Button {...props} className={`bd-update-button ${className}`} size={size} look={look} color={color} onClick={onClick}>{children}</Button>}
</DiscordModules.Tooltip>;
}
function CoreUpdaterPanel({hasUpdate, remoteVersion, update}) {
return <Drawer name="BetterDiscord" collapsible={true}>
<SettingItem name={`Core v${Config.version}`} note={hasUpdate ? Strings.Updater.versionAvailable.format({version: remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
{!hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{hasUpdate && <button className="bd-button" onClick={update}>{Strings.Updater.updateButton}</button>}
{!hasUpdate && <div className="bd-filled-checkmark"><Checkmark size="18px" /></div>}
{hasUpdate && makeButton(Strings.Updater.updateButton, <Download />, update, {className: "no-animation"})}
</SettingItem>
</Drawer>;
}
@ -31,13 +55,17 @@ function NoUpdates({type}) {
function AddonUpdaterPanel({pending, type, updater, update, updateAll}) {
const filenames = pending;
return <Drawer name={Strings.Panels[type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => updateAll(type)} : null}>
return <Drawer
name={Strings.Panels[type]}
collapsible={true}
titleChildren={filenames.length > 1 ? makeButton(Strings.Updater.updateAll, <Reload size="20px" />, () => updateAll(type)) : null}>
{!filenames.length && <NoUpdates type={type} />}
{filenames.map(f => {
const info = updater.cache[f];
const addon = updater.manager.addonList.find(a => a.filename === f);
return <SettingItem name={`${addon.name} v${addon.version}`} note={Strings.Updater.versionAvailable.format({version: info.version})} inline={true} id={addon.name}>
<button className="bd-button" onClick={() => update(type, f)}>{Strings.Updater.updateButton}</button>
{makeButton(Strings.Updater.updateButton, <Reload />, () => update(type, f))}
{/* <Button size={Button.Sizes.SMALL} onClick={() => update(type, f)}>{Strings.Updater.updateButton}</Button> */}
</SettingItem>;
})}
</Drawer>;
@ -106,7 +134,9 @@ export default function UpdaterPanel({coreUpdater, pluginUpdater, themeUpdater})
}, [updateAddon, updates]);
return [
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: checkForUpdates}} />,
<SettingsTitle text={Strings.Panels.updates}>
{makeButton(Strings.Updater.checkForUpdates, <Sync />, checkForUpdates, {className: "bd-update-check", stopAnimation: true})}
</SettingsTitle>,
<CoreUpdaterPanel remoteVersion={coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={themeUpdater} />,

View File

@ -1,5 +1,5 @@
const path = require("path");
const asar = require("asar");
const asar = require("@electron/asar");
const doSanityChecks = require("./validate");
const buildPackage = require("./package");