Compare commits

...

186 Commits
it2 ... master

Author SHA1 Message Date
Alexei Stukov 835cc3134c
Merge pull request #318 from JsSucks/2.0.0-beta.6
beta6
2019-03-16 19:22:45 +02:00
Jiiks 02b30153ee beta6 2019-03-16 19:18:28 +02:00
Alexei Stukov d09dc5a2a5
Merge pull request #315 from Mega-Mewthree/patch-3
Fix #303, Try 3
2019-03-16 19:12:08 +02:00
Mega-Mewthree 98d9a30027
Fix #303, Try 3 2019-03-12 22:11:05 -07:00
Alexei Stukov 5cb4bc15bd
Merge pull request #312 from samuelthomas2774/component-patches
React component patches
2019-03-12 22:44:26 +02:00
Alexei Stukov bb2aba04d5
Missing FileUtils import 2019-03-12 22:40:21 +02:00
Samuel Elliott 82e9b0bd6a
Fix packed plugins 2019-03-12 19:47:55 +00:00
Samuel Elliott 47575d3449
Don’t watch packed themes 2019-03-12 19:22:27 +00:00
Samuel Elliott 32e2582ded
Use the system temporary directory for packed content 2019-03-12 19:10:09 +00:00
Samuel Elliott d58dda6f50
Fix E2EE message button patch 2019-03-12 17:23:56 +00:00
Samuel Elliott 648954d533
Rerender messages when disabling coloured text and fix jumbo Discord emoji in spoilers 2019-03-12 15:59:18 +00:00
Samuel Elliott 4aa38f4582
Move E2EE and emote module to their own directories 2019-03-12 15:27:05 +00:00
Samuel Elliott b3ba1aef13
Render emotes in spoilers 2019-03-11 17:56:29 +00:00
Samuel Elliott 2a6cbd39b7
Remove logging 2019-03-11 16:43:18 +00:00
Samuel Elliott ce5bcb9b85
Patch MessageAccessories instead of ImageWrapper for emotes sent as images 2019-03-10 21:29:17 +00:00
Samuel Elliott 5a3821ad3e
Package installer UploadArea patch 2019-03-10 20:29:55 +00:00
Samuel Elliott fd0032b24c
Autocomplete 2019-03-10 18:12:43 +00:00
Samuel Elliott 226719b36e
Rerender messages after loading emotes 2019-03-10 17:45:20 +00:00
Samuel Elliott fcfee53928
Move all component selectors + filters to ReactAutoPatcher 2019-03-10 16:58:24 +00:00
Samuel Elliott 285ae34b50
Fix component patches 2019-03-10 16:30:13 +00:00
Alexei Stukov a170a97688
Merge pull request #306 from samuelthomas2774/keytar-4.4.1
macOS + Linux keytar + node-sass bindings
2019-03-10 15:30:36 +02:00
Samuel Elliott a770f57b28
Linux keytar + node-sass bindings for Discord Canary 2019-03-10 00:04:02 +00:00
Samuel Elliott 5757fc20c9
macOS keytar + node-sass bindings for Discord Canary 2019-03-09 23:57:29 +00:00
Samuel Elliott 421289f63b
Linux keytar binding 2019-03-09 23:36:47 +00:00
Samuel Elliott ac85316578
.. 2019-03-09 22:02:40 +00:00
Samuel Elliott 08af9be061
Fix guilds wrapper position 2019-03-09 21:55:13 +00:00
Samuel Elliott ead0fbbd1e
Fix extra space on macOS in the plugins and themes panel and use BdMenuItems to add the super secret view 2019-03-09 21:43:41 +00:00
Samuel Elliott dc85a808f8
Fix undefined/storage file 2019-03-09 21:24:01 +00:00
Samuel Elliott aecfa814f9
Fix error Cannot find module ‘core/dist/package.json’ 2019-03-09 20:52:04 +00:00
Samuel Elliott 436f3d3c36
macOS keytar binding 2019-03-09 20:41:40 +00:00
Alexei Stukov 68a8187964
Merge pull request #300 from JsSucks/Jiiks-patch-1
Update issue templates
2019-03-08 21:54:49 +02:00
Alexei Stukov f30e4c12fe
Update issue templates 2019-03-08 21:51:44 +02:00
Alexei Stukov 686514ed1d
Merge pull request #299 from JsSucks/Jiiks-patch-1
Update issue templates
2019-03-08 21:43:50 +02:00
Alexei Stukov d795da1750
Update issue templates 2019-03-08 21:38:52 +02:00
Alexei Stukov 3219ff7c6e
Merge pull request #293 from JsSucks/2.0.0-beta.4
beta 4 to master
2019-03-08 14:11:13 +02:00
Alexei Stukov 33567a2cfd
Merge pull request #292 from JsSucks/updater
Updater
2019-03-08 14:08:07 +02:00
Jiiks 85310bfbff Stop updater thread 2019-03-08 14:00:54 +02:00
Jiiks d1d79a37b7 Start updater and set interval to 30 minutes 2019-03-08 13:58:46 +02:00
Jiiks d95592acc9 Styling and debug view 2019-03-08 13:57:02 +02:00
Jiiks f6a3fb65da v69 bindings 2019-03-08 13:33:52 +02:00
Alexei Stukov 6788cca363
Merge pull request #291 from XeonPowder/patch-1
fixed comma
2019-03-08 00:41:41 +02:00
Lars van der Zande 1fb442e096
fixed comma 2019-03-07 16:30:21 -06:00
Jiiks 6e64ff61c5 Copy csp.json 2019-03-07 23:50:17 +02:00
Jiiks dd8fe68a11 Better 2019-03-07 22:57:07 +02:00
Jiiks a3829089f9 Secrets for devs 2019-03-07 22:54:16 +02:00
Jiiks 174c1ee791 add some secrets 2019-03-07 22:44:29 +02:00
Jiiks b8793fd2b6 Package parsing for production 2019-03-07 21:09:48 +02:00
Jiiks 31986ca3a0 Try to rename first. 2019-03-07 20:22:11 +02:00
Jiiks 10ff740f75 Remove asar external from production 2019-03-07 19:34:32 +02:00
Jiiks 83fbab63c0 add release test script for faster release testing. copy extra files 2019-03-07 19:21:27 +02:00
Jiiks c4670946e6 Use replace to set production 2019-03-07 19:10:08 +02:00
Jiiks e5239d952e Send error event 2019-03-07 14:15:24 +02:00
Jiiks a57783a9d8 add updates available notification 2019-03-07 11:41:32 +02:00
Jiiks 001a6e4fda Functional updater 2019-03-07 11:35:24 +02:00
Jiiks dd621038f9 Reload and restart notifications 2019-03-06 12:57:19 +02:00
Jiiks 288c233447 tar.gz download 2019-03-06 12:11:45 +02:00
Jiiks 399c6e792b Updater ui 2019-03-06 09:29:23 +02:00
Jiiks 0be6facba4 assign old state, some updater checks. 2019-03-06 09:04:03 +02:00
Jiiks dc7247a12d Send updates to client 2019-03-06 08:36:54 +02:00
Jiiks 377c4fd104 Send updater events 2019-03-06 08:19:09 +02:00
Jiiks 252d496dc2 Get release from github, todo use common 2019-03-06 02:57:57 +02:00
Jiiks 83e334c3f8 Getters, debug 2019-03-06 02:16:27 +02:00
Jiiks e72ad10dfc add semver 2019-03-06 02:02:02 +02:00
Jiiks 9ef392c575 test updater 2019-03-05 23:46:42 +02:00
Jiiks 76057efbb7 Remove old updater code 2019-03-05 23:35:39 +02:00
Jiiks dcb121750a Use core version as the main version 2019-03-05 23:30:41 +02:00
Jiiks 13fa769e9e add versions to config 2019-03-05 23:26:17 +02:00
Jiiks 3143991239 Nevet test editor in production 2019-03-05 21:31:40 +02:00
Jiiks 1ae0c5aa4d Never ignore externals in production 2019-03-05 21:31:20 +02:00
Jiiks 5ea39f86f7 add axios to vendor 2019-03-05 21:28:32 +02:00
Jiiks d6a946e096 headers 2019-03-05 21:26:36 +02:00
Jiiks b68c1fbd04 Move csp and add some sources. 2019-03-05 21:02:06 +02:00
Jiiks 07d3629622 axios wrapper base 2019-03-05 20:38:21 +02:00
Jiiks 15daa9acef add axios because it's better 2019-03-05 20:22:26 +02:00
Jiiks 817a4a03b6 add versions to test args and version getters to config 2019-03-05 12:56:19 +02:00
Jiiks e63386e9eb Updater base 2019-03-05 12:53:08 +02:00
Jiiks 402acdfea9 Wrong main 2019-03-05 10:11:45 +02:00
Jiiks b440206d07 add webpack parallel build/watch 2019-03-05 10:07:49 +02:00
Jiiks c7bea4a743 remove csseditor 2019-03-05 09:43:25 +02:00
Jiiks 2528d87b8f beta.4 branch. all future pulls should go to the latest dev branch instead of master. 2019-03-05 08:46:27 +02:00
Alexei Stukov 405d74fada
Merge pull request #290 from JsSucks/Jiiks-patch-1
I like badges
2019-03-05 08:41:27 +02:00
Alexei Stukov b311220132
I like badges 2019-03-05 08:39:11 +02:00
Alexei Stukov 66b47457b1
Merge pull request #289 from JsSucks/snyk-badge
Snyk badge
2019-03-05 08:30:49 +02:00
Alexei Stukov 6e9f9f8bf8
put it in correct place 2019-03-05 07:46:02 +02:00
Alexei Stukov 88a113dc8f
snyk badge 2019-03-05 07:44:13 +02:00
Alexei Stukov a61f860466
Merge pull request #288 from JsSucks/update-versions
Update versions
2019-03-05 07:43:53 +02:00
Jiiks 729a4607bd Use mver for release tag instead. 2019-03-05 07:35:59 +02:00
Jiiks d81dcc9aa2 refresh lock, fix editor production config 2019-03-05 07:32:11 +02:00
Jiiks 1e4f3fa82b semver 2019-03-05 07:25:18 +02:00
Alexei Stukov 150a1d63c4
Merge pull request #287 from JsSucks/deps-and-scripts
Deps and scripts
2019-03-04 20:12:14 +02:00
Jiiks 6b481733b9 editor webpack merge 2019-03-04 20:08:48 +02:00
Jiiks e07b9b1550 bindings 2019-03-04 19:50:34 +02:00
Jiiks 99c2b53ec6 depcopy and configs 2019-03-04 17:02:52 +02:00
Jiiks 2a93e5d2a3 lint 2019-03-04 10:25:00 +02:00
Jiiks 3661207602 switch to babel.config.js 2019-03-04 10:12:42 +02:00
Jiiks 92845728cc command line pls 2019-03-04 10:09:58 +02:00
Jiiks c99753fc8c Use webpack merge to simplify configs 2019-03-04 10:07:32 +02:00
Jiiks 688e6022a0 pls 2019-03-04 09:55:34 +02:00
Jiiks 5408d994be babel config for client 2019-03-04 09:52:25 +02:00
Zack Rauen 003c9766bc remove uglifyjs plugin 2019-03-03 14:47:44 -05:00
Zack Rauen 82e9c257ce Update deps 2019-03-02 22:15:24 -05:00
Jiiks 6167cc7c4b remove installer build 2019-03-01 22:02:06 +02:00
Jiiks 1bacecf8d4 vtc 2019-03-01 22:01:37 +02:00
Jiiks d102686379 vue, vue-color, remove cm, remove csseditor scripts 2019-03-01 21:56:35 +02:00
Jiiks 1160955629 add chokidar 2019-03-01 20:15:30 +02:00
Jiiks e60f765a50 Move bdedit to devdeps 2019-03-01 20:14:33 +02:00
Alexei Stukov d3db696616
Merge pull request #284 from JsSucks/installer-compliance
Installer compliance
2019-02-28 19:25:39 +02:00
Jiiks b8d16c6e4d Load correct editor file 2019-02-28 17:07:53 +02:00
Jiiks b22923d12f editor packaging 2019-02-28 17:00:25 +02:00
Jiiks 1ea307efdd Editor release build 2019-02-28 16:51:45 +02:00
Jiiks d98cff878f uglify editor 2019-02-28 16:47:49 +02:00
Jiiks dd11708f9f return null if no path 2019-02-28 15:20:36 +02:00
Jiiks 665a7818c9 Make sure editor path is set 2019-02-28 15:12:17 +02:00
Jiiks d75b907ae4 ensure user.scss exists 2019-02-28 15:10:34 +02:00
Alexei Stukov f051bc4812
Merge pull request #286 from JsSucks/bdedit
Bdedit
2019-02-28 14:46:48 +02:00
Jiiks 9773f78506 Toasts and name conflict handler 2019-02-28 12:37:57 +02:00
Alexei Stukov f4b7c99c31
Merge pull request #283 from JsSucks/bdedit
Bdedit
2019-02-28 09:51:20 +02:00
Jiiks 97519b2307 lint 2019-02-28 09:48:45 +02:00
Jiiks 806ca5028a auto imports 2019-02-28 09:46:24 +02:00
Jiiks db89e3a1a0 sass doesn't like empty 2019-02-28 09:45:06 +02:00
Jiiks 6030a78b91 Delete and rename handlers 2019-02-28 06:40:33 +02:00
Jiiks 99ec82795c reveal, copy and copypath handlers 2019-02-28 06:05:54 +02:00
Jiiks b1e8b591ba Remove todo 2019-02-27 20:34:31 +02:00
Jiiks ac9e16632d Snippet save/load 2019-02-27 20:34:11 +02:00
Jiiks d8f977b57a Not a dir anymore 2019-02-27 16:43:13 +02:00
Jiiks c3283c47f6 Tons of smol things 2019-02-27 08:10:04 +02:00
Jiiks 2b86f2d741 File watchers 2019-02-27 01:33:23 +02:00
Jiiks 9f6389845a add live update 5 seconds after last change 2019-02-26 22:24:05 +02:00
Jiiks 7489fd0aaf Titlebar buttons fix 2019-02-26 20:30:07 +02:00
Jiiks 7e1c379fd3 Set user.scss caption 2019-02-26 20:19:57 +02:00
Jiiks c0765120e8 Juggle paths and keep user.scss in data 2019-02-26 20:18:33 +02:00
Jiiks 7e10de32cb add new paths and enable snippet import 2019-02-26 19:54:13 +02:00
Jiiks 145d61fe5e Enable importing other files 2019-02-26 18:32:06 +02:00
Jiiks 72b278de6e js should be javascript 2019-02-26 16:33:14 +02:00
Jiiks 5e977e8dc4 Hoist user.scss 2019-02-25 18:05:51 +02:00
Jiiks 109eb31aa5 custom css inject and file saved state 2019-02-25 17:56:56 +02:00
Jiiks 1b6f3005f5 No need to compile if not scss 2019-02-25 13:01:46 +02:00
Jiiks d02f894d4a Style inject 2019-02-25 12:41:22 +02:00
Jiiks 20561e2938 Set type 2019-02-25 12:10:00 +02:00
Jiiks a3724d739b Read files on load at least for now 2019-02-24 21:56:46 +02:00
Jiiks cf1da34e16 add editor file writing 2019-02-24 21:38:33 +02:00
Jiiks 5454950838 add writefile 2019-02-24 21:36:04 +02:00
Jiiks ed7abe3571 Don't need all these prefixes since they're automated 2019-02-24 21:29:54 +02:00
Jiiks b14bf93ef9 remove todo 2019-02-24 21:27:05 +02:00
Jiiks 3436bbe52b add editor test flag 2019-02-24 21:26:52 +02:00
Jiiks 1a26e77dd9 Move editor events to editor 2019-02-24 21:24:37 +02:00
Jiiks da1fc0a2f0 Smol things 2019-02-24 19:57:46 +02:00
Jiiks d1fd5ae881 imodule and base for new editor module in client 2019-02-24 19:46:11 +02:00
Jiiks 8c04e7d2d3 File/snippet saving signals 2019-02-24 18:43:58 +02:00
Jiiks 3fc1adc503 get files/snippets from core 2019-02-24 18:39:28 +02:00
Jiiks 691c9f378a new file/snippet 2019-02-24 18:29:00 +02:00
Jiiks a769385219 Run scripts from editor in Discord window 2019-02-24 16:04:25 +02:00
Jiiks f1f23fa220 Initial BDEdit addition. No logic yet. 2019-02-24 13:03:15 +02:00
Jiiks 99ef0d9f81 add bdedit 2019-02-24 12:28:29 +02:00
Jiiks 5597c485d1 oops 2019-02-24 12:12:13 +02:00
Jiiks 7f08ba27e6 Remove my test script 2019-02-24 12:02:30 +02:00
Jiiks 99614aecdc Fix linter warnings 2019-02-24 11:56:07 +02:00
Alexei Stukov 726db7f0c9
Merge pull request #279 from JsSucks/dnd-patch
Add dnd in non-channels and fix upload failover
2019-02-24 11:46:57 +02:00
Zack Rauen 67c39fd9d9 Add dnd in non-channels and fix upload failover 2019-02-23 09:42:53 -05:00
Jiiks 23d4eb77b2 Fix test mode 2019-02-20 20:13:27 +02:00
Jiiks 124ec078aa only warn if tag is not set 2019-02-20 16:12:56 +02:00
Jiiks e6c9bc447e allow tag to be set with args 2019-02-20 16:10:18 +02:00
Jiiks 7bc4b652fa add child process as external instead of asar 2019-02-20 16:03:03 +02:00
Alexei Stukov 6ab1f5c844
Merge pull request #278 from JsSucks/Jiiks-patch-1
Core version
2019-02-18 15:26:32 +02:00
Alexei Stukov 816ff2ed08
Core version 2019-02-18 15:23:05 +02:00
Jiiks 6ca36ebbb5 Should use core package 2019-02-18 15:21:51 +02:00
Jiiks f511e4556e keep stub 2019-02-18 15:20:44 +02:00
Alexei Stukov 36515432c7
Merge pull request #277 from JsSucks/Jiiks-patch-1
stub should be a file
2019-02-18 15:07:54 +02:00
Alexei Stukov 47782c0168
stub should be a file 2019-02-18 15:03:17 +02:00
Jiiks 3a370f2827 higher compression for core 2019-02-18 15:01:30 +02:00
Jiiks 90f5596cb2 files should be under respective dir 2019-02-18 14:57:00 +02:00
Jiiks 75439a600e stub should be a file 2019-02-18 14:54:27 +02:00
Alexei Stukov f46960fdd0
Merge pull request #276 from JsSucks/Jiiks-patch-1
Installer Test 2
2019-02-18 12:29:38 +02:00
Alexei Stukov f684780842
Installer Test 2 2019-02-18 12:26:29 +02:00
Jiiks 6c8631f1cd TODO editor packaging/renaming 2019-02-18 12:14:38 +02:00
Jiiks 0311602539 update scripts 2019-02-18 12:13:51 +02:00
Jiiks b5e8098d20 Not having asar in externals causes some weird error 2019-02-18 12:03:54 +02:00
Jiiks 111be57b59 release task 2019-02-18 11:42:34 +02:00
Jiiks 76e19c8469 wip script updates 2019-02-18 09:21:28 +02:00
Jiiks ccff016820 init comms module duh 2019-02-17 14:14:07 +02:00
Jiiks 021e282b1c add tests flag back 2019-02-17 12:26:07 +02:00
Jiiks 6699fcc1e7 add base for compatibility 2019-02-17 12:22:13 +02:00
Jiiks f3ea192974 Paths compatibility 2019-02-17 12:18:16 +02:00
Jiiks 9c1a93f4c1 Remove installer scripts, correct sparkplug external 2019-02-17 11:58:45 +02:00
Jiiks 2c23f18e89 New installer compliance 2019-02-17 11:12:12 +02:00
Jiiks c88d2cdae9 paths is an object now 2019-02-17 08:48:41 +02:00
145 changed files with 8301 additions and 9429 deletions

View File

@ -1,9 +0,0 @@
{
"presets": [
["env", {
"targets": {
"node": "6.7.0"
}
}]
]
}

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,33 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: bug
assignees: ''
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**System information**
<!-- os, discord branch etc -->
**Additional context**
<!-- Add any other context about the problem here. -->
**Are you willing and able to fix this?**
<!-- "Yes" or, if "no", what can current contributors do to help you create a PR? -->

12
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@ -0,0 +1,12 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug

View File

@ -0,0 +1,27 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: feature
assignees: ''
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
**Are you willing and able to implement this?**
<!-- "Yes" or, if "no", what can current contributors do to help you create a PR? -->

2
.gitignore vendored
View File

@ -4,6 +4,7 @@ dist
etc
release
tests/tmp
tests/log.txt
# User data
@ -12,3 +13,4 @@ user.config.json
/.vs
/npm-debug.log
/tests/ext/extensions
/tests/userdata

View File

@ -1,4 +1,12 @@
# BetterDiscordApp [![Travis][build-badge]][build]
# BetterDiscordApp [![Travis][build-badge]][build] [![Snyk][snyk-badge]][snyk-url] [![deps][deps-badge]][deps-url] [![devdeps][devdeps-badge]][devdeps-url]
[build-badge]: https://img.shields.io/travis/JsSucks/BetterDiscordApp/master.svg
[build]: https://travis-ci.org/JsSucks/BetterDiscordApp
[snyk-badge]: https://snyk.io/test/github/JsSucks/BetterDiscordApp/badge.svg
[snyk-url]: https://snyk.io/test/github/JsSucks/BetterDiscordApp
[deps-badge]: https://david-dm.org/JsSucks/BetterDiscordApp.svg
[deps-url]: https://david-dm.org/JsSucks/BetterDiscordApp
[devdeps-badge]: https://david-dm.org/JsSucks/BetterDiscordApp/dev-status.svg
[devdeps-url]: https://david-dm.org/JsSucks/BetterDiscordApp?type=dev

17
babel.config.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = function(api) {
api.cache(true);
const presets = [['@babel/env', {
targets: {
'node': '8.6.0'
}
}]];
const plugins = [];
return {
presets,
plugins
}
}

18
client/babel.config.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = function(api) {
api.cache(true);
const presets = [['@babel/env', {
targets: {
'node': '8.6.0',
'chrome': '60'
}
}], '@babel/react'];
const plugins = [];
return {
presets,
plugins
}
}

View File

@ -2,7 +2,7 @@
"name": "bdclient",
"description": "BetterDiscord client package",
"author": "Jiiks",
"version": "2.0.0b",
"version": "2.0.0-beta.6",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "dist/betterdiscord.client.js",

View File

@ -21,7 +21,7 @@ export default new class BlockedMessages extends BuiltinModule {
async enabled(e) {
const MessageListComponents = Reflection.module.byProps('BlockedMessageGroup');
MessageListComponents.OriginalBlockedMessageGroup = MessageListComponents.BlockedMessageGroup;
MessageListComponents.BlockedMessageGroup = () => { return null; };
MessageListComponents.BlockedMessageGroup = () => null;
this.cancelBlockedMessages = () => {
MessageListComponents.BlockedMessageGroup = MessageListComponents.OriginalBlockedMessageGroup;
delete MessageListComponents.OriginalBlockedMessageGroup;

View File

@ -22,10 +22,10 @@ export default class BuiltinModule {
this.patch = this.patch.bind(this);
}
init() {
async init() {
this.setting.on('setting-updated', this._settingUpdated);
if (this.setting.value) {
if (this.enabled) this.enabled();
if (this.enabled) await this.enabled();
if (this.applyPatches) this.applyPatches();
}
}
@ -38,16 +38,15 @@ export default class BuiltinModule {
return Patcher.getPatchesByCaller(`BD:${this.moduleName}`);
}
_settingUpdated(e) {
const { value } = e;
if (value === true) {
if (this.enabled) this.enabled(e);
if (this.applyPatches) this.applyPatches();
return;
}
if (value === false) {
if (this.disabled) this.disabled(e);
async _settingUpdated(e) {
if (e.value) {
if (this.enabled) await this.enabled(e);
if (this.applyPatches) await this.applyPatches();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
} else {
if (this.disabled) await this.disabled(e);
this.unpatch();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
}
}
@ -75,12 +74,14 @@ export default class BuiltinModule {
*/
patch(module, fnName, cb, when = 'after') {
if (!['before', 'after', 'instead'].includes(when)) when = 'after';
Patch(`BD:${this.moduleName}`, module)[when](fnName, cb.bind(this));
return Patch(`BD:${this.moduleName}`, module)[when](fnName, cb.bind(this));
}
childPatch(module, fnName, child, cb, when = 'after') {
const last = child.pop();
this.patch(module, fnName, (component, args, retVal) => {
this.patch(retVal[child[0]], child[1], cb, when);
const unpatch = this.patch(child.reduce((obj, key) => obj[key], retVal), last, function(...args) {unpatch(); return cb.call(this, component, ...args);}, when);
});
}

View File

@ -42,6 +42,10 @@ export default new class ColoredText extends BuiltinModule {
this.intensitySetting.off('setting-updated', this._intensityUpdated);
}
rerenderPatchedComponents() {
if (this.MessageContent) this.MessageContent.forceUpdateAll();
}
/* Methods */
_intensityUpdated() {
this.MessageContent.forceUpdateAll();
@ -50,16 +54,16 @@ export default new class ColoredText extends BuiltinModule {
/* Patches */
async applyPatches() {
if (this.patches.length) return;
this.MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons'));
this.MessageContent = await ReactComponents.getComponent('MessageContent');
this.patch(this.MessageContent.component.prototype, 'render', this.injectColoredText);
this.MessageContent.forceUpdateAll();
}
/**
* Set markup text colour to match role colour
*/
injectColoredText(thisObject, args, originalReturn) {
this.patch(originalReturn.props, 'children', function(obj, args, returnValue) {
const unpatch = this.patch(originalReturn.props, 'children', (obj, args, returnValue) => {
unpatch();
const { TinyColor } = Reflection.modules;
const markup = Utils.findInReactTree(returnValue, m => m && m.props && m.props.className && m.props.className.includes('da-markup'));
const roleColor = thisObject.props.message.colorString;

View File

@ -9,8 +9,8 @@
*/
import { Settings, Cache, Events } from 'modules';
import BuiltinModule from './BuiltinModule';
import { Reflection, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules';
import BuiltinModule from '../BuiltinModule';
import { Reflection, ReactComponents, DiscordApi, Security } from 'modules';
import { VueInjector, Modals, Toasts } from 'ui';
import { ClientLogger as Logger, ClientIPC } from 'common';
import { request } from 'vendor';
@ -172,7 +172,7 @@ export default new class E2EE extends BuiltinModule {
this.patch(Dispatcher, 'dispatch', this.dispatcherPatch, 'before');
this.patchMessageContent();
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', { selector: Reflection.resolve('channelTextArea', 'emojiButton').selector });
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea');
this.patchChannelTextArea(ChannelTextArea);
this.patchChannelTextAreaSubmit(ChannelTextArea);
ChannelTextArea.forceUpdateAll();
@ -236,12 +236,14 @@ export default new class E2EE extends BuiltinModule {
}
async patchMessageContent() {
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons'));
const MessageContent = await ReactComponents.getComponent('MessageContent');
this.patch(MessageContent.component.prototype, 'render', this.beforeRenderMessageContent, 'before');
this.patch(MessageContent.component.prototype, 'render', this.afterRenderMessageContent);
this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent);
MessageContent.forceUpdateAll();
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: Reflection.resolve('imageWrapper').selector });
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper');
this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before');
ImageWrapper.forceUpdateAll();
}
beforeRenderMessageContent(component) {
@ -285,10 +287,16 @@ export default new class E2EE extends BuiltinModule {
component.props.message.contentParsed = create.contentParsed;
}
afterRenderMessageContent(component, args, retVal) {
afterRenderMessageContent(component, _childrenObject, args, retVal) {
if (!component.props.message.bd_encrypted) return;
const buttons = Utils.findInReactTree(retVal, m => Array.isArray(m) && m[1].props && m[1].props.currentUserId);
const { className } = Reflection.resolve('buttonContainer', 'avatar', 'username');
const buttonContainer = Utils.findInReactTree(retVal, m => m && m.className && m.className.indexOf(className) !== -1);
if (!buttonContainer) return;
const buttons = buttonContainer.children.props.children;
if (!buttons) return;
try {
buttons.unshift(VueInjector.createReactElement(E2EEMessageButton));
} catch (err) {

View File

@ -45,7 +45,7 @@
import { E2EE } from 'builtin';
import { Settings, DiscordApi, Reflection } from 'modules';
import { Toasts } from 'ui';
import { MiLock, MiImagePlus, MiIcVpnKey } from '../ui/components/common/MaterialIcon';
import { MiLock, MiImagePlus, MiIcVpnKey } from 'commoncomponents';
export default {
components: {

View File

@ -17,7 +17,7 @@
</template>
<script>
import { MiLock } from '../ui/components/common/MaterialIcon';
import { MiLock } from 'commoncomponents';
export default {
components: {

View File

@ -0,0 +1,3 @@
export { default as default } from './E2EE';
export { default as E2EEComponent } from './E2EEComponent.vue';
export { default as E2EEMessageButton } from './E2EEMessageButton.vue';

View File

@ -9,18 +9,11 @@
*/
import { Settings } from 'modules';
import BuiltinModule from './BuiltinModule';
import EmoteModule from './EmoteModule';
import GlobalAc from '../ui/autocomplete';
import BuiltinModule from '../BuiltinModule';
import EmoteModule, { EMOTE_SOURCES } from './EmoteModule';
import GlobalAc from 'autocomplete';
import { BdContextMenu } from 'ui';
const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
]
export default new class EmoteAc extends BuiltinModule {
/* Getters */

View File

@ -8,15 +8,10 @@
* LICENSE file in the root directory of this source tree.
*/
import VrWrapper from '../ui/vrwrapper';
import VrWrapper from '../../ui/vrwrapper';
import { EMOTE_SOURCES } from '.';
import EmoteComponent from './EmoteComponent.vue';
const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
]
export default class Emote extends VrWrapper {
constructor(type, id, name) {

View File

@ -5,7 +5,7 @@
<script>
import { ClientLogger as Logger } from 'common';
import EmoteModule from './EmoteModule';
import { MiStar } from '../ui/components/common';
import { MiStar } from 'commoncomponents';
export default {
components: {

View File

@ -8,20 +8,17 @@
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import BuiltinModule from '../BuiltinModule';
import path from 'path';
import { request } from 'vendor';
import { Utils, FileUtils } from 'common';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { DiscordApi, Settings, Globals, Reflection, ReactComponents, Database } from 'modules';
import { DiscordContextMenu } from 'ui';
import Emote from './EmoteComponent.js';
import Autocomplete from '../ui/components/common/Autocomplete.vue';
import GlobalAc from '../ui/autocomplete';
const EMOTE_SOURCES = [
export const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
@ -131,6 +128,8 @@ export default new class EmoteModule extends BuiltinModule {
this.database.set(id, { id: emote.value.id || value, type });
}
Logger.log('EmoteModule', ['Loaded emote database']);
}
async loadUserData() {
@ -218,15 +217,18 @@ export default new class EmoteModule extends BuiltinModule {
async applyPatches() {
this.patchMessageContent();
this.patchSendAndEdit();
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: Reflection.resolve('imageWrapper').selector });
this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before');
this.patchSpoiler();
const MessageAccessories = await ReactComponents.getComponent('MessageAccessories');
this.patch(MessageAccessories.component.prototype, 'render', this.afterRenderMessageAccessories, 'after');
MessageAccessories.forceUpdateAll();
}
/**
* Patches MessageContent render method
*/
async patchMessageContent() {
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons'));
const MessageContent = await ReactComponents.getComponent('MessageContent');
this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent);
MessageContent.forceUpdateAll();
}
@ -240,10 +242,26 @@ export default new class EmoteModule extends BuiltinModule {
this.patch(MessageActions, 'editMessage', this.handleEditMessage, 'instead');
}
async patchSpoiler() {
const Spoiler = await ReactComponents.getComponent('Spoiler');
this.childPatch(Spoiler.component.prototype, 'render', ['props', 'children', 'props', 'children'], this.afterRenderSpoiler);
Spoiler.forceUpdateAll();
}
afterRenderSpoiler(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
filter.className.includes('inlineContent'));
if (!markup) return;
markup.children = this.processMarkup(markup.children);
}
/**
* Handle message render
*/
afterRenderMessageContent(component, args, retVal) {
afterRenderMessageContent(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
@ -256,13 +274,15 @@ export default new class EmoteModule extends BuiltinModule {
/**
* Handle send message
*/
async handleSendMessage(component, args, orig) {
async handleSendMessage(MessageActions, args, orig) {
if (!args.length) return orig(...args);
const { content } = args[1];
if (!content) return orig(...args);
Logger.log('EmoteModule', ['Sending message', MessageActions, args, orig]);
const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value &&
(DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES));
(DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.type === 'GROUP_DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES));
if (!emoteAsImage || content.split(' ').length > 1) {
args[1].content = args[1].content.split(' ').map(word => {
@ -271,7 +291,7 @@ export default new class EmoteModule extends BuiltinModule {
const emote = this.findByName(isEmote[1], true);
if (!emote) return word;
this.addToMostUsed(emote);
return emote ? `:${isEmote[1]}:` : word;
return emote ? `;${isEmote[1]};` : word;
}
return word;
}).join(' ');
@ -305,23 +325,27 @@ export default new class EmoteModule extends BuiltinModule {
if (!content) return orig(...args);
args[2].content = args[2].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word);
return isEmote ? `:${isEmote[1]}:` : word;
return isEmote ? `;${isEmote[1]};` : word;
}).join(' ');
return orig(...args);
}
/**
* Handle imagewrapper render
* Handle MessageAccessories render
*/
beforeRenderImageWrapper(component, args, retVal) {
if (!component.props || !component.props.src) return;
afterRenderMessageAccessories(component, args, retVal) {
if (!component.props || !component.props.message) return;
if (!component.props.message.attachments || component.props.message.attachments.length !== 1) return;
const src = component.props.original || component.props.src.split('?')[0];
if (!src || !src.includes('.bdemote.')) return;
const emoteName = src.split('/').pop().split('.')[0];
const emote = this.findByName(emoteName);
const filename = component.props.message.attachments[0].filename;
const match = filename.match(/([^/]*)\.bdemote\.(gif|png)$/i);
if (!match) return;
const emote = this.findByName(match[1]);
if (!emote) return;
retVal.props.children = emote.render();
emote.jumboable = true;
retVal.props.children[2] = emote.render();
}
/**
@ -339,14 +363,14 @@ export default new class EmoteModule extends BuiltinModule {
for (const child of markup) {
if (typeof child !== 'string') {
if (typeof child === 'object') {
const isEmoji = Utils.findInReactTree(child, 'emojiName');
if (isEmoji) child.props.children.props.jumboable = jumboable;
const isEmoji = Utils.findInReactTree(child, filter => filter && filter.emojiName);
if (isEmoji) isEmoji.jumboable = jumboable;
}
newMarkup.push(child);
continue;
}
if (!/:(\w+):/g.test(child)) {
if (!/;(\w+);/g.test(child)) {
newMarkup.push(child);
continue;
}
@ -355,7 +379,7 @@ export default new class EmoteModule extends BuiltinModule {
let s = '';
for (const word of words) {
const isemote = /:(.*?):/g.exec(word);
const isemote = /;(.*?);/g.exec(word);
if (!isemote) {
s += word;
continue;

View File

@ -0,0 +1,4 @@
export { default as EmoteModule, EMOTE_SOURCES } from './EmoteModule';
export { default as Emote } from './EmoteComponent';
export { default as EmoteComponent } from './EmoteComponent.vue';
export { default as EmoteAc } from './EmoteAc';

View File

@ -1,16 +1,19 @@
import { default as EmoteModule } from './EmoteModule';
import { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
import { default as VueDevtoolsModule } from './VueDevToolsModule';
import { default as TrackingProtection } from './TrackingProtection';
import { default as E2EE } from './E2EE';
import { default as ColoredText } from './ColoredText';
import { default as TwentyFourHour } from './24Hour';
import { default as KillClyde } from './KillClyde';
import { default as BlockedMessages } from './BlockedMessages';
import { default as VoiceDisconnect } from './VoiceDisconnect';
import { default as EmoteAc } from './EmoteAc';
import { EmoteModule, EmoteAc } from './Emotes';
import ReactDevtoolsModule from './ReactDevtoolsModule';
import VueDevtoolsModule from './VueDevToolsModule';
import TrackingProtection from './TrackingProtection';
import E2EE from './E2EE';
import ColoredText from './ColoredText';
import TwentyFourHour from './24Hour';
import KillClyde from './KillClyde';
import BlockedMessages from './BlockedMessages';
import VoiceDisconnect from './VoiceDisconnect';
export default class {
static get modules() {
return require('./builtin');
}
static initAll() {
EmoteModule.init();
ReactDevtoolsModule.init();

View File

@ -12,7 +12,7 @@ import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
export default new class E2EE extends BuiltinModule {
export default new class TrackingProtection extends BuiltinModule {
/* Getters */
get moduleName() { return 'TrackingProtection' }

View File

@ -1,4 +1,4 @@
export { default as EmoteModule } from './EmoteModule';
export { EmoteModule, EmoteAc } from './Emotes';
export { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
export { default as VueDevtoolsModule } from './VueDevToolsModule';
export { default as TrackingProtection } from './TrackingProtection';

View File

@ -8,17 +8,16 @@
* LICENSE file in the root directory of this source tree.
*/
import { DOM, BdUI, BdMenu, Modals, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
import { DOM, BdUI, BdMenu, Modals, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete } from 'ui';
import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache, Reflection, PackageInstaller } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
import { Events, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache, Reflection, PackageInstaller } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils, Axi } from 'common';
import { BuiltinManager, EmoteModule, ReactDevtoolsModule, VueDevtoolsModule, TrackingProtection, E2EE } from 'builtin';
import electron from 'electron';
import path from 'path';
import { setTimeout } from 'timers';
const tests = typeof PRODUCTION === 'undefined';
const ignoreExternal = false;
const ignoreExternal = tests && true;
class BetterDiscord {
@ -28,18 +27,18 @@ class BetterDiscord {
Logger.log('main', 'BetterDiscord starting');
this._bd = {
DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu, DiscordContextMenu,
DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete,
Events, CssEditor, Globals, Settings, Database, Updater,
Events, Globals, Settings, Database, Updater,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager, PackageInstaller,
Vendor,
Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi,
EmoteModule,
BuiltinManager, EmoteModule,
BdWebApi,
Connectivity,
Cache,
Logger, ClientIPC, Utils,
Logger, ClientIPC, Utils, Axi,
plugins: PluginManager.localContent,
themes: ThemeManager.localContent,

View File

@ -65,6 +65,10 @@ export default class Content extends AsyncEventEmitter {
get config() { return this.settings.categories }
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
get packed() { return this.dirName.packed }
get packagePath() { return this.dirName.packagePath }
get packageName() { return this.dirName.pkg }
/**
* Opens a settings modal for this content.
* @return {Modal}

View File

@ -220,6 +220,7 @@ export default class {
const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
asar.extractAll(packagePath, unpackedPath);
return this.preloadContent({
config,
contentPath: unpackedPath,
@ -228,8 +229,8 @@ export default class {
packageName,
packed: true
}, reload, index);
} catch (err) {
Logger.log('ContentManager', ['Error extracting packed content', err]);
throw err;
}
}
@ -322,12 +323,6 @@ export default class {
return content;
} catch (err) {
throw err;
} finally {
if (typeof dirName === 'object' && dirName.packed) {
rimraf(dirName.contentPath, err => {
if (err) Logger.err(err);
});
}
}
}
@ -353,6 +348,7 @@ export default class {
await unload;
await FileUtils.recursiveDeleteDirectory(content.paths.contentPath);
if (content.packed) await FileUtils.recursiveDeleteDirectory(content.packagePath);
return true;
} catch (err) {
Logger.err(this.moduleName, err);
@ -384,7 +380,7 @@ export default class {
if (this.unloadContentHook) this.unloadContentHook(content);
if (reload) return content.packed ? this.preloadPackedContent(content.packed.pkg, true, index) : this.preloadContent(content.dirName, true, index);
if (reload) return content.packed ? this.preloadPackedContent(content.packagePath, true, index) : this.preloadContent(content.dirName, true, index);
this.localContent.splice(index, 1);
} catch (err) {

View File

@ -38,6 +38,14 @@ export default new class {
ClientIPC.on('bd-get-scss', () => this.scss, true);
ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds));
ClientIPC.on('bd-editor-runScript', (e, script) => {
try {
new Function(script)();
e.reply('ok');
} catch (err) {
e.reply({ err: err.stack || err });
}
});
ClientIPC.on('bd-save-scss', async (e, scss) => {
await this.updateScss(scss);

View File

@ -126,6 +126,7 @@ export default class DiscordApi {
static get currentGuild() {
const guild = Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId());
if (guild) return Guild.from(guild);
return null;
}
/**
@ -135,6 +136,7 @@ export default class DiscordApi {
static get currentChannel() {
const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId());
if (channel) return Channel.from(channel);
return null;
}
/**
@ -144,6 +146,7 @@ export default class DiscordApi {
static get currentUser() {
const user = Modules.UserStore.getCurrentUser();
if (user) return User.from(user);
return null;
}
/**

View File

@ -0,0 +1,66 @@
/**
* BetterDiscord Editor Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Module from './imodule';
import { DOM } from 'ui';
export default new class extends Module {
get name() { return 'Editor' }
get delay() { return false; }
setInitialState(state) {
return {
editorBounds: undefined
};
}
initialize() {
super.initialize();
(async () => {
try {
// TODO this is temporary
const userScss = await this.send('readDataFile', 'user.scss');
const compiled = await this.send('compileSass', { data: userScss });
this.injectStyle('customcss', compiled.css.toString());
} catch (err) {
console.warn('SCSS Compilation error', err);
}
})();
}
events(ipc) {
ipc.on('editor-runScript', (e, script) => {
try {
new Function(script)();
e.reply('ok');
} catch (err) {
e.reply({ err: err.stack || err });
}
});
ipc.on('editor-injectStyle', (e, { id, style }) => {
this.injectStyle(id, style);
e.reply('ok');
});
}
injectStyle(id, style) {
return DOM.injectStyle(style, `userstyle-${id}`);
}
/**
* Show editor, flashes if already visible.
*/
async show() {
await this.send('editor-open', this.state.editorBounds);
}
}

View File

@ -36,10 +36,6 @@ export default new class extends Module {
async first() {
const config = await ClientIPC.send('getConfig');
config.paths.push({
id: 'tmp',
path: path.join(config.paths.find(p => p.id === 'base').path, 'tmp')
});
this.setState({ config });
// This is for Discord to stop error reporting :3
@ -102,7 +98,7 @@ export default new class extends Module {
}
get version() {
return this.config.version;
return this.config.versions.core;
}
}

View File

@ -0,0 +1,73 @@
/**
* BetterDiscord Module Base
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Base Module that every non-static module should extend
*/
import { ClientLogger as Logger, ClientIPC } from 'common';
export default class Module {
constructor(args) {
this.__ = {
state: args || {},
args
};
this.setState = this.setState.bind(this);
if (this.delay) { // If delay is set then module is set to load delayed from modulemanager
this.initialize = this.initialize.bind(this);
this.init = this.initialize;
} else {
this.initialize();
this.init = () => { };
}
}
initialize() {
if (this.bindings) this.bindings();
if (this.setInitialState) this.setState(this.setInitialState(this.state));
if (this.events) this.events(ClientIPC);
}
setState(newState) {
const oldState = Object.assign({}, this.state);
Object.assign(this.state, newState);
if (this.stateChanged) this.stateChanged(oldState, newState);
}
set args(t) { }
get args() { return this.__.args; }
set state(state) { return this.__.state = state; }
get state() { return this.__.state; }
async send(channel, message) {
return ClientIPC.send(channel, message);
}
log(msg) {
Logger.log(this.name, msg);
}
warn(msg) {
Logger.log(this.name, msg);
}
err(msg) {
Logger.log(this.name, msg);
}
info(msg) {
Logger.log(this.name, msg);
}
}

View File

@ -9,7 +9,7 @@
*/
import { ClientLogger as Logger } from 'common';
import { SocketProxy, EventHook, CssEditor } from 'modules';
import { SocketProxy, EventHook } from 'modules';
import { ProfileBadges, ClassNormaliser } from 'ui';
import Updater from './updater';
@ -27,7 +27,6 @@ export default class {
new ClassNormaliser(),
new SocketProxy(),
new EventHook(),
CssEditor,
Updater
]);
}

View File

@ -1,5 +1,6 @@
export { default as Events } from './events';
export { default as CssEditor } from './csseditor';
export { default as Editor } from './editor';
export { default as Globals } from './globals';
export { default as Settings } from './settings';
export { default as Database } from './database';

View File

@ -6,23 +6,25 @@ import rimraf from 'rimraf';
import { request } from 'vendor';
import { Modals } from 'ui';
import { Utils } from 'common';
import { Utils, FileUtils } from 'common';
import PluginManager from './pluginmanager';
import Globals from './globals';
import Security from './security';
import { ReactComponents } from './reactcomponents';
import Reflection from './reflection';
import DiscordApi from './discordapi';
import ThemeManager from './thememanager';
import { MonkeyPatch } from './patcher';
import { DOM } from 'ui';
export default class PackageInstaller {
/**
* Handler for drag and drop package install
* @param {String} filePath Path to local file
* @param {String} channelId Current channel id
* @param {Boolean} canUpload If the user can upload files in current window
* @returns {Number} returns action code from modal
*/
static async dragAndDropHandler(filePath, channelId) {
static async dragAndDropHandler(filePath, canUpload) {
try {
const config = JSON.parse(asar.extractFile(filePath, 'config.json').toString());
const { info, main } = config;
@ -36,12 +38,8 @@ export default class PackageInstaller {
const isPlugin = info.type && info.type === 'plugin' || main.endsWith('.js');
// Show install modal
const modalResult = await Modals.installModal(isPlugin ? 'plugin' : 'theme', config, filePath, icon).promise;
if (modalResult === 0) {
// Upload it instead
}
const modalResult = await Modals.installModal(isPlugin ? 'plugin' : 'theme', config, filePath, icon, canUpload).promise;
return modalResult;
} catch (err) {
console.log(err);
}
@ -86,15 +84,10 @@ export default class PackageInstaller {
await oldContent.unload(true);
if (oldContent.packed && oldContent.packed.packageName !== nameOrId) {
rimraf(oldContent.packed.packagePath, err => {
if (err) throw err;
});
} else {
rimraf(oldContent.contentPath, err => {
if (err) throw err;
});
if (oldContent.packed && oldContent.packageName !== nameOrId) {
await FileUtils.deleteFile(oldContent.packagePath).catch(err => null);
}
await FileUtils.recursiveDeleteDirectory(oldContent.contentPath).catch(err => null);
return manager.preloadPackedContent(outputName);
} catch (err) {
@ -135,33 +128,51 @@ export default class PackageInstaller {
}
}
static async handleDrop(stateNode, e, original) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return original && original.call(stateNode, e);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (stateNode) stateNode.clearDragging();
const currentChannel = DiscordApi.currentChannel;
const canUpload = currentChannel ?
currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.SEND_MESSAGES) &&
currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.ATTACH_FILES) : false;
const files = Array.from(e.dataTransfer.files).slice(0);
const actionCode = await this.dragAndDropHandler(e.dataTransfer.files[0].path, canUpload);
if (actionCode === 0 && stateNode) stateNode.promptToUpload(files, currentChannel.id, true, !e.shiftKey);
}
/**
* Patches Discord upload area for .bd files
*/
static async uploadAreaPatch() {
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', { selector });
static async uploadAreaPatch(UploadArea) {
// Add a listener to root for when not in a channel
const root = DOM.getElement('#app-mount');
const rootHandleDrop = this.handleDrop.bind(this, undefined);
root.addEventListener('drop', rootHandleDrop);
const reflect = Reflection.DOM(selector);
const stateNode = reflect.getComponentStateNode(this.UploadArea);
const callback = function (e) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
stateNode.clearDragging();
const unpatchUploadAreaHandleDrop = MonkeyPatch('BD:ReactComponents', UploadArea.component.prototype).instead('handleDrop', (component, [e], original) => this.handleDrop(component, e, original));
PackageInstaller.dragAndDropHandler(e.dataTransfer.files[0].path, DiscordApi.currentChannel.id);
this.unpatchUploadArea = () => {
unpatchUploadAreaHandleDrop();
root.removeEventListener('drop', rootHandleDrop);
this.unpatchUploadArea = undefined;
};
// Remove their handler, add ours, then read theirs to give ours priority to stop theirs when we get a .bd file.
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
for (const element of document.querySelectorAll(UploadArea.important.selector)) {
const stateNode = Reflection.DOM(element).getComponentStateNode(UploadArea);
this.unpatchUploadArea = function () {
reflect.element.removeEventListener('drop', callback);
};
element.removeEventListener('drop', stateNode.handleDrop);
stateNode.handleDrop = UploadArea.component.prototype.handleDrop.bind(stateNode);
element.addEventListener('drop', stateNode.handleDrop);
stateNode.forceUpdate();
}
}
}

View File

@ -109,21 +109,9 @@ export default class extends ContentManager {
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
const instance = new plugin({
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: packed ? packed.packageName : paths.dirName,
mainPath: paths.mainPath
}
configs, info, main, paths
});
if (packed) instance.packed = {
pkg: packed.pkg,
packageName: packed.packageName,
packagePath: packed.packagePath,
packed: true
}; else instance.packed = false;
if (instance.enabled && this.loaded) {
instance.userConfig.enabled = false;
instance.start(false);

View File

@ -173,9 +173,18 @@ class ReactComponent {
this.important = important;
}
get elements() {
if (!this.important || !this.important.selector) return [];
return document.querySelectorAll(this.important.selector);
}
get stateNodes() {
return [...this.elements].map(e => Reflection.DOM(e).getComponentStateNode(this));
}
forceUpdateAll() {
if (!this.important || !this.important.selector) return;
for (const e of document.querySelectorAll(this.important.selector)) {
for (const e of this.elements) {
Reflection.DOM(e).forceUpdate(this);
}
}
@ -186,6 +195,7 @@ export class ReactComponents {
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) }
static get listeners() { return this._listeners || (this._listeners = []) }
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static get componentAliases() { return this._componentAliases || (this._componentAliases = []) }
static get ReactComponent() { return ReactComponent }
@ -222,6 +232,8 @@ export class ReactComponents {
* @return {Promise => ReactComponent}
*/
static async getComponent(name, important, filter) {
name = this.getComponentName(name);
const have = this.components.find(c => c.id === name);
if (have) return have;
@ -239,7 +251,13 @@ export class ReactComponents {
let component, reflect;
for (const element of elements) {
reflect = Reflection.DOM(element);
component = filter ? reflect.components.find(filter) : reflect.component;
component = filter ? reflect.components.find(component => {
try {
return filter.call(undefined, component);
} catch (err) {
return false;
}
}) : reflect.component;
if (component) break;
}
@ -276,6 +294,19 @@ export class ReactComponents {
});
}
static getComponentName(name) {
const resolvedAliases = [];
while (this.componentAliases[name]) {
resolvedAliases.push(name);
name = this.componentAliases[name];
if (resolvedAliases.includes(name)) break;
}
return name;
}
static setName(name, filter) {
const have = this.components.find(c => c.id === name);
if (have) return have;
@ -351,6 +382,21 @@ export class ReactAutoPatcher {
this.Message.forceUpdateAll();
}
static async patchMessageContent() {
const { selector } = Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited');
this.MessageContent = await ReactComponents.getComponent('MessageContent', {selector}, c => c.defaultProps && c.defaultProps.hasOwnProperty('disableButtons'));
}
static async patchSpoiler() {
const { selector } = Reflection.resolve('spoilerText', 'spoilerContainer');
this.Spoiler = await ReactComponents.getComponent('Spoiler', {selector}, c => c.prototype.renderSpoilerText);
}
static async patchMessageAccessories() {
const { selector } = Reflection.resolve('container', 'containerCozy', 'embedWrapper');
this.MessageAccessories = await ReactComponents.getComponent('MessageAccessories', {selector});
}
static async patchMessageGroup() {
const { selector } = Reflection.resolve('container', 'message', 'messageCozy');
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector});
@ -369,7 +415,16 @@ export class ReactAutoPatcher {
this.MessageGroup.forceUpdateAll();
}
static async patchImageWrapper() {
ReactComponents.componentAliases.ImageWrapper = 'Image';
const { selector } = Reflection.resolve('imageWrapper');
this.ImageWrapper = await ReactComponents.getComponent('ImageWrapper', {selector}, c => typeof c.defaultProps.children === 'function');
}
static async patchChannelMember() {
ReactComponents.componentAliases.ChannelMember = 'MemberListItem';
const { selector } = Reflection.resolve('member', 'memberInner', 'activity');
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', {selector}, m => m.prototype.renderActivity);
@ -385,8 +440,13 @@ export class ReactAutoPatcher {
this.ChannelMember.forceUpdateAll();
}
static async patchNameTag() {
const { selector } = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon');
this.NameTag = await ReactComponents.getComponent('NameTag', {selector});
}
static async patchGuild() {
const selector = `div.${Reflection.resolve('guild', 'guildsWrapper').className}:not(:first-child)`;
const selector = `div.${Reflection.resolve('container', 'guildIcon', 'selected', 'unread').className}:not(:first-child)`;
this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.prototype.renderBadge);
this.unpatchGuild = MonkeyPatch('BD:ReactComponents', this.Guild.component.prototype).after('render', (component, args, retVal) => {
@ -403,7 +463,7 @@ export class ReactAutoPatcher {
* The Channel component contains the header, message scroller, message form and member list.
*/
static async patchChannel() {
const selector = '.chat';
const { selector } = Reflection.resolve('chat', 'title', 'channelName');
this.Channel = await ReactComponents.getComponent('Channel', {selector});
this.unpatchChannel = MonkeyPatch('BD:ReactComponents', this.Channel.component.prototype).after('render', (component, args, retVal) => {
@ -419,10 +479,17 @@ export class ReactAutoPatcher {
this.Channel.forceUpdateAll();
}
static async patchChannelTextArea() {
const { selector } = Reflection.resolve('channelTextArea', 'autocomplete');
this.ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector});
}
/**
* The GuildTextChannel component represents a text channel in the guild channel list.
*/
static async patchGuildTextChannel() {
ReactComponents.componentAliases.GuildTextChannel = 'TextChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel', {selector}, c => c.prototype.renderMentionBadge);
@ -435,6 +502,8 @@ export class ReactAutoPatcher {
* The GuildVoiceChannel component represents a voice channel in the guild channel list.
*/
static async patchGuildVoiceChannel() {
ReactComponents.componentAliases.GuildVoiceChannel = 'VoiceChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildVoiceChannel = await ReactComponents.getComponent('GuildVoiceChannel', {selector}, c => c.prototype.handleVoiceConnect);
@ -447,7 +516,9 @@ export class ReactAutoPatcher {
* The DirectMessage component represents a channel in the direct messages list.
*/
static async patchDirectMessage() {
const selector = '.channel.private';
ReactComponents.componentAliases.DirectMessage = 'PrivateChannel';
const { selector } = Reflection.resolve('channel', 'avatar', 'name');
this.DirectMessage = await ReactComponents.getComponent('DirectMessage', {selector}, c => c.prototype.renderAvatar);
this.unpatchDirectMessage = MonkeyPatch('BD:ReactComponents', this.DirectMessage.component.prototype).after('render', this._afterChannelRender);
@ -469,15 +540,18 @@ export class ReactAutoPatcher {
}
static async patchUserProfileModal() {
ReactComponents.componentAliases.UserProfileModal = 'UserProfileBody';
const { selector } = Reflection.resolve('root', 'topSectionNormal');
this.UserProfileModal = await ReactComponents.getComponent('UserProfileModal', {selector}, Filters.byPrototypeFields(['renderHeader', 'renderBadges']));
this.UserProfileModal = await ReactComponents.getComponent('UserProfileModal', {selector}, c => c.prototype.renderHeader && c.prototype.renderBadges);
this.unpatchUserProfileModal = MonkeyPatch('BD:ReactComponents', this.UserProfileModal.component.prototype).after('render', (component, args, retVal) => {
const root = retVal.props.children[0] || retVal.props.children;
const { user } = component.props;
if (!user) return;
retVal.props['data-user-id'] = user.id;
if (user.bot) retVal.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
root.props['data-user-id'] = user.id;
if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
});
this.UserProfileModal.forceUpdateAll();
@ -485,24 +559,28 @@ export class ReactAutoPatcher {
static async patchUserPopout() {
const { selector } = Reflection.resolve('userPopout', 'headerNormal');
this.UserPopout = await ReactComponents.getComponent('UserPopout', {selector});
this.UserPopout = await ReactComponents.getComponent('UserPopout', {selector}, c => c.prototype.renderHeader);
this.unpatchUserPopout = MonkeyPatch('BD:ReactComponents', this.UserPopout.component.prototype).after('render', (component, args, retVal) => {
const root = retVal.props.children[0] || retVal.props.children;
const { user, guild, guildMember } = component.props;
if (!user) return;
retVal.props['data-user-id'] = user.id;
if (user.bot) retVal.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
if (guild) retVal.props['data-guild-id'] = guild.id;
if (guild && user.id === guild.ownerId) retVal.props.className += ' bd-isGuildOwner';
if (guild && guildMember) retVal.props.className += ' bd-isGuildMember';
if (guildMember && guildMember.roles.length) retVal.props.className += ' bd-hasRoles';
root.props['data-user-id'] = user.id;
if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
if (guild) root.props['data-guild-id'] = guild.id;
if (guild && user.id === guild.ownerId) root.props.className += ' bd-isGuildOwner';
if (guild && guildMember) root.props.className += ' bd-isGuildMember';
if (guildMember && guildMember.roles.length) root.props.className += ' bd-hasRoles';
});
this.UserPopout.forceUpdateAll();
}
static async patchUploadArea() {
PackageInstaller.uploadAreaPatch();
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', {selector});
PackageInstaller.uploadAreaPatch(this.UploadArea);
}
}

View File

@ -288,7 +288,7 @@ class Module {
if (this._require) return this._require;
const __webpack_require__ = this.getWebpackRequire();
if (!__webpack_require__) return;
if (!__webpack_require__) return null;
this.hookWebpackRequireCache(__webpack_require__);
return this._require = __webpack_require__;

View File

@ -272,7 +272,7 @@ class WebpackModules {
if (this._require) return this._require;
const __webpack_require__ = this.getWebpackRequire();
if (!__webpack_require__) return;
if (!__webpack_require__) return null;
this.hookWebpackRequireCache(__webpack_require__);
return this._require = __webpack_require__;

View File

@ -69,10 +69,11 @@ export default class Theme extends Content {
path: this.paths.mainPath.replace(/\\/g, '/')
});
// Why are these getters?
Logger.log(this.name, ['Finished compiling theme', new class Info {
get SCSS_variables() { console.log(config); }
get Compiled_SCSS() { console.log(result.css.toString()); }
get Result() { console.log(result); }
get SCSS_variables() { console.log(config); return ''; }
get Compiled_SCSS() { console.log(result.css.toString()); return ''; }
get Result() { console.log(result); return ''; }
}]);
return {
@ -146,6 +147,11 @@ export default class Theme extends Content {
* @param {Array} files Files to watch
*/
set watchfiles(files) {
if (this.packed) {
// Don't watch files for packed themes
return;
}
if (!files) files = [];
for (const file of files) {

View File

@ -36,12 +36,7 @@ export default class ThemeManager extends ContentManager {
static async loadTheme(paths, configs, info, main) {
try {
const instance = new Theme({
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,
mainPath: paths.mainPath
}
configs, info, main, paths
});
if (instance.enabled) {
instance.userConfig.enabled = false;

View File

@ -8,81 +8,183 @@
* LICENSE file in the root directory of this source tree.
*/
import Events from './events';
import Globals from './globals';
import { ClientLogger as Logger } from 'common';
import request from 'request-promise-native';
import { Notifications } from 'ui';
import { Reflection, Globals } from 'modules';
export default new class {
import Events from './events';
import Module from './imodule';
export default new class extends Module {
get updates() { return this.state.updates }
get bdUpdates() { return this.state.updates.bd }
get error() { return null; }
get updatesAvailable() { return this.state.updatesAvailable; }
constructor() {
this.updatesAvailable = false;
this.latestVersion = undefined;
this.error = undefined;
this.init = this.init.bind(this);
this.checkForUpdates = this.checkForUpdates.bind(this);
super({
updatesAvailable: false,
error: null,
updates: { bd: [] },
updating: false
});
}
/**
* The interval to wait before checking for updates.
*/
get interval() {
return 60 * 1000 * 30;
bindings() {
this.restartNotif = this.restartNotif.bind(this);
this.reloadNotif = this.reloadNotif.bind(this);
this.startUpdate = this.startUpdate.bind(this);
this.setUpdateStatus = this.setUpdateStatus.bind(this);
this.testUi = this.testUi.bind(this);
}
init() {
this.updateInterval = setInterval(this.checkForUpdates, this.interval);
restartNotif() {
Notifications.add('Updates Finished!', 'Restart required.', [
{
text: 'Restart Later',
onClick: () => { setTimeout(this.restartNotif, 5 * 60000); return true; }
},
{
text: 'Restart Now',
onClick: () => {
try {
const { remote } = Globals.require('electron');
window.close();
Reflection.module.byProps('showToken', 'hideToken').showToken();
remote.app.relaunch();
remote.app.exit(0);
} catch (err) {
console.err(err);
return true;
}
}
}
]);
}
/**
* Installs an update.
* TODO
*/
async update() {
try {
await new Promise(resolve => setTimeout(resolve, 5000));
this.updatesAvailable = false;
this.latestVersion = Globals.version;
Events.emit('update-check-end');
} catch (err) {
this.error = err;
this.checkForUpdates();
throw err;
}
reloadNotif() {
Notifications.add('Updates Finished!', 'Reload required.', [
{
text: 'Reload Later',
onClick: () => { setTimeout(this.reloadNotif, 5 * 60000); return true; }
},
{
text: 'Reload Now',
onClick: () => {
document.location.reload();
}
}
]);
}
/**
* Checks for updates.
* @return {Promise}
*/
async checkForUpdates() {
if (this.updatesAvailable) return true;
Events.emit('update-check-start');
Logger.info('Updater', 'Checking for updates');
events(ipc) {
ipc.on('updater-checkForUpdates', () => {
if (this.state.updating) return; // We're already updating. Updater should be paused anyways at this point.
Events.emit('update-check-start');
});
try {
const response = await request({
uri: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
json: true
ipc.on('updater-noUpdates', () => {
if (this.state.updatesAvailable) return; // If for some reason we get this even though we have updates already.
this.setState({
updatesAvailable: false,
updates: {}
});
});
this.latestVersion = response.version;
Events.emit('update-check-end');
Logger.info('Updater', `Latest Version: ${response.version} - Current Version: ${Globals.version}`);
ipc.on('updater-updatesAvailable', (_, updates) => {
console.log(updates);
if (this.state.updating) return; // If for some reason we get more updates when we're already updating
updates.bd = updates.bd.map(update => {
update.text = `${update.id.charAt(0).toUpperCase()}${update.id.slice(1)}`;
update.hint = `Current: ${update.currentVersion} | Latest: ${update.version}`;
update.status = {
update: true,
updating: false,
updated: false,
error: null
};
if (this.latestVersion !== Globals.version) {
this.updatesAvailable = true;
Events.emit('updates-available');
return true;
return update;
});
this.setState({
updates,
updatesAvailable: true
});
});
ipc.on('updater-updated', (_, info) => {
const { reloadRequired, restartRequired } = info;
if (restartRequired) {
this.restartNotif();
return;
}
return false;
} catch (err) {
Events.emit('update-check-fail', err);
throw err;
if (reloadRequired) {
this.reloadNotif();
return;
}
});
ipc.on('updater-updateFinished', (_, update) => {
this.setUpdateStatus(update.id, 'updated', true);
});
ipc.on('updater-updateError', (_, update) => {
this.setUpdateStatus(update.id, 'error', update.error);
});
}
stateChanged(oldState, newState) {
if (!newState.updatesAvailable) return Events.emit('update-check-end');
if (!oldState.updatesAvailable && newState.updatesAvailable) {
Events.emit('updates-available');
Notifications.add('', 'Updates Available!', [
{
text: 'Ignore',
onClick: () => { return true; }
},
{
text: 'Show Updates',
onClick: () => {
Events.emit('bd-open-menu', 'updater');
return true;
}
}
]);
}
}
setUpdateStatus(updateId, statusChild, statusValue) {
for (const u of this.bdUpdates) {
if (u.id === updateId) {
u.status[statusChild] = statusValue;
return;
}
}
}
toggleUpdate(update) {
update.status.update = !update.status.update;
}
async startUpdate() {
console.log('start update');
const updates = { bd: [] };
for (const update of this.bdUpdates) {
if (update.status.update) {
update.status.updating = true;
updates.bd.push(update);
}
}
console.log(updates);
this.send('updater-startUpdate', updates);
}
testUi(updates) {
this.setState({
updates,
updatesAvailable: true
});
}
}

View File

@ -11,6 +11,7 @@
import jQuery from 'jquery';
import lodash from 'lodash';
import Vue from 'vue';
import { Axi } from 'common';
import request from 'request-promise-native';
@ -40,6 +41,8 @@ export default class {
*/
static get Vue() { return Vue }
static get axios() { return Axi.axios }
static get request() { return request }
static get Combokeys() { return Combokeys }

View File

@ -179,7 +179,7 @@ export class PermissionOverwrite {
}
get guild() {
if (this.channel) return this.channel.guild;
return this.channel ? this.channel.guild : null;
}
}
@ -187,7 +187,7 @@ export class RolePermissionOverwrite extends PermissionOverwrite {
get roleId() { return this.discordObject.id }
get role() {
if (this.guild) return this.guild.roles.find(r => r.id === this.roleId);
return this.guild ? this.guild.roles.find(r => r.id === this.roleId) : null;
}
}

View File

@ -166,14 +166,14 @@ export class Guild {
* The guild's AFK channel.
*/
get afkChannel() {
if (this.afkChannelId) return Channel.fromId(this.afkChannelId);
return this.afkChannelId ? Channel.fromId(this.afkChannelId) : null;
}
/**
* The channel system messages are sent to.
*/
get systemChannel() {
if (this.systemChannelId) return Channel.fromId(this.systemChannelId);
return this.systemChannelId ? Channel.fromId(this.systemChannelId) : null;
}
/**

View File

@ -42,11 +42,11 @@ export class Reaction {
}
get message() {
if (this.channel) return this.channel.messages.find(m => m.id === this.messageId);
return this.channel ? this.channel.messages.find(m => m.id === this.messageId) : null;
}
get guild() {
if (this.channel) return this.channel.guild;
return this.channel ? this.channel.guild : null;
}
}
@ -84,11 +84,11 @@ export class Embed {
}
get message() {
if (this.channel) return this.channel.messages.find(m => m.id === this.messageId);
return this.channel ? this.channel.messages.find(m => m.id === this.messageId) : null;
}
get guild() {
if (this.channel) return this.channel.guild;
return this.channel ? this.channel.guild : null;
}
}
@ -143,6 +143,7 @@ export class Message {
get author() {
if (this.discordObject.author && !this.webhookId) return User.from(this.discordObject.author);
return null;
}
get channel() {
@ -150,7 +151,7 @@ export class Message {
}
get guild() {
if (this.channel) return this.channel.guild;
return this.channel ? this.channel.guild : null;
}
/**
@ -202,7 +203,7 @@ export class DefaultMessage extends Message {
get application() { return this.discordObject.application }
get webhook() {
if (this.webhookId) return this.discordObject.author;
return this.webhookId ? this.discordObject.author : null;
}
get mentions() {

View File

@ -77,7 +77,7 @@ export class User {
get note() {
const note = Modules.UserNoteStore.getNote(this.id);
if (note) return note;
return note ? note : null;
}
/**

View File

@ -1,122 +1,125 @@
.bd-settingsButton {
position: absolute;
z-index: 1;
top: 22px;
width: 70px;
height: 48px;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .2), 0 2px 0 rgba(0, 0, 0, .06);
opacity: 1;
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 27px;
}
.platform-linux & { // sass-lint:disable-line class-name-format
top: 0;
}
.bd-settingsButtonBtn {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
width: 70px;
position: absolute;
z-index: 1;
top: 22px;
width: 70px;
height: 48px;
cursor: pointer;
filter: grayscale(100%);
opacity: .5;
position: relative;
transition: all .3s cubic-bezier(.4,0,0,1);
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .2), 0 2px 0 rgba(0, 0, 0, .06);
opacity: 1;
&::before,
&::after {
content:"";
display: block;
position: absolute;
top:0;
left:0;
background-repeat: no-repeat;
background-position: center;
}
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 27px;
}
&::before {
width: 70px;
height: 48px;
background-image: $logoSmallBw;
background-size: 50% 50%;
opacity: 1;
transition:all .3s cubic-bezier(.4,0,0,1), opacity .01s;
}
&::after {
width: 130px;
height: 43px;
background-image: $logoBigBw;
background-size: 100% 100%;
transform:translate(-7px,2px)scale(0.5);
opacity: 0;
transition:all .3s cubic-bezier(.4,0,0,1);
}
.platform-linux & { // sass-lint:disable-line class-name-format
top: 0;
}
&:not(.bd-loading) {
&:hover {
filter: none;
opacity: 1;
}
}
.bd-settingsButtonBtn {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
width: 70px;
height: 48px;
cursor: pointer;
filter: grayscale(100%);
opacity: .5;
position: relative;
transition: all .3s cubic-bezier(.4, 0, 0, 1);
&.bd-loading {
animation: bd-settingsButtonPulse 1.5s infinite;
}
&::before,
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
background-repeat: no-repeat;
background-position: center;
}
&.bd-updates {
filter: hue-rotate(250deg) !important; // sass-lint:disable-line no-important
opacity: 1 !important; // sass-lint:disable-line no-important
}
}
&::before {
width: 70px;
height: 48px;
background-image: $logoSmallBw;
background-size: 50% 50%;
opacity: 1;
transition: all .3s cubic-bezier(.4, 0, 0, 1), opacity .01s;
}
&.bd-hideButton {
animation: bd-fadeOut .4s ease-out;
&::after {
width: 130px;
height: 43px;
background-image: $logoBigBw;
background-size: 100% 100%;
transform: translate(-7px, 2px) scale(.5);
opacity: 0;
transition: all .3s cubic-bezier(.4, 0, 0, 1);
}
&.bd-active {
animation: bd-fadeIn .4s ease-in;
}
&:not(.bd-loading) {
&:hover {
filter: none;
opacity: 1;
}
}
&:not(.bd-active) {
&:not(.bd-animating) {
display: none;
}
}
}
&.bd-loading {
animation: bd-settingsButtonPulse 1.5s infinite;
}
&.bd-active {
opacity: 1;
}
&.bd-updates {
filter: hue-rotate(250deg) !important; // sass-lint:disable-line no-important
opacity: 1 !important; // sass-lint:disable-line no-important
}
}
&.bd-active,
&.bd-hideButton {
background: transparent;
box-shadow: none;
&.bd-hideButton {
animation: bd-fadeOut .4s ease-out;
.bd-settingsButtonBtn {
filter: none;
opacity: 1;
width: 130px;
height: 43px;
transform:translate(25px,18px);
cursor: default;
&::before{
opacity:0;
transform:translate(-16px,-3px)scale(1.9);
transition:all .3s cubic-bezier(.4,0,0,1), opacity .1s .3s;
}
&::after{
opacity: 1;
transform:scale(1);
}
}
}
&.bd-active {
animation: bd-fadeIn .4s ease-in;
}
&.bd-active,
&.bd-animating {
z-index: 3001;
}
&:not(.bd-active) {
&:not(.bd-animating) {
display: none;
}
}
}
&.bd-active {
opacity: 1;
}
&.bd-active,
&.bd-hideButton {
background: transparent;
box-shadow: none;
.bd-settingsButtonBtn {
filter: none;
opacity: 1;
width: 130px;
height: 43px;
transform: translate(25px, 18px);
cursor: default;
&::before {
opacity: 0;
transform: translate(-16px, -3px) scale(1.9);
transition: all .3s cubic-bezier(.4, 0, 0, 1), opacity .1s .3s;
}
&::after {
opacity: 1;
transform: scale(1);
}
}
}
&.bd-active,
&.bd-animating {
z-index: 3001;
}
}

View File

@ -0,0 +1,9 @@
.bd-contentColumn .bd-devview {
display: grid;
grid-template-columns: 33% 33% 33%;
.bd-button {
font-size: 10px;
height: 20px;
}
}

View File

@ -10,3 +10,4 @@
@import './kvp';
@import './collection';
@import './e2ee';
@import './devview';

View File

@ -1,180 +1,180 @@
.bd-settings {
position: absolute;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
top: 22px;
left: 0;
bottom: 0;
z-index: 1001;
width: 310px;
transform: translateX(-310px);
opacity: 0;
transition: transform .3s cubic-bezier(.4,0,0,1), opacity .25s ease;
pointer-events: none;
position: absolute;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
top: 22px;
left: 0;
bottom: 0;
z-index: 1001;
width: 310px;
transform: translateX(-310px);
opacity: 0;
transition: transform .3s cubic-bezier(.4, 0, 0, 1), opacity .25s ease;
pointer-events: none;
&.bd-active {
width: 900px;
transform: none;
opacity: 1;
}
&.bd-active {
width: 900px;
transform: none;
opacity: 1;
}
.bd-settingsX {
position: absolute;
top: 18px;
left: 255px;
border: 2px solid #6e6e6e;
border-radius: 50%;
width: 25px;
height: 25px;
justify-content: center;
display: flex;
align-items: center;
cursor: pointer;
.bd-settingsX {
position: absolute;
top: 18px;
left: 255px;
border: 2px solid #6e6e6e;
border-radius: 50%;
width: 25px;
height: 25px;
justify-content: center;
display: flex;
align-items: center;
cursor: pointer;
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 43px;
}
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 43px;
}
.bd-xText {
color: #72767d;
position: absolute;
top: 32px;
font-weight: 600;
font-size: 13px;
}
.bd-xText {
color: #72767d;
position: absolute;
top: 32px;
font-weight: 600;
font-size: 13px;
}
.bd-materialDesignIcon {
justify-content: center;
display: flex;
fill: #72767d;
}
.bd-materialDesignIcon {
justify-content: center;
display: flex;
fill: #72767d;
}
&:hover {
background-color: hsla(218, 5%, 47%, .3);
&:hover {
background-color: hsla(218, 5%, 47%, .3);
.bd-materialDesignIcon {
fill: #fff;
}
}
}
.bd-materialDesignIcon {
fill: #fff;
}
}
}
.bd-info {
display: flex;
align-items: flex-end;
overflow: hidden;
padding: 0 25px;
margin: 10px 0;
.bd-info {
display: flex;
align-items: flex-end;
overflow: hidden;
padding: 0 25px;
margin: 10px 0;
.bd-vtext {
color: #414245;
font-weight: 700;
font-size: 12px;
flex-grow: 1;
height: 20px;
cursor: default;
user-select: none;
}
.bd-vtext {
color: #414245;
font-weight: 700;
font-size: 12px;
flex-grow: 1;
height: 20px;
cursor: default;
user-select: none;
}
.bd-materialButton {
cursor: pointer;
.bd-materialButton {
cursor: pointer;
&:hover {
.bd-materialDesignIcon {
fill: #fff;
}
}
}
&:hover {
.bd-materialDesignIcon {
fill: #fff;
}
}
}
.bd-materialDesignIcon {
fill: #414245;
.bd-materialDesignIcon {
fill: #414245;
&:hover {
fill: #fff;
}
}
}
&:hover {
fill: #fff;
}
}
}
.bd-sidebarView {
&::after {
content: '';
height: 100%;
width: 310px;
background-color: #202225;
top: 100%;
display: block;
position: absolute;
}
.bd-sidebarView {
&::after {
content: '';
height: 100%;
width: 310px;
background-color: #202225;
top: 100%;
display: block;
position: absolute;
}
.bd-sidebarRegion {
.bd-scroller {
padding-top: 0;
}
}
.bd-sidebarRegion {
.bd-scroller {
padding-top: 0;
}
}
.bd-contentRegion {
width: 590px;
}
.bd-contentRegion {
width: 590px;
}
&.bd-active {
.bd-contentRegion {
transition: all .3s cubic-bezier(.4,0,0,1);
transform: none;
opacity: 1;
}
}
&.bd-active {
.bd-contentRegion {
transition: all .3s cubic-bezier(.4, 0, 0, 1);
transform: none;
opacity: 1;
}
}
&.bd-stop {
.bd-sidebarRegion {
z-index: 1003;
}
&.bd-stop {
.bd-sidebarRegion {
z-index: 1003;
}
.bd-contentRegion {
z-index: 1002;
}
}
}
.bd-contentRegion {
z-index: 1002;
}
}
}
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 0;
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 0;
.bd-sidebarView {
.bd-sidebarRegion {
padding-top: 22px;
}
}
}
.bd-sidebarView {
.bd-sidebarRegion {
padding-top: 22px;
}
}
}
.platform-linux & { // sass-lint:disable-line class-name-format
top: 0;
}
.platform-linux & { // sass-lint:disable-line class-name-format
top: 0;
}
&:not(.bd-active) > .bd-sidebarView.bd-active, // sass-lint:disable-line force-element-nesting
&.bd-settingsOut .bd-sidebarView.bd-active { // sass-lint:disable-line force-element-nesting
.bd-contentRegion {
transform: translate(-600px, 0%);
opacity: 0;
width: 590px;
}
}
&:not(.bd-active) > .bd-sidebarView.bd-active, // sass-lint:disable-line force-element-nesting
&.bd-settingsOut .bd-sidebarView.bd-active { // sass-lint:disable-line force-element-nesting
.bd-contentRegion {
transform: translate(-600px, 0%);
opacity: 0;
width: 590px;
}
}
&:not(.bd-active) {
.bd-sidebarView {
&.bd-active {
.bd-contentRegion {
transform: translate(-600px, 100%);
}
}
}
}
&:not(.bd-active) {
.bd-sidebarView {
&.bd-active {
.bd-contentRegion {
transform: translate(-600px, 100%);
}
}
}
}
}
.bd-sidebar {
.bd-settingsButton {
position: absolute;
top: 0;
.bd-settingsButton {
position: absolute;
top: 0;
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 22px;
}
}
.platform-darwin & { // sass-lint:disable-line class-name-format
top: 22px;
}
}
}

View File

@ -3,4 +3,22 @@
margin: 0 0 10px;
color: #fff;
}
.bd-settingSwitch {
.bd-spinner7 {
height: 24px;
}
.bd-updaterStatus {
text-align: right;
&.bd-err {
color: $colerr;
}
&.bd-ok {
color: $colok;
}
}
}
}

View File

@ -1,13 +1,13 @@
// sass-lint:disable-all
body:not(.bd-hideButton) {
[class*='guildsWrapper-'] {
[class*='layer-'] > * > [class*='wrapper-'] {
padding-top: 49px !important;
}
.platform-osx [class*='guildsWrapper-'] {
.platform-osx [class*='layer-'] > * > [class*='wrapper-'] {
margin-top: 26px;
}
[class*='guildsWrapper-'] + [class*='flex'] {
[class*='layer-'] > * > [class*='wrapper-'] + [class*='flex'] {
border-radius: 0 0 0 5px;
}
@ -27,3 +27,9 @@ body:not(.bd-hideButton) {
.bd-settingsWrapper.platform-linux {
transform: none;
}
// Remove the margin on message attachments with an emote
.da-containerCozy + .da-containerCozy > * > .bd-emote {
margin-top: -8px;
margin-bottom: -8px;
}

View File

@ -28,21 +28,23 @@
.bd-chevron1 {
position: absolute;
svg{
svg {
transform: scale(1) translateY(0);
}
}
.bd-chevron2{
.bd-chevron2 {
position: absolute;
svg{
svg {
visibility: hidden;
transform: scale(1.4,.5) translateY(6px) rotate(180deg);
transform: scale(1.4, .5) translateY(6px) rotate(180deg);
}
}
svg {
transition: transform .2s cubic-bezier(.2,0,0,1);
transition: transform .2s cubic-bezier(.2, 0, 0, 1);
}
}
@ -79,7 +81,7 @@
.bd-drawerOpenButton {
.bd-chevron1 {
svg {
transform: scale(1.4,.5) translateY(-6px);
transform: scale(1.4, .5) translateY(-6px);
visibility: hidden;
}
}

View File

@ -47,7 +47,7 @@
bottom: 3px;
background: #f6f6f7;
border-radius: 10px;
transition: all .15s cubic-bezier(.2,0,0,1);
transition: all .15s cubic-bezier(.2, 0, 0, 1);
box-shadow: 0 3px 1px 0 rgba(0, 0, 0, .05), 0 2px 2px 0 rgba(0, 0, 0, .1), 0 3px 3px 0 rgba(0, 0, 0, .05);
}

View File

@ -22,12 +22,12 @@
> .bd-scroller {
overflow-y: scroll;
}
}
.bd-settings & {
.platform-darwin & { // sass-lint:disable-line class-name-format
padding-top: 22px;
}
}
.bd-settings & {
.platform-darwin & { // sass-lint:disable-line class-name-format
padding-top: 22px;
}
}

View File

@ -1,50 +1,51 @@
.bd-sidebar {
width: 100%;
padding-right: 20px;
padding: 0;
width: 100%;
padding-right: 20px;
padding: 0;
.bd-header {
padding: 6px 0;
margin-left: 10px;
margin-top: 15px;
color: rgba(255, 255, 255, .15);
font-size: 14px;
line-height: 16px;
text-transform: uppercase;
font-weight: 600;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
}
.bd-header {
padding: 6px 0;
margin-left: 10px;
margin-top: 15px;
color: rgba(255, 255, 255, .15);
font-size: 14px;
line-height: 16px;
text-transform: uppercase;
font-weight: 600;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
}
.bd-item {
border-radius: 3px;
margin-bottom: 2px;
padding-bottom: 6px;
padding-top: 6px;
padding: 6px 10px;
color: $coldimwhite;
cursor: pointer;
font-size: 17px;
line-height: 20px;
position: relative;
flex-shrink: 0;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
.bd-item {
border-radius: 3px;
margin-bottom: 2px;
padding-bottom: 6px;
padding-top: 6px;
padding: 6px 10px;
color: $coldimwhite;
cursor: pointer;
font-size: 17px;
line-height: 20px;
position: relative;
flex-shrink: 0;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
&:hover {
background-color: rgba(185,185,185,.1);
color: #f6f6f6;
}
&.bd-active {
background: $colbdgreen;
color: #fff;
cursor: default;
}
}
&:hover {
background-color: rgba(185, 185, 185, .1);
color: #f6f6f6;
}
&.bd-active {
background: $colbdgreen;
color: #fff;
cursor: default;
}
}
}

View File

@ -11,7 +11,7 @@ export default new class Autocomplete {
}
async init() {
this.cta = await ReactComponents.getComponent('ChannelTextArea', { selector: Reflection.resolve('channelTextArea', 'emojiButton').selector });
this.cta = await ReactComponents.getComponent('ChannelTextArea');
MonkeyPatch('BD:Autocomplete', this.cta.component.prototype).after('render', this.channelTextAreaAfterRender.bind(this));
this.initialized = true;
}

View File

@ -23,7 +23,7 @@
</Sidebar>
<div slot="sidebarfooter" class="bd-info">
<span class="bd-vtext">v2.0.0a by Jiiks/JsSucks</span>
<span class="bd-vtext">{{versionString}}</span>
<div @click="openGithub" v-tooltip="'GitHub'" class="bd-materialButton">
<MiGithubCircle size="16" />
</div>
@ -63,11 +63,11 @@
<script>
// Imports
import { Events, Settings } from 'modules';
import { Events, Settings, Globals, Reflection } from 'modules';
import { BdMenuItems } from 'ui';
import { shell } from 'electron';
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView, ConnectivityView } from './bd';
import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView, ConnectivityView, SuperSecretView } from './bd';
import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common';
export default {
@ -96,6 +96,9 @@
category.push(item);
}
return categories;
},
versionString() {
return Globals.version;
}
},
methods: {
@ -123,6 +126,13 @@
},
created() {
Events.on('bd-open-menu', this.openMenuHandler = item => item && this.itemOnClick(this.items.find(i => i === item || i.id === item || i.contentid === item || i.set === item).id));
try {
const currentUser = Reflection.module.byName('UserStore').getCurrentUser();
if (['81388395867156480', '98003542823944192', '249746236008169473', '284056145272766465', '478559353516064769'].includes(currentUser.id)) {
BdMenuItems.addVueComponent('BD Devs', 'Super Secret', SuperSecretView);
}
} catch (err) {}
},
destroyed() {
if (this.openMenuHandler) Events.off('bd-open-menu', this.openMenuHandler);

View File

@ -8,6 +8,8 @@
* LICENSE file in the root directory of this source tree.
*/
// TODO this should be remade as editor instead of css editor
<template>
<SettingsWrapper headertext="CSS Editor">
<div class="bd-cssEditor">
@ -49,7 +51,7 @@
<script>
// Imports
import { Settings, CssEditor } from 'modules';
import { Settings, Editor } from 'modules';
import { SettingsWrapper } from './';
import { FormButton } from '../common';
import SettingsPanel from './SettingsPanel.vue';
@ -64,18 +66,18 @@
},
data() {
return {
CssEditor
Editor
};
},
computed: {
error() {
return this.CssEditor.error;
return this.Editor.error;
},
compiling() {
return this.CssEditor.compiling;
return this.Editor.compiling;
},
systemEditorPath() {
return this.CssEditor.filePath;
return this.Editor.filePath;
},
liveUpdateSetting() {
return Settings.getSetting('css', 'default', 'live-update');
@ -92,13 +94,13 @@
},
methods: {
openInternalEditor() {
this.CssEditor.show();
this.Editor.show();
},
openSystemEditor() {
this.CssEditor.openSystemEditor();
// this.Editor.openSystemEditor();
},
recompile() {
this.CssEditor.recompile();
// this.Editor.recompile();
}
}
}

View File

@ -0,0 +1,105 @@
/**
* BetterDiscord Developer View Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<SettingsWrapper headertext="Super Secret">
<div class="bd-flex bd-flexCol bd-devview">
<FormButton @click="forceUpdate">Force Update</FormButton>
<FormButton @click="debugConfig">Config Debug</FormButton>
<FormButton @click="testUpdateUi">Update UI Test</FormButton>
</div>
</SettingsWrapper>
</template>
<script>
import SettingsWrapper from './SettingsWrapper.vue';
import { FormButton } from '../common';
import { Globals, Events, Updater } from 'modules';
import { ClientIPC } from 'common';
export default {
data() {
return {
};
},
components: {
SettingsWrapper,
FormButton
},
methods: {
forceUpdate() {
ClientIPC.send('debug-updater-forceUpdate');
},
debugConfig() {
console.log(Globals);
},
testUpdateUi() {
Updater.testUi({
'bd': [
{
'id': 'update',
'version': '3.0.0',
'currentVersion': '2.0.0',
'text': 'Update test',
'hint': 'Current: 2.0.0 | Latest: 3.0.0',
'status': {
'update': true,
'updating': false,
'updated': false,
'error': null
}
},
{
'id': 'updating',
'version': '3.0.0',
'currentVersion': '2.0.0',
'text': 'Updating test',
'hint': 'Current: 2.0.0 | Latest: 3.0.0',
'status': {
'update': true,
'updating': true,
'updated': false,
'error': null
}
},
{
'id': 'updated',
'version': '3.0.0',
'currentVersion': '2.0.0',
'text': 'Updated test',
'hint': 'Current: 2.0.0 | Latest: 3.0.0',
'status': {
'update': true,
'updating': true,
'updated': true,
'error': null
}
},
{
'id': 'error',
'version': '3.0.0',
'currentVersion': '2.0.0',
'text': 'Error test',
'hint': 'Current: 2.0.0 | Latest: 3.0.0',
'status': {
'update': true,
'updating': true,
'updated': false,
'error': 'Failed to update.'
}
}
],
'haveUpdates': true
});
}
}
}
</script>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Updater Status Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-settingSwitch">
<div class="bd-title">
<h3>{{item.text}}</h3>
<h3 class="bd-updaterStatus bd-err" v-if="item.status.error">Update Failed!</h3>
<h3 class="bd-updaterStatus bd-ok" v-else-if="item.status.updated">Done</h3>
<div class="bd-spinner7" v-else-if="item.status.updating" />
<h3 class="bd-updaterStatus" v-else>Unknown</h3>
</div>
<div class="bd-hint">{{item.hint}}</div>
</div>
</template>
<script>
export default {
props: ['item']
}
</script>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Updater Switch Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-settingSwitch">
<div class="bd-title">
<h3>{{item.text}}</h3>
<div class="bd-switchWrapper" @click="() => toggle(item)">
<input type="checkbox" class="bd-switchCheckbox" />
<div class="bd-switch" :class="{'bd-checked': item.status.update}" />
</div>
</div>
<div class="bd-hint">{{item.hint}}</div>
</div>
</template>
<script>
export default {
props: ['item', 'toggle']
}
</script>

View File

@ -11,19 +11,22 @@
<template>
<SettingsWrapper headertext="Updates">
<div class="bd-flex bd-flexCol bd-updaterview">
<div v-if="error" class="bd-formItem">
<h5 style="margin-bottom: 10px;">Error installing updates</h5>
<div class="bd-err bd-preWrap"><div class="bd-pre">{{ error.formatted }}</div></div>
<div class="bd-formDivider"></div>
<div class="bd-settingsCategories">
<div class="bd-settingsCategory" v-if="bdUpdates && bdUpdates.length">
<div class="bd-formItem">
<h5>BetterDiscord</h5>
</div>
<div class="bd-formDivider"></div>
<div v-for="update in bdUpdates">
<UpdaterStatus :item="update" v-if="update.status.updating" />
<UpdaterToggle :item="update" :toggle="() => updater.toggleUpdate(update)" v-else />
<div class="bd-formDivider"></div>
</div>
</div>
</div>
<div class="bd-formButton bd-button" @click="update">
Update
</div>
<template v-if="updatesAvailable">
<p>Version {{ newVersion }} is available. You are currently running version {{ currentVersion }}.</p>
<FormButton :onClick="install" :loading="updating">Install</FormButton>
</template>
<template v-else>
<p>You're all up to date!</p>
</template>
</div>
</SettingsWrapper>
</template>
@ -32,7 +35,8 @@
import { Globals, Updater } from 'modules';
import { ClientLogger as Logger } from 'common';
import SettingsWrapper from './SettingsWrapper.vue';
import { FormButton } from '../common';
import UpdaterToggle from './UpdaterToggle.vue';
import UpdaterStatus from './UpdaterStatus.vue';
export default {
data() {
@ -44,26 +48,29 @@
},
components: {
SettingsWrapper,
FormButton
UpdaterToggle,
UpdaterStatus
},
computed: {
updatesAvailable() {
return this.updater.updatesAvailable;
},
newVersion() {
return this.updater.latestVersion;
return '2.0.0-beta.4';
},
error() {
return this.updater.error;
},
updates() {
return this.updater.updates;
},
bdUpdates() {
return this.updater.bdUpdates;
}
},
methods: {
async install() {
this.updating = true;
try {
await this.updater.update();
} catch (err) {}
this.updating = false;
update() {
this.updater.startUpdate();
}
}
}

View File

@ -4,5 +4,8 @@ export { default as CssEditorView } from './CssEditor.vue';
export { default as PluginsView } from './PluginsView.vue';
export { default as ThemesView } from './ThemesView.vue';
export { default as UpdaterView } from './UpdaterView.vue';
export { default as UpdaterStatus } from './UpdaterStatus.vue';
export { default as UpdaterToggle } from './UpdaterToggle.vue';
export { default as BdBadge } from './BdBadge.vue';
export { default as ConnectivityView } from './ConnectivityView.vue';
export { default as SuperSecretView } from './SuperSecretView.vue';

View File

@ -33,16 +33,16 @@
</div>
<div v-else-if="!verified" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus bd-err">Not verified!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();" v-if="modal.canUpload">Upload</div>
<div class="bd-button bd-err" @click="install" v-if="allowUnsafe">{{ !alreadyInstalled ? 'Install' : 'Update' }}</div>
</div>
<div v-else-if="alreadyInstalled && upToDate" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus">Up to date version already installed!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();" v-if="modal.canUpload">Upload</div>
</div>
<div v-else slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus bd-ok">Verified!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();" v-if="modal.canUpload">Upload</div>
<div class="bd-button bd-ok" @click="install">{{ !alreadyInstalled ? 'Install' : 'Update' }}</div>
</div>
</template>

View File

@ -9,7 +9,7 @@
*/
import { Utils, ClientLogger as Logger } from 'common';
import { ReactComponents, Reflection, MonkeyPatch } from 'modules';
import { Reflection, MonkeyPatch } from 'modules';
import { VueInjector, Toasts } from 'ui';
import CMGroup from './components/contextmenu/Group.vue';

View File

@ -191,12 +191,12 @@ export default class Modals {
return new Modal(modal, InputModal);
}
static installModal(contentType, config, filePath, icon) {
return this.add(this.createInstallModal(contentType, config, filePath, icon));
static installModal(contentType, config, filePath, icon, canUpload = false) {
return this.add(this.createInstallModal(contentType, config, filePath, icon, canUpload));
}
static createInstallModal(contentType, config, filePath, icon) {
const modal = { contentType, config, filePath, icon };
static createInstallModal(contentType, config, filePath, icon, canUpload = false) {
const modal = { contentType, config, filePath, icon, canUpload };
modal.promise = new Promise((resolve, reject) => {
modal.confirm = value => resolve(value);
modal.beforeClose = () => reject();

View File

@ -87,8 +87,7 @@ export default class extends Module {
async patchNameTag() {
if (this.PatchedNameTag) return this.PatchedNameTag;
const selector = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon').selector;
const NameTag = await ReactComponents.getComponent('NameTag', {selector});
const NameTag = await ReactComponents.getComponent('NameTag');
this.PatchedNameTag = class extends NameTag.component {
render() {

View File

@ -9,6 +9,7 @@ export * from './contextmenus';
export { default as VueInjector } from './vueinjector';
export { default as Reflection } from './reflection';
export { default as Autocomplete } from './autocomplete';
export { default as ProfileBadges } from './profilebadges';
export { default as ClassNormaliser } from './classnormaliser';

View File

@ -71,7 +71,7 @@ export default class {
get vueMount() {
const element = ReactDOM.findDOMNode(this);
if (!element) return;
if (!element) return null;
if (this.props.mountAtTop) return element;
if (element.firstChild) return element.firstChild;
const newElement = document.createElement('span');

View File

@ -0,0 +1,63 @@
const path = require('path');
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const jsLoader = {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
};
const vueLoader = {
test: /\.(vue)$/,
use: 'vue-loader'
};
const scssLoader = {
test: /\.scss$/,
exclude: /node_modules/,
use: ['css-loader', 'sass-loader']
};
module.exports = {
entry: './src/index.js',
module: {
rules: [jsLoader, vueLoader, scssLoader]
},
externals: {
electron: 'require("electron")',
fs: 'require("fs")',
path: 'require("path")',
util: 'require("util")',
process: 'require("process")',
net: 'require("net")',
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
sparkplug: 'require("../../core/dist/sparkplug")',
'node-crypto': 'require("crypto")',
'child_process': 'require("child_process")'
},
resolve: {
alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
},
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules'),
path.resolve('src', 'modules'),
path.resolve('src', 'ui'),
path.resolve('src', 'plugins'),
path.resolve('src', 'structs'),
path.resolve('src', 'builtin')
]
},
node: {
process: false,
__filename: false,
__dirname: false
},
plugins: [
new VueLoaderPlugin()
]
};

View File

@ -1,68 +1,22 @@
const path = require('path');
const baseconfig = require('./webpack.base.config');
const merge = require('webpack-merge');
const webpack = require('webpack');
const jsLoader = {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['react']
}
};
const vueLoader = {
test: /\.(vue)$/,
loader: 'vue-loader'
};
const scssLoader = {
test: /\.scss$/,
exclude: /node_modules/,
loader: ['css-loader', 'sass-loader']
};
module.exports = {
entry: './src/index.js',
const config = {
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'betterdiscord.client.js'
},
module: {
loaders: [jsLoader, vueLoader, scssLoader]
},
externals: {
electron: 'require("electron")',
asar: 'require("asar")',
fs: 'require("fs")',
path: 'require("path")',
util: 'require("util")',
process: 'require("process")',
net: 'require("net")',
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
sparkplug: 'require("../../core/dist/sparkplug")',
'node-crypto': 'require("crypto")'
},
resolve: {
alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
},
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules'),
path.resolve('src', 'modules'),
path.resolve('src', 'ui'),
path.resolve('src', 'plugins'),
path.resolve('src', 'structs'),
path.resolve('src', 'builtin')
]
},
node: {
process: false,
__filename: false,
__dirname: false
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.EvalSourceMapDevToolPlugin()
]
new webpack.NamedModulesPlugin()
],
externals: {
asar: 'require("asar")'
}
};
module.exports = merge(baseconfig, config);

View File

@ -1,70 +1,24 @@
const path = require('path');
const baseconfig = require('./webpack.base.config');
const merge = require('webpack-merge');
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const jsLoader = {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['react']
}
};
const vueLoader = {
test: /\.(vue)$/,
loader: 'vue-loader'
};
const scssLoader = {
test: /\.scss$/,
exclude: /node_modules/,
loader: ['css-loader', 'sass-loader']
};
module.exports = {
entry: './src/index.js',
const config = {
mode: 'production',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'betterdiscord.client-release.js'
},
module: {
loaders: [jsLoader, vueLoader, scssLoader]
},
externals: {
electron: 'require("electron")',
fs: 'require("fs")',
path: 'require("path")',
util: 'require("util")',
process: 'require("process")',
net: 'require("net")',
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
sparkplug: 'require("./sparkplug")',
'node-crypto': 'require("crypto")'
},
resolve: {
alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
},
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules'),
path.resolve('src', 'modules'),
path.resolve('src', 'ui'),
path.resolve('src', 'plugins'),
path.resolve('src', 'structs'),
path.resolve('src', 'builtin')
]
},
node: {
process: false,
__filename: false,
__dirname: false
},
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true)
}),
new UglifyJsPlugin()
]
})
],
externals: {
sparkplug: 'require("./sparkplug")'
}
};
module.exports = merge(baseconfig, config);

51
common/modules/axi.js Normal file
View File

@ -0,0 +1,51 @@
/**
* BetterDiscord axios wrapper
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import axios from 'axios';
export default class AxiosWrapper {
static get axios() { return axios; }
static get github() {
return this._github ? this._github : (
this._github = {
main: this.create('https://github.com'),
api: this.create('https://api.github.com')
}
);
}
static get zl() {
return this._zl ? this._zl : (this._zl = {
api: this.create('https://zl', 1000, this.zlHeaders),
cdn: this.create('https://zl', 1000, this.zlHeaders)
});
}
static create(baseUrl, timeout = 1000, headers = null) {
return axios.create({ baseURL: baseUrl, timeout, headers: headers ? headers : this.defaultHeaders });
}
static get defaultHeaders() {
return {
'User-Agent': 'BetterDiscordApp User'
};
}
static get zlHeaders() {
return {
'User-Agent': 'BetterDiscordApp User',
'X-ZL-Apikey': '1a20cce89a2dbd163fc9570f3246c20891e62b2818ada55f82fa3d1d96fa7ef4',
'X-ZL-User': 'anonymous'
}
}
}

View File

@ -3,3 +3,4 @@ export { default as Filters } from './filters';
export { default as Logger, ClientLogger } from './logger';
export { default as ClientIPC } from './bdipc';
export { default as AsyncEventEmitter } from './async-eventemitter';
export { default as Axi } from './axi';

17
core/babel.config.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = function(api) {
api.cache(true);
const presets = [['@babel/env', {
targets: {
'node': '8.6.0'
}
}]];
const plugins = [];
return {
presets,
plugins
}
}

View File

@ -2,7 +2,7 @@
"name": "bdcore",
"description": "BetterDiscord core package",
"author": "Jiiks",
"version": "2.0.0b",
"version": "2.0.0-beta.6",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "dist/main.js",

25
core/src/csp.json Normal file
View File

@ -0,0 +1,25 @@
{
"img-src": [
"https://cdn.betterttv.net",
"https://cdn.frankerfacez.com",
"https://i.imgur.com"
],
"style-src": [
"https://fonts.googleapis.com"
],
"script-src": [
"'sha256-fSHKdpQGCHaIqWP3SpJOuUHrLp49jy4dWHzZ/RBJ/p4='",
"'sha256-VFJcfKY5B3EBkFDgQnv3CozPwBlZcxwssfLVWlPFfZU='",
"'sha256-VzDmLZ4PxPkOS/KY7ITzLQsSWhfCnvUrNculcj8UNgE='",
"'sha256-l6K+77Z1cmldR9gIvaVWlboF/zr5MXCQHcsEHfnr5TU='"
],
"connect-src": [
"https://github.com",
"https://api.github.com",
"https://betterdiscord.net",
"https://api.betterdiscord.net",
"https://cdn.betterdiscord.net",
"https://api.supersecretbdapiandcdn.net",
"https://cdn.supersecretbdapiandcdn.net"
]
}

3
core/src/csp.txt Normal file
View File

@ -0,0 +1,3 @@
React Devtools: sha256-fSHKdpQGCHaIqWP3SpJOuUHrLp49jy4dWHzZ/RBJ/p4=
Vue Devtools: sha256-VFJcfKY5B3EBkFDgQnv3CozPwBlZcxwssfLVWlPFfZU=
Vue Detector: sha256-l6K+77Z1cmldR9gIvaVWlboF/zr5MXCQHcsEHfnr5TU=

View File

@ -8,100 +8,57 @@
* LICENSE file in the root directory of this source tree.
*/
/*PRODUCTION*/
const TESTS = typeof PRODUCTION === 'undefined';
const TEST_ARGS = () => {
const _basePath = path.resolve(__dirname, '..', '..');
const _baseDataPath = path.resolve(_basePath, 'tests');
const _corePkg = require(path.resolve(_basePath, 'core', 'package.json'));
const _clientPkg = require(path.resolve(_basePath, 'client', 'package.json'));
const _editorPkg = require(path.resolve(_basePath, 'editor', 'package.json'));
const coreVersion = _corePkg.version;
const clientVersion = _clientPkg.version;
const editorVersion = _editorPkg.version;
return {
coreVersion,
clientVersion,
editorVersion,
'options': {
'autoInject': true,
'commonCore': true,
'commonData': true
},
'paths': {
'client': path.resolve(_basePath, 'client', 'dist'),
'core': path.resolve(_basePath, 'core', 'dist'),
'data': path.resolve(_baseDataPath, 'data'),
'editor': path.resolve(_basePath, 'editor', 'dist'),
// tmp: path.join(_basePath, 'tmp')
tmp: path.join(os.tmpdir(), 'betterdiscord', `${process.getuid()}`)
}
}
}
const TEST_EDITOR = TESTS && true;
import process from 'process';
import os from 'os';
import path from 'path';
import sass from 'node-sass';
import { BrowserWindow as OriginalBrowserWindow, dialog, session } from 'electron';
import { BrowserWindow as OriginalBrowserWindow, dialog, session, shell } from 'electron';
import deepmerge from 'deepmerge';
import ContentSecurityPolicy from 'csp-parse';
import keytar from 'keytar';
import { FileUtils, BDIpc, Config, WindowUtils, CSSEditor, Database } from './modules';
const tests = typeof PRODUCTION === 'undefined';
const _basePath = tests ? path.resolve(__dirname, '..', '..') : __dirname;
const _baseDataPath = tests ? path.resolve(_basePath, 'tests') : _basePath;
import { FileUtils, BDIpc, Config, WindowUtils, Updater, Editor, Database } from './modules';
const sparkplug = path.resolve(__dirname, 'sparkplug.js');
const _clientScript = tests
? path.resolve(_basePath, 'client', 'dist', 'betterdiscord.client.js')
: path.resolve(_basePath, 'betterdiscord.client.js');
const _cssEditorPath = tests
? path.resolve(__dirname, '..', '..', 'csseditor', 'dist')
: path.resolve(__dirname, 'csseditor');
let configProxy;
const _dataPath = path.resolve(_baseDataPath, 'data');
const _extPath = path.resolve(_baseDataPath, 'ext');
const _pluginPath = path.resolve(_extPath, 'plugins');
const _themePath = path.resolve(_extPath, 'themes');
const _modulePath = path.resolve(_extPath, 'modules');
const version = require(path.resolve(_basePath, 'package.json')).version;
const paths = [
{ id: 'base', path: _basePath },
{ id: 'cs', path: _clientScript },
{ id: 'data', path: _dataPath },
{ id: 'ext', path: _extPath },
{ id: 'plugins', path: _pluginPath },
{ id: 'themes', path: _themePath },
{ id: 'modules', path: _modulePath },
{ id: 'csseditor', path: _cssEditorPath }
];
const globals = {
version,
paths
};
const CSP = {
'img-src': ['https://cdn.betterttv.net', 'https://cdn.frankerfacez.com'],
'script-src': [
`'sha256-fSHKdpQGCHaIqWP3SpJOuUHrLp49jy4dWHzZ/RBJ/p4='`, // React Devtools
`'sha256-VFJcfKY5B3EBkFDgQnv3CozPwBlZcxwssfLVWlPFfZU='`, // Vue Devtools
`'sha256-VzDmLZ4PxPkOS/KY7ITzLQsSWhfCnvUrNculcj8UNgE=' 'sha256-l6K+77Z1cmldR9gIvaVWlboF/zr5MXCQHcsEHfnr5TU='` // Vue Detector
]
};
class BrowserWindow extends OriginalBrowserWindow {
constructor(originalOptions) {
const userOptions = BrowserWindow.userWindowPreferences;
const options = deepmerge(originalOptions, userOptions);
options.webPreferences = Object.assign({}, options.webPreferences);
// Make sure Node integration is enabled
options.webPreferences.preload = sparkplug;
super(options);
Object.defineProperty(this, '__bd_preload', {value: []});
if (originalOptions.webPreferences && originalOptions.webPreferences.preload) {
this.__bd_preload.push(originalOptions.webPreferences.preload);
}
if (userOptions.webPreferences && userOptions.webPreferences.preload) {
this.__bd_preload.push(path.resolve(_dataPath, userOptions.webPreferences.preload));
}
Object.defineProperty(this, '__bd_options', {value: options});
Object.freeze(options);
Object.freeze(options.webPreferences);
Object.freeze(this.__bd_preload);
}
static get userWindowPreferences() {
try {
const userWindowPreferences = require(path.join(_dataPath, 'window'));
if (typeof userWindowPreferences === 'object') return userWindowPreferences;
} catch (err) {
console.log('[BetterDiscord] Error getting window preferences:', err);
}
return {};
}
}
const CSP = TESTS ? require('../src/csp.json') : require('./csp.json');
class Comms {
constructor(bd) {
@ -116,8 +73,9 @@ class Comms {
BDIpc.on('bd-sendToDiscord', (event, m) => this.sendToDiscord(m.channel, m.message), true);
BDIpc.on('bd-openCssEditor', (event, options) => this.bd.csseditor.openEditor(options), true);
BDIpc.on('bd-sendToCssEditor', (event, m) => this.sendToCssEditor(m.channel, m.message), true);
// BDIpc.on('bd-openCssEditor', (event, options) => this.bd.csseditor.openEditor(options), true);
// BDIpc.on('bd-sendToCssEditor', (event, m) => this.sendToCssEditor(m.channel, m.message), true);
// BDIpc.on('bd-openCssEditor', (event, options) => this.bd.editor.openEditor(options), true);
BDIpc.on('bd-native-open', (event, options) => {
dialog.showOpenDialog(OriginalBrowserWindow.fromWebContents(event.ipcEvent.sender), options, filenames => {
@ -137,12 +95,54 @@ class Comms {
});
});
BDIpc.on('bd-dba', (event, options) => this.bd.dbInstance.exec(options), true);
BDIpc.on('bd-dba', (event, options) => this.bd.database.exec(options), true);
BDIpc.on('bd-keytar-get', (event, {service, account}) => keytar.getPassword(service, account), true);
BDIpc.on('bd-keytar-set', (event, {service, account, password}) => keytar.setPassword(service, account, password), true);
BDIpc.on('bd-keytar-delete', (event, {service, account}) => keytar.deletePassword(service, account), true);
BDIpc.on('bd-keytar-find-credentials', (event, {service}) => keytar.findCredentials(service), true);
BDIpc.on('bd-keytar-get', (event, { service, account }) => keytar.getPassword(service, account), true);
BDIpc.on('bd-keytar-set', (event, { service, account, password }) => keytar.setPassword(service, account, password), true);
BDIpc.on('bd-keytar-delete', (event, { service, account }) => keytar.deletePassword(service, account), true);
BDIpc.on('bd-keytar-find-credentials', (event, { service }) => keytar.findCredentials(service), true);
BDIpc.on('bd-readDataFile', async (event, fileName) => {
const rf = await FileUtils.readFile(path.resolve(configProxy().getPath('data'), fileName));
event.reply(rf);
});
BDIpc.on('bd-explorer', (_, _path) => {
if (_path.static) _path = this.bd.config.getPath(_path.static);
else if (_path.full) _path = _path.full;
else if (_path.sub) _path = path.resolve(this.bd.config.getPath(_path.sub.base), [..._path.sub.subs]);
try {
shell.openItem(_path);
} catch (err) {
console.log(err);
}
});
BDIpc.on('bd-getPath', (event, paths) => {
event.reply(path.resolve(this.bd.config.getPath(paths[0]), ...paths.splice(1)));
});
BDIpc.on('bd-rmFile', async (event, paths) => {
const fullPath = path.resolve(this.bd.config.getPath(paths[0]), ...paths.splice(1));
try {
await FileUtils.rm(fullPath);
event.reply('ok');
} catch (err) {
event.reject(err);
}
});
BDIpc.on('bd-rnFile', async (event, paths) => {
const oldPath = path.resolve(this.bd.config.getPath(paths.oldName[0]), ...paths.oldName.splice(1));
const newPath = path.resolve(this.bd.config.getPath(paths.newName[0]), ...paths.newName.splice(1));
try {
await FileUtils.rn(oldPath, newPath);
event.reply('ok');
} catch (err) {
event.reject(err);
}
});
}
async send(channel, message) {
@ -158,38 +158,90 @@ class Comms {
}
}
class BrowserWindow extends OriginalBrowserWindow {
constructor(originalOptions) {
const userOptions = BrowserWindow.userWindowPreferences;
const options = deepmerge(originalOptions, userOptions);
options.webPreferences = Object.assign({}, options.webPreferences);
// Make sure Node integration is enabled
options.webPreferences.preload = sparkplug;
super(options);
Object.defineProperty(this, '__bd_preload', { value: [] });
if (originalOptions.webPreferences && originalOptions.webPreferences.preload) {
this.__bd_preload.push(originalOptions.webPreferences.preload);
}
if (userOptions.webPreferences && userOptions.webPreferences.preload) {
this.__bd_preload.push(path.resolve(configProxy().getPath('data'), userOptions.webPreferences.preload));
}
Object.defineProperty(this, '__bd_options', { value: options });
Object.freeze(options);
Object.freeze(options.webPreferences);
Object.freeze(this.__bd_preload);
}
static get userWindowPreferences() {
try {
const userWindowPreferences = require(path.join(configProxy().getPath('data'), 'window'));
if (typeof userWindowPreferences === 'object') return userWindowPreferences;
} catch (err) {
console.log('[BetterDiscord] Error getting window preferences:', err);
}
return {};
}
}
export class BetterDiscord {
get comms() { return this._comms ? this._comms : (this._commas = new Comms(this)); }
get database() { return this._db ? this._db : (this._db = new Database(this.config.getPath('data'))); }
get config() { return this._config ? this._config : (this._config = new Config(this._args)); }
get window() { return this.windowUtils ? this.windowUtils.window : undefined; }
get editor() { return this._editor ? this._editor : (this._editor = new Editor(this, this.config.getPath('editor'))); }
get updater() { return this._updater ? this._updater : (this._updater = new Updater(this)); }
get sendToDiscord() { return this.windowUtils.send; }
constructor(args) {
if (TESTS) args = TEST_ARGS();
console.log('[BetterDiscord|args] ', JSON.stringify(args, null, 4));
if (BetterDiscord.loaded) {
console.log('Creating two BetterDiscord objects???');
console.log('[BetterDiscord] Creating two BetterDiscord objects???');
return null;
}
BetterDiscord.loaded = true;
this._args = args;
this.config.compatibility();
this.injectScripts = this.injectScripts.bind(this);
this.ignite = this.ignite.bind(this);
this.config = new Config(args || globals);
this.dbInstance = new Database(this.config.getPath('data'));
this.comms = new Comms(this);
this.bindings();
this.extraPaths();
this.parseClientPackage();
this.parseEditorPackage();
this.parseCorePackage();
configProxy = () => this.config;
const autoInitComms = this.comms;
const autoInitEditor = this.editor;
this.updater.start();
this.init();
}
bindings() {
this.injectScripts = this.injectScripts.bind(this);
this.ignite = this.ignite.bind(this);
this.ensureDirectories = this.ensureDirectories.bind(this);
}
async init() {
console.log('[BetterDiscord] init');
await this.waitForWindowUtils();
if (!tests) {
const basePath = this.config.getPath('base');
const files = await FileUtils.listDirectory(basePath);
const latestCs = FileUtils.resolveLatest(files, file => file.endsWith('.js') && file.startsWith('client.'), file => file.replace('client.', '').replace('.js', ''), 'client.', '.js');
this.config.getPath('cs', true).path = path.resolve(basePath, latestCs);
}
await FileUtils.ensureDirectory(this.config.getPath('ext'));
this.csseditor = new CSSEditor(this, this.config.getPath('csseditor'));
await this.ensureDirectories();
this.windowUtils.on('did-finish-load', () => this.injectScripts(true));
@ -199,9 +251,30 @@ export class BetterDiscord {
setTimeout(() => {
this.injectScripts();
if (TEST_EDITOR) this.editor.openEditor({});
}, 500);
}
async ensureDirectories() {
await FileUtils.ensureDirectory(this.config.getPath('ext'));
await FileUtils.ensureDirectory(this.config.getPath('userdata'));
await Promise.all([
FileUtils.ensureDirectory(this.config.getPath('plugins')),
FileUtils.ensureDirectory(this.config.getPath('themes')),
FileUtils.ensureDirectory(this.config.getPath('modules')),
FileUtils.ensureDirectory(this.config.getPath('userfiles'))
]);
}
async waitForWindowUtils() {
if (this.windowUtils) return this.windowUtils;
const window = await this.waitForWindow();
return this.windowUtils = new WindowUtils({ window });
}
/**
* Wait for Discord to load before doing any injection
*/
async waitForWindow() {
return new Promise(resolve => {
const defer = setInterval(() => {
@ -215,14 +288,55 @@ export class BetterDiscord {
});
}
async waitForWindowUtils() {
if (this.windowUtils) return this.windowUtils;
const window = await this.waitForWindow();
return this.windowUtils = new WindowUtils({ window });
/**
* Parses the package.json of client script into config
*/
parseClientPackage() {
const clientPath = this.config.getPath('client');
const clientPkg = TESTS ? require(`${path.resolve(clientPath, '..')}/package.json`) : require(`${clientPath}/package.json`);
const { version } = clientPkg;
const main = TESTS ? 'betterdiscord.client.js' : clientPkg.main;
this.config.addPath('client_script', `${clientPath}/${main}`);
this.config.setClientVersion(version);
console.log(`[BetterDiscord] Client v${this.config.clientVersion} - ${this.config.getPath('client_script')}`);
}
get window() {
return this.windowUtils ? this.windowUtils.window : undefined;
parseCorePackage() {
const corePath = this.config.getPath('core');
const corePkg = TESTS ? require(`${path.resolve(corePath, '..')}/package.json`) : require(`${corePath}/package.json`);
const { version } = corePkg;
this.config.setCoreVersion(version);
}
parseEditorPackage() {
const editorPath = this.config.getPath('editor');
const editorPkg = TESTS ? require(`${path.resolve(editorPath, '..')}/package.json`) : require(`${editorPath}/package.json`);
const { version } = editorPkg;
this.config.setEditorVersion(version);
}
/**
* Add extra paths to config
*/
extraPaths() {
const base = path.resolve(this.config.getPath('data'), '..');
const userdata = path.resolve(base, 'userdata');
const ext = path.resolve(base, 'ext');
const plugins = path.resolve(ext, 'plugins');
const themes = path.resolve(ext, 'themes');
const modules = path.resolve(ext, 'modules');
const userfiles = path.resolve(userdata, 'files');
const snippets = path.resolve(userdata, 'snippets.json');
this.config.addPath('base', base);
this.config.addPath('ext', ext);
this.config.addPath('plugins', plugins);
this.config.addPath('themes', themes);
this.config.addPath('modules', modules);
this.config.addPath('userdata', userdata);
this.config.addPath('userfiles', userfiles);
this.config.addPath('snippets', snippets);
if (!this.config.getPath('editor')) this.config.addPath('editor', path.resolve(base, 'editor'));
}
/**
@ -245,8 +359,8 @@ export class BetterDiscord {
* @param {Boolean} reload Whether the main window was reloaded
*/
async injectScripts(reload = false) {
console.log(`RELOAD? ${reload}`);
return this.windowUtils.injectScript(this.config.getPath('cs'));
console.log(`[BetterDiscord] injecting ${this.config.getPath('client_script')}. Reload: ${reload}`);
return this.windowUtils.injectScript(this.config.getPath('client_script'));
}
/**
@ -255,10 +369,11 @@ export class BetterDiscord {
* Basically BetterDiscord needs to load before discord_desktop_core.
*/
static patchBrowserWindow() {
console.log('[BetterDiscord] patching BrowserWindow');
const electron = require('electron');
const electron_path = require.resolve('electron');
Object.assign(BrowserWindow, electron.BrowserWindow); // Assigns the new chrome-specific ones
const newElectron = Object.assign({}, electron, {BrowserWindow});
const newElectron = Object.assign({}, electron, { BrowserWindow });
require.cache[electron_path].exports = newElectron;
}
@ -266,6 +381,7 @@ export class BetterDiscord {
* Attaches an event handler for HTTP requests to update the Content Security Policy.
*/
static hookSessionRequest() {
console.log('[BetterDiscord] hook session request');
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
for (const [header, values] of Object.entries(details.responseHeaders)) {
if (!header.match(/^Content-Security-Policy(-Report-Only)?$/i)) continue;
@ -283,7 +399,6 @@ export class BetterDiscord {
callback({ responseHeaders: details.responseHeaders });
});
}
}
BetterDiscord.patchBrowserWindow();

53
core/src/modules/axi.js Normal file
View File

@ -0,0 +1,53 @@
/**
* BetterDiscord axios wrapper
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import axios from 'axios';
export default class AxiosWrapper {
static get axios() { return axios; }
static get(url) { return axios.get(url) }
static get github() {
return this._github ? this._github : (
this._github = {
main: this.create('https://github.com'),
api: this.create('https://api.github.com')
}
);
}
static get zl() {
return this._zl ? this._zl : (this._zl = {
api: this.create('https://zl', 1000, this.zlHeaders),
cdn: this.create('https://zl', 1000, this.zlHeaders)
});
}
static create(baseUrl, timeout = 1000, headers = null) {
return axios.create({ baseURL: baseUrl, timeout, headers: headers ? headers : this.defaultHeaders });
}
static get defaultHeaders() {
return {
'User-Agent': 'BetterDiscordApp User'
};
}
static get zlHeaders() {
return {
'User-Agent': 'BetterDiscordApp User',
'X-ZL-Apikey': '1a20cce89a2dbd163fc9570f3246c20891e62b2818ada55f82fa3d1d96fa7ef4',
'X-ZL-User': 'anonymous'
}
}
}

View File

@ -16,20 +16,62 @@ export default class Config extends Module {
return this.args.version;
}
get versions() {
return {
core: this.coreVersion,
client: this.clientVersion,
editor: this.editorVersion
};
}
get coreVersion() {
return this.state.coreVersion;
}
get clientVersion() {
return this.state.clientVersion;
}
get editorVersion() {
return this.state.editorVersion;
}
setClientVersion(clientVersion) {
this.state.clientVersion = clientVersion;
}
setCoreVersion(coreVersion) {
this.state.coreVersion = coreVersion;
}
setEditorVersion(editorVersion) {
this.state.editorVersion = editorVersion;
}
get paths() {
return this.args.paths;
}
getPath(id, full) {
const path = this.paths.find(path => path.id === id);
const path = this.paths.find(p => p.id === id);
if (!path) return null;
return full ? path : path.path;
}
addPath(id, path) {
this.paths.push({ id, path });
}
get config() {
return {
version: this.version,
versions: this.versions,
paths: this.paths
};
}
// Compatibility with old client code and new installer args
compatibility() {
this.args.paths = Object.entries(this.args.paths).map(([id, path]) => ({ id, path }));
}
}

285
core/src/modules/editor.js Normal file
View File

@ -0,0 +1,285 @@
/**
* BetterDiscord Editor Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import { BrowserWindow } from 'electron';
import Module from './modulebase';
import { WindowUtils, FileUtils } from './utils';
import BDIpc from './bdipc';
import sass from 'node-sass';
import chokidar from 'chokidar';
export default class Editor extends Module {
constructor(bd, path) {
super();
this.editorPath = path;
this.bd = bd;
this.initListeners();
this.initWatchers();
}
initListeners() {
BDIpc.on('openCssEditor', (event, options) => this.openEditor(options), true);
BDIpc.on('editor-open', (event, options) => this.openEditor(options), true);
BDIpc.on('editor-runScript', async (event, script) => {
const result = await this.sendToDiscord('editor-runScript', script);
event.reply(result);
});
BDIpc.on('editor-getFiles', async (event) => {
try {
const files = await FileUtils.listDirectory(this.bd.config.getPath('userfiles'));
const constructFiles = await Promise.all(files.map(async file => {
// const content = await FileUtils.readFile(path.resolve(this.bd.config.getPath('userfiles'), file));
return { type: 'file', name: file, saved: true, mode: this.resolveMode(file), content: '', savedContent: '', read: false, changed: false };
}));
const userscssPath = path.resolve(this.bd.config.getPath('data'), 'user.scss');
await FileUtils.ensureFile(userscssPath);
const userscss = await FileUtils.readFile(userscssPath);
constructFiles.push({
caption: 'userstyle',
type: 'file',
name: 'user.scss',
saved: true,
mode: 'scss',
content: userscss,
savedContent: userscss,
hoisted: true,
liveUpdate: true,
read: true,
changed: false
});
event.reply(constructFiles);
} catch (err) {
console.log(err);
event.reject({ err });
}
});
BDIpc.on('editor-getSnippets', async (event) => {
try {
const snippets = await FileUtils.readJsonFromFile(this.bd.config.getPath('snippets'));
event.reply(snippets.map(snippet => {
return {
type: 'snippet',
name: snippet.name,
mode: this.resolveMode(snippet.name),
content: snippet.content,
savedContent: snippet.content,
read: true,
saved: true
}
}));
} catch (err) {
console.log(err);
event.reply([]);
return;
}
});
BDIpc.on('editor-saveFile', async (event, file) => {
const filePath = (file.hoisted && file.name === 'user.scss') ?
path.resolve(this.bd.config.getPath('data'), 'user.scss') :
path.resolve(this.bd.config.getPath('userfiles'), file.name);
try {
await FileUtils.writeFile(filePath, file.content);
event.reply('ok');
} catch (err) {
console.log(err);
event.reject({ err });
}
});
BDIpc.on('editor-saveSnippet', async (event, snippet) => {
try {
await FileUtils.writeFile(this.bd.config.getPath('snippets'), JSON.stringify(snippet));
event.reply('ok');
} catch (err) {
console.log(err);
event.reject({ err });
}
});
BDIpc.on('editor-injectStyle', async (event, { id, style, mode }) => {
if (mode !== 'scss') {
await this.sendToDiscord('editor-injectStyle', { id, style });
event.reply('ok');
return;
}
style = await Promise.all(style.split('\n').map(async(line) => {
if (!line.startsWith('@import')) return line;
const filename = line.split(' ')[1].replace(/'|"|;/g, '');
const filePath = path.resolve(this.bd.config.getPath('userfiles'), filename).replace(/\\/g, '/');
try {
await FileUtils.fileExists(filePath);
} catch (err) {
`/*${filename}*/`;
}
return `@import '${filePath}';`;
}));
style = style.join('\n');
sass.render({ data: style }, (err, result) => {
if (err) {
console.log(err);
event.reply({ err });
return;
}
const { css } = result;
(async () => {
await this.sendToDiscord('editor-injectStyle', { id, style: css.toString() });
event.reply('ok');
})();
});
});
BDIpc.on('editor-readFile', async (event, file) => {
const content = await FileUtils.readFile(path.resolve(this.bd.config.getPath('userfiles'), file.name));
event.reply(content);
});
}
initWatchers() {
this.fileWatcher = chokidar.watch(this.bd.config.getPath('userfiles'));
this.fileWatcher.on('add', file => {
const fileName = path.basename(file);
try {
this.send('editor-addFile', {
type: 'file',
name: fileName,
saved: true,
mode: this.resolveMode(fileName),
content: '',
savedContent: ''
});
} catch (err) {}
});
this.fileWatcher.on('unlink', file => {
const fileName = path.basename(file);
try {
this.send('editor-remFile', { name: fileName });
} catch (err) {}
});
this.fileWatcher.on('change', file => {
this.send('editor-fileChange', { name: path.basename(file) });
});
}
resolveMode(fileName) {
if (!fileName.includes('.')) return 'text';
const ext = fileName.substr(fileName.lastIndexOf('.') + 1);
if (this.modes.hasOwnProperty(ext)) return this.modes[ext];
return 'text';
}
get modes() {
return {
'css': 'css',
'scss': 'scss',
'js': 'javascript',
'txt': 'text',
'json': 'json'
};
}
/**
* Opens an editor.
* @return {Promise}
*/
async openEditor(options) {
if (!this.editorPkg) {
this.editorPkg = await FileUtils.readJsonFromFile(path.join(this.editorPath, 'package.json'));
}
console.log(this.editorPkg);
return new Promise((resolve, reject) => {
if (this.editor) {
if (this.editor.isFocused()) return;
this.editor.focus();
this.editor.flashFrame(true);
return resolve(true);
}
options = Object.assign({}, this.options, options);
this.editor = new BrowserWindow(options);
this.editor.loadURL('about:blank');
this.editor.setSheetOffset(33);
this.editorUtils = new WindowUtils({ window: this.editor });
this.editor.on('close', () => {
this.bd.windowUtils.send('bd-save-csseditor-bounds', this.editor.getBounds());
this.editor = null;
});
this.editor.once('ready-to-show', () => {
this.editor.show();
});
this.editor.webContents.on('did-finish-load', () => {
this.editorUtils.injectScript(path.join(this.editorPath, this.editorPkg.main));
resolve(true);
});
})
}
/**
* Sends data to the editor.
* @param {String} channel
* @param {Any} data
*/
send(channel, data) {
if (!this.editor) throw { message: 'The CSS editor is not open.' };
return BDIpc.send(this.editor, channel, data);
}
async sendToDiscord(channel, message) {
return this.bd.windowUtils.send(channel, message);
}
/**
* Sets the CSS editor's always on top flag.
*/
set alwaysOnTop(state) {
if (!this.editor) return;
this.editor.setAlwaysOnTop(state);
}
/**
* Default options to pass to BrowserWindow.
*/
get options() {
return {
width: 800,
height: 600,
show: false,
frame: false
};
}
}

View File

@ -2,4 +2,6 @@ export { default as BDIpc } from './bdipc';
export { Utils, FileUtils, WindowUtils } from './utils';
export { default as Config } from './config';
export { default as CSSEditor } from './csseditor';
export { default as Editor } from './editor';
export { default as Database } from './database';
export { default as Updater } from './updater';

View File

@ -11,6 +11,9 @@
/**
* Base Module that every non-static module should extend.
*/
import { default as BDIpc } from './bdipc';
export default class Module {
constructor(args) {
@ -24,6 +27,7 @@ export default class Module {
init() {
if (this.bindings) this.bindings();
if (this.setInitialState) this.setInitialState(this.state);
if (this.events) this.events(BDIpc);
}
set args(t) {}

286
core/src/modules/updater.js Normal file
View File

@ -0,0 +1,286 @@
/**
* BetterDiscord Updater Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Module from './modulebase';
import { FileUtils } from './utils';
import semver from 'semver';
import Axi from './axi';
import zlib from 'zlib';
import tarfs from 'tar-fs';
const TEST_UPDATE = [
{
'id': 'core',
'version': '2.0.0-beta.5'
},
{
'id': 'client',
'version': '2.0.0-beta.5'
},
{
'id': 'editor',
'version': '0.4.1'
}
];
class ReleaseInfo {
constructor(versions) {
this.versions = versions;
}
get core() {
const f = this.files.find(f => f.id === 'core');
f.upToDate = semver.satisfies(this.versions.core, `>=${f.version}`, { includePrerelease: true });
f.currentVersion = this.versions.core;
return f;
}
get client() {
const f = this.files.find(f => f.id === 'client');
f.upToDate = semver.satisfies(this.versions.client, `>=${f.version}`, { includePrerelease: true });
f.currentVersion = this.versions.client;
return f;
}
get editor() {
const f = this.files.find(f => f.id === 'editor');
f.upToDate = semver.satisfies(this.versions.editor, `>=${f.version}`, { includePrerelease: true });
f.currentVersion = this.versions.editor;
return f;
}
test() {
this.files = TEST_UPDATE;
}
}
export default class Updater extends Module {
constructor(bd) {
super();
this.bd = bd;
}
bindings() {
this.checkForUpdates = this.checkForUpdates.bind(this);
this.checkForBdUpdates = this.checkForBdUpdates.bind(this);
this.updateAll = this.updateAll.bind(this);
this.updateFinished = this.updateFinished.bind(this);
this.start = this.start.bind(this);
}
events(ipc) {
ipc.on('updater-startUpdate', (_, updates) => {
clearInterval(this.updaterThread);
this.updateAll(updates);
});
ipc.on('debug-updater-forceUpdate', () => {
this.checkForUpdates(true);
});
}
async updateBd(update) {
try {
console.log('[BetterDiscord:Updater] Updating', update.id);
await this.downloadTarGz(`https://github.com/JsSucks/BetterDiscordApp${update.remote}`, this.bd.config.getPath('base'));
this.updateFinished(update);
// Cleanup
await FileUtils.rm(`${this.bd.config.getPath(update.id)}_old`);
} catch (err) {
console.log('[BetterDiscord:Updater] Failed to update', update.id);
console.log(err);
update.error = err;
this.bd.sendToDiscord('updater-updateError', update);
}
}
async updateAll(updates) {
const bd = updates.bd || [];
const plugins = updates.plugins || [];
const themes = updates.themes || [];
const modules = updates.modules || [];
this.restartRequired = this.reloadRequired = false;
this.finishedUpdates = 0;
this.totalUpdates = bd.length + plugins.length + themes.length + modules.length;
const renamed = [];
// TODO cleaner
if (bd.length) {
for (const update of bd) {
try {
await FileUtils.rm(`${this.bd.config.getPath(update.id)}_old`);
// Try to rename dirs first
await FileUtils.rn(this.bd.config.getPath(update.id), `${this.bd.config.getPath(update.id)}_old`);
renamed.push({ 'old': this.bd.config.getPath(update.id), 'new': `${this.bd.config.getPath(update.id)}_old`});
} catch (err) {
if (renamed.length) {
// Restore dirs
for (const r of renamed) {
await FileUtils.rn(r.new, r.old);
}
}
throw err;
}
}
for (const update of bd) {
this.updateBd(update);
}
}
}
updateFinished(update) {
if (update.id === 'core') this.restartRequired = true;
if (update.id === 'client') this.reloadRequired = true;
console.log('[BetterDiscord:Updater] Finished updating', update.id);
this.bd.sendToDiscord('updater-updateFinished', update);
this.finishedUpdates++;
if (this.finishedUpdates >= this.totalUpdates) {
this.bd.sendToDiscord('updater-updated', { restartRequired: this.restartRequired, reloadRequired: this.reloadRequired });
}
}
start(interval = 30) {
this.updaterThread = setInterval(this.checkForUpdates, interval * 60 * 1000);
}
validate(releaseInfo) {
return releaseInfo &&
typeof releaseInfo === 'object' &&
releaseInfo.files &&
Array.isArray(releaseInfo.files) &&
releaseInfo.files.length >= 4;
}
async latestRelease() {
try {
const release = await Axi.github.api.get('repos/JsSucks/BetterDiscordApp/releases/latest'); // TODO replace with config
const releaseInfoAsset = release.data.assets.find(asset => asset.name === 'releaseinfo.json');
const releaseInfo = await Axi.get(releaseInfoAsset['browser_download_url']);
if (this.validate(releaseInfo.data)) return releaseInfo.data;
return this.latestReleaseFallback();
} catch (err) {
console.log(err);
return this.latestReleaseFallback();
}
}
async latestReleaseFallback() {
console.log('fallback');
}
async checkForBdUpdates(forced = false) {
try {
const { coreVersion, clientVersion, editorVersion } = this.bd.config;
const releaseInfo = new ReleaseInfo({ core: coreVersion, client: clientVersion, editor: editorVersion });
const latestRelease = await this.latestRelease();
if (forced) {
latestRelease.files = latestRelease.files.map(file => {
file.version = '10.0.0';
return file;
});
}
releaseInfo.files = latestRelease.files;
const updates = [];
const { core, client, editor } = releaseInfo;
if (!core.upToDate) updates.push(core);
if (!client.upToDate) updates.push(client);
if (!editor.upToDate) updates.push(editor);
return updates;
} catch (err) {
console.log('[BetterDiscord:Updater]', err);
return [];
}
}
async checkForUpdates(forced = false) {
console.log('[BetterDiscord:Updater] Checking for updates');
this.bd.sendToDiscord('updater-checkForUpdates', '');
try {
const bd = await this.checkForBdUpdates(forced);
const updates = { bd, haveUpdates: false };
if (bd.length) updates.haveUpdates = true;
if (!updates.haveUpdates) {
this.bd.sendToDiscord('updater-noUpdates', '');
return true;
}
this.bd.sendToDiscord('updater-updatesAvailable', updates);
return true;
} catch (err) {
console.log('[BetterDiscord:Updater]', err);
this.bd.sendToDiscord('updater-error', err);
return 'err';
}
}
async downloadTarGz(url, dest, responseType = 'stream', headers = null) {
try {
const stream = await Axi.axios({
url,
type: 'GET',
responseType,
headers: headers ||
{
'Content-Type': 'application/octet-stream',
'Accept': 'application/octet-stream'
}
});
return new Promise((resolve, reject) => {
stream.data.pipe(zlib.createGunzip()).pipe(tarfs.extract(dest)).on('finish', resolve).on('error', reject);
});
} catch (err) {
throw err;
}
}
debug(releaseInfo) {
const { core, client, editor } = releaseInfo;
if (!core.upToDate) {
console.log(`[BetterDiscord:Updater] Core update available: ${core.currentVersion} > ${core.version}`);
} else {
console.log(`[BetterDiscord:Updater] Core up to date: ${core.currentVersion} = ${core.version}`);
}
if (!client.upToDate) {
console.log(`[BetterDiscord:Updater] Client update available: ${client.currentVersion} > ${client.version}`);
} else {
console.log(`[BetterDiscord:Updater] Client up to date: ${client.currentVersion} = ${client.version}`);
}
if (!editor.upToDate) {
console.log(`[BetterDiscord:Updater] Editor update available: ${editor.currentVersion} > ${editor.version}`);
} else {
console.log(`[BetterDiscord:Updater] Editor up to date: ${editor.currentVersion} = ${editor.version}`);
}
}
}

View File

@ -11,6 +11,7 @@
// TODO Use common
import fs from 'fs';
import rimraf from 'rimraf';
import Module from './modulebase';
import BDIpc from './bdipc';
@ -84,6 +85,20 @@ export class FileUtils {
});
}
static async ensureFile(path) {
try {
await this.fileExists(path);
return true;
} catch (err) {
try {
await this.writeFile(path, '');
return true;
} catch (err) {
throw err;
}
}
}
static async readJsonFromFile(path) {
let readFile;
try {
@ -99,6 +114,15 @@ export class FileUtils {
}
}
static async writeFile(path, data, options = {}) {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, options, err => {
if (err) return reject(err);
resolve();
});
});
}
static async listDirectory(path) {
try {
await this.directoryExists(path);
@ -155,6 +179,27 @@ export class FileUtils {
}
}
}
static async rm(path) {
return new Promise((resolve, reject) => {
rimraf(path, err => {
if (err) {
console.log(err);
reject(err);
}
resolve();
});
});
}
static async rn(oldPath, newPath) {
return new Promise((resolve, reject) => {
fs.rename(oldPath, newPath, err => {
if (err) return reject(err);
resolve();
});
});
}
}
export class WindowUtils extends Module {

View File

@ -1,191 +0,0 @@
<template>
<div class="container">
<div class="titlebar">
<div class="draggable"></div>
<div class="icon">
<div class="inner"></div>
</div>
<div class="title">CSS Editor</div>
<div class="flex-spacer"></div>
<div class="controls">
<button :class="{active: alwaysOnTop}" ref="aot" title="Toggle always on top" @click="toggleaot">P</button>
<button title="Close CSS Editor" @click="close">X</button>
</div>
</div>
<div id="spinner" v-if="loading">
<div class="valign">Loading Please Wait...</div>
</div>
<div id="editor" class="editor">
<codemirror ref="mycm" :options="cmOptions" @input="cmOnChange" />
</div>
<div class="parser-error" v-if="error">{{ error.formatted }}</div>
<div class="tools">
<div class="flex-row">
<button @click="save">Save</button>
<button @click="update">Update</button>
<div class="flex-spacer"></div>
<div id="chkboxLiveUpdate">
<label><input type="checkbox" @click="toggleLiveUpdate" v-model="liveUpdate" /><span>Live Update</span></label>
</div>
</div>
</div>
</div>
</template>
<script>
import { ClientIPC } from 'common';
import { remote } from 'electron';
import 'codemirror/addon/scroll/simplescrollbars.js';
import 'codemirror/mode/css/css.js';
import 'codemirror/addon/hint/css-hint.js';
import 'codemirror/addon/search/search.js';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/hint/show-hint.js';
const ExcludedIntelliSenseTriggerKeys = {
'8': 'backspace',
'9': 'tab',
'13': 'enter',
'16': 'shift',
'17': 'ctrl',
'18': 'alt',
'19': 'pause',
'20': 'capslock',
'27': 'escape',
'33': 'pageup',
'34': 'pagedown',
'35': 'end',
'36': 'home',
'37': 'left',
'38': 'up',
'39': 'right',
'40': 'down',
'45': 'insert',
'46': 'delete',
'91': 'left window key',
'92': 'right window key',
'93': 'select',
'107': 'add',
'109': 'subtract',
'110': 'decimal point',
'111': 'divide',
'112': 'f1',
'113': 'f2',
'114': 'f3',
'115': 'f4',
'116': 'f5',
'117': 'f6',
'118': 'f7',
'119': 'f8',
'120': 'f9',
'121': 'f10',
'122': 'f11',
'123': 'f12',
'144': 'numlock',
'145': 'scrolllock',
'186': 'semicolon',
'187': 'equalsign',
'188': 'comma',
'189': 'dash',
'190': 'period',
'191': 'slash',
'192': 'graveaccent',
'220': 'backslash',
'222': 'quote'
};
export default {
data() {
return {
loading: true,
codeMirror: null,
alwaysOnTop: false,
liveUpdate: false,
cmOptions: {
indentUnit: 4,
tabSize: 4,
mode: 'text/x-scss',
lineNumbers: true,
theme: 'material',
scrollbarStyle: 'overlay',
extraKeys: {
'Ctrl-Space': 'autocomplete'
},
dialog: {
'position': 'bottom'
}
},
error: null
}
},
computed: {
codemirror() {
return this.$refs.mycm.codemirror;
},
CodeMirror() {
return this.$refs.mycm;
}
},
created() {
ClientIPC.on('set-scss', (_, scss) => this.setScss(scss));
ClientIPC.on('scss-error', (_, err) => {
this.error = err;
this.$forceUpdate();
if (err)
console.error('SCSS parse error:', err);
});
ClientIPC.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate);
},
mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp);
(async () => {
this.setScss(await ClientIPC.sendToDiscord('get-scss'));
this.liveUpdate = await ClientIPC.sendToDiscord('get-liveupdate');
})();
},
watch: {
liveUpdate(liveUpdate) {
ClientIPC.sendToDiscord('set-liveupdate', liveUpdate);
}
},
methods: {
save() {
const scss = this.codemirror.getValue();
ClientIPC.sendToDiscord('save-scss', scss);
},
update() {
const scss = this.codemirror.getValue();
ClientIPC.sendToDiscord('update-scss', scss);
},
toggleaot() {
this.alwaysOnTop = !this.alwaysOnTop;
remote.getCurrentWindow().setAlwaysOnTop(this.alwaysOnTop);
},
close() {
window.close();
},
setScss(scss) {
this.loading = false;
this.codemirror.setValue(scss || '');
},
cmOnChange(value) {
if(this.liveUpdate) ClientIPC.sendToDiscord('update-scss', value);
},
cmOnKeyUp(editor, event) {
if (event.ctrlKey) return;
if (ExcludedIntelliSenseTriggerKeys[event.keyCode]) return;
cmCommands.autocomplete(editor, null, { completeSingle: false });
},
toggleLiveUpdate(e) {
this.liveUpdate = !this.liveUpdate;
}
}
}
</script>

View File

@ -1,105 +0,0 @@
.CodeMirror-scroll {
cursor: text;
}
.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler {
background: #38444a;
}
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: rgb(41, 43, 47);
}
.CodeMirror-overlayscroll-horizontal,
.CodeMirror-overlayscroll-horizontal div {
height: 10px;
}
.CodeMirror-overlayscroll-vertical,
.CodeMirror-overlayscroll-vertical div {
width: 10px;
}
.CodeMirror-scrollbar-filler {
width: 10px;
height: 10px;
background: rgb(41, 43, 47);
}
.cm-s-material.CodeMirror {
background: #36393f;
}
.CodeMirror-scroll {
cursor: text;
}
.cm-s-material .CodeMirror-gutters {
background: #292b2f;
}
.CodeMirror-gutter {
min-width: 34px;
border-right: 1px solid hsla(218,5%,47%,.3);
cursor: default;
}
.CodeMirror-hints {
/*background: #1e262a;*/
background: #292b2f;
box-shadow: 2px 3px 5px rgba(4, 4, 4, 0.22);
border: 1px solid #262f33;
&::-webkit-scrollbar {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,.4);
border-color: transparent;
}
&::-webkit-scrollbar-thumb,
&::-webkit-scrollbar-track {
background-clip: padding-box;
border-width: 3px;
border-style: solid;
border-radius: 7px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
border-color: transparent;
}
}
.CodeMirror-linenumber,
.CodeMirror-line {
padding: 0 5px !important;
}
.CodeMirror-linenumber {
cursor: text;
}
.cm-s-material .CodeMirror-linenumber {
color: #f6f6f7;
}
.CodeMirror-hint {
color: #bac9d2;
}
li.CodeMirror-hint-active {
color: #bac9d2;
/*background: #3b4950;*/
background: #36393f;
}
.CodeMirror-dialog-top {
bottom: 0;
top: auto;
border: none;
background: #1e262a;
}

View File

@ -1,17 +0,0 @@
.editor {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
.vue-codemirror {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
&, & .CodeMirror {
flex-grow: 1;
}
}
}

View File

@ -1,13 +0,0 @@
@import '../../../node_modules/codemirror/lib/codemirror.css';
@import '../../../node_modules/codemirror/theme/material.css';
@import '../../../node_modules/codemirror/addon/scroll/simplescrollbars.css';
@import '../../../node_modules/codemirror/addon/dialog/dialog.css';
@import '../../../node_modules/codemirror/addon/hint/show-hint.css';
@import './images.scss';
@import './main.scss';
@import './titlebar.scss';
@import './spinner.scss';
@import './editor.scss';
@import './tools.scss';
@import './codemirror.scss';

View File

@ -1,14 +0,0 @@
#spinner {
background: rgba(51, 48, 48, 0.41);
position: absolute;
top: 34px;
left: 0;
right: 0;
bottom: 0;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
font-weight: 600;
font-size: 2em;
z-index: 90000;
user-select: none;
}

View File

@ -1,66 +0,0 @@
.parser-error {
padding: 4px 6px;
background: #292b2f;
border-top: 1px solid hsla(218,5%,47%,.3);
color: #d84040;
font-family: monospace;
white-space: pre-wrap;
font-size: 12px;
}
.tools {
height: 36px;
background: #292b2f;
border-top: 1px solid hsla(218,5%,47%,.3);
display: flex;
flex-direction: column;
user-select: none;
.flex-row {
flex-grow: 1;
padding: 4px 5px;
}
button {
border-radius: 3px;
width: 100px;
padding: 3px 10px;
font-size: 12px;
font-weight: 600;
background: #36393f;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
transition: background-color .2s ease, color .2s ease;
cursor: pointer;
border: 0;
margin-right: 4px;
flex: 0 0 auto;
&:hover {
background: #44474e;
color: #fff;
}
}
#chkboxLiveUpdate {
padding: 3px 10px;
line-height: 22px;
flex: 0 0 auto;
label {
cursor: pointer;
}
input[type="checkbox"] {
margin: 0 6px 0 0;
cursor: pointer;
}
span {
font-size: 12px;
font-weight: 500;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
}
}
}

View File

@ -1,46 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const vueLoader = {
test: /\.(vue)$/,
exclude: /node_modules/,
loader: 'vue-loader'
};
const scssLoader = {
test: /\.(css|scss)$/,
loader: ['css-loader', 'sass-loader']
};
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'csseditor-release.js'
},
module: {
loaders: [vueLoader, scssLoader]
},
externals: {
electron: 'window.require("electron")',
fs: 'window.require("fs")',
util: 'window.require("util")',
process: 'require("process")'
},
resolve: {
alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
},
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules')
]
},
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true)
}),
new UglifyJsPlugin()
]
};

View File

@ -1,11 +1,11 @@
{
"name": "bdcsseditor",
"description": "BetterDiscord css editor package",
"author": "Jiiks",
"name": "bdeditorwindow",
"description": "BetterDiscord editor window",
"author": "JsSucks",
"version": "0.4.0",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "dist/csseditor.js",
"main": "dist/editor.js",
"contributors": [
"Jiiks",
"Pohky"

275
editor/src/Editor.vue Normal file
View File

@ -0,0 +1,275 @@
<template>
<div class="container">
<div class="titlebar">
<div class="draggable"></div>
<div class="icon">
<div class="inner"></div>
</div>
<div class="title">Content Editor</div>
<div class="flex-spacer"></div>
<div class="controls">
<button :class="{active: alwaysOnTop}" ref="aot" title="Toggle always on top" @click="toggleaot">P</button>
<button title="Close CSS Editor" @click="close">X</button>
</div>
</div>
<BDEdit :files="files"
:parentLoading="loading"
:snippets="snippets"
:updateContent="updateContent"
:runScript="runScript"
:newFile="newFile"
:saveFile="saveFile"
:newSnippet="newSnippet"
:saveSnippet="saveSnippet"
:readFile="readFile"
:readSnippet="readSnippet"
:injectStyle="injectStyle"
:toggleLiveUpdate="toggleLiveUpdate"
:ctxAction="ctxAction"
:toast="toast"/>
</div>
</template>
<script>
import { ClientIPC } from 'common';
import { remote } from 'electron';
import { BDEdit } from 'bdedit';
ace.acequire = ace.require;
const modes = {
'css': 'css',
'scss': 'scss',
'js': 'javascript',
'txt': 'text',
'json': 'json'
};
function resolveMode(fileName) {
if (!fileName.includes('.')) return 'text';
const ext = fileName.substr(fileName.lastIndexOf('.') + 1);
if (modes.hasOwnProperty(ext)) return modes[ext];
return 'text';
}
export default {
data() {
return {
files: [],
snippets: [],
loading: true,
alwaysOnTop: false,
error: undefined,
lastSaved: undefined,
toast: { active: false, msg: '' }
}
},
components: { BDEdit },
created() {
ClientIPC.on('bd-editor-addFile', (_, file) => {
if (this.files.find(f => f.name === file.name)) return;
this.addFile(file);
});
ClientIPC.on('bd-editor-remFile', (_, file) => {
this.files = this.files.filter(f => f.name !== file.name);
});
ClientIPC.on('bd-editor-addSnippet', (_, snippet) => {
if (this.snippets.find(s => s.name === snippet.name)) return;
this.addSnippet(snippet);
});
ClientIPC.on('bd-editor-remSnippet', (_, snippet) => {
this.snippets = this.snippets.filter(s => s.name !== snippet.name);
});
ClientIPC.on('bd-editor-fileChange', (_, file) => {
if (this.lastSaved && this.lastSaved.name === file.name) { // If we saved in our editor then don't trigger
this.lastSaved = undefined;
return;
}
const f = this.files.find(f => f.name === file.name);
if (f) f.changed = true;
});
},
async mounted() {
this.files = await ClientIPC.send('bd-editor-getFiles');
this.snippets = await ClientIPC.send('bd-editor-getSnippets');
this.loading = false;
},
methods: {
addFile(file) {
this.files.push(file);
},
addSnippet(snippet) { this.snippets.push(file) },
updateContent(item, content) {
item.content = content;
item.saved = item.content === item.savedContent;
if (this.liveUpdateTimeout) clearTimeout(this.liveUpdateTimeout);
if (item.liveUpdateEnabled) {
this.liveUpdateTimeout = setTimeout(() => {
this.injectStyle(item);
}, 5000);
}
},
async runScript(script) {
return ClientIPC.send('editor-runScript', script);
},
newFile(fileName) {
const prefix = fileName;
const mode = resolveMode(fileName);
let newName = prefix;
let iter = 0;
while (this.files.find(file => file.name === newName)) {
newName = `${prefix.split('.')[0]}_${iter}.${prefix.split('.')[1]}`;
iter++;
}
const newItem = { type: 'file', name: newName, content: '', mode, saved: false, read: true };
this.files.push(newItem);
return newItem;
},
newSnippet(snippetName) {
const prefix = snippetName;
const mode = resolveMode(snippetName);
let newName = prefix;
let iter = 0;
while (this.snippets.find(snippet => snippet.name === newName)) {
newName = `${prefix}_${iter}`;
iter++;
}
const newItem = { type: 'snippet', name: newName, content: '', savedContent: '', mode, saved: false, read: true };
this.snippets.push(newItem);
return newItem;
},
async saveFile(file) {
try {
this.lastSaved = file;
const result = await ClientIPC.send('bd-editor-saveFile', file);
file.savedContent = file.content;
file.saved = true;
} catch (err) {
console.log(err);
}
},
async saveSnippet(snippet) {
snippet.saved = true;
snippet.savedContent = snippet.content;
const result = await ClientIPC.send('bd-editor-saveSnippet', this.snippets.map(snippet => {
return {
name: snippet.name,
content: snippet.savedContent
}
}));
},
async readFile(file) {
const content = await ClientIPC.send('editor-readFile', file);
file.read = true;
file.changed = false;
file.content = file.savedContent = content;
return content;
},
async readSnippet(snippet) {
const content = await ClientIPC.send('editor-readSnippet', snippet);
snippet.read = true;
return content;
},
async injectStyle(item) {
const style = item.content.length <= 0 ? ' ' : item.content;
const result = await ClientIPC.send('bd-editor-injectStyle', { id: item.name.split('.')[0], style, mode: item.mode });
return result;
},
async ctxAction(action, item) {
if (action === 'reveal') {
ClientIPC.send('explorer', { 'static': 'userfiles' });
return;
}
if (action === 'copy') {
remote.clipboard.writeText(item.content);
return;
}
if (action === 'copyPath') {
const fullPath = await ClientIPC.send('getPath', ['userfiles', item.name]);
remote.clipboard.writeText(fullPath);
return;
}
if (action === 'rename') { // TODO select correct file after
this.loading = true;
const { oldName, newName } = item;
if (this.files.find(f => f.name === newName)) {
this.loading = false;
this.showToast('err', `File ${newName} already exists`);
return {
err: `File ${newName} already exists`
};
}
try {
await ClientIPC.send('rnFile', { oldName: ['userfiles', oldName], newName: ['userfiles', newName] });
return item;
} catch (err) {
console.log(err);
return { err };
} finally {
this.loading = false;
}
}
if (action === 'delete') {
this.loading = true;
try {
await ClientIPC.send('rmFile', ['userfiles', item.name]);
} catch (err) {
console.log(err);
} finally {
this.loading = false;
}
return;
}
},
showToast(type, msg, timeout = 3000) {
this.toast = { active: true, type, msg };
setTimeout(() => { this.toast.active = false }, timeout);
},
toggleLiveUpdate(item) {
item.liveUpdateEnabled = !item.liveUpdateEnabled;
return item;
},
toggleaot() {
this.alwaysOnTop = !this.alwaysOnTop;
remote.getCurrentWindow().setAlwaysOnTop(this.alwaysOnTop);
},
close() {
window.close();
}
}
}
</script>

View File

@ -1,15 +1,10 @@
// const styles = require('./styles/index.scss');
import Vue from 'vue';
import VueCodemirror from 'vue-codemirror';
import Editor from './Editor.vue';
import styles from './styles/index.scss';
Vue.use(VueCodemirror, {});
window.cmCommands = VueCodemirror.CodeMirror.commands;
const mount = document.createElement('div');
mount.classList.add('container');
document.body.appendChild(mount);

View File

@ -1,36 +1,20 @@
@import './vars.scss';
@import './titlebar.scss';
html, body {
margin: 0;
padding: 0;
display: flex;
max-height: 100%;
height: 100%;
width: 100%;
background: #2c383e;
min-width: 700px;
min-height: 400px;
}
* {
outline: none;
}
.flex-spacer {
flex-grow: 1;
}
.flex-row {
display: flex;
flex-direction: row;
}
.valign {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.container {
display: flex;
flex-grow: 1;
flex-direction: column;
height: 100%;
}

View File

@ -28,12 +28,17 @@
font-size: 15px;
}
.flex-spacer {
flex-grow: 1;
}
.controls {
margin: 0 0 0 2px;
font-size: 0;
button {
-webkit-app-region: no-drag;
outline: none !important;
border-radius: 3px;
width: 25px;
font-size: 12px;
@ -50,12 +55,12 @@
margin: 0 0 0 4px;
&:hover {
background: #44474e;
background: $bdGreen;
color: #FFF;
}
&.active {
background: #3a71c1;
background: $bdGreen;
}
}
}

Some files were not shown because too many files have changed in this diff Show More