diff --git a/client/src/index.js b/client/src/index.js index b88fc2d9..e774f928 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -20,6 +20,7 @@ class BetterDiscord { window.ClientIPC = ClientIPC; window.css = CssEditor; window.pm = PluginManager; + window.tm = ThemeManager; window.events = Events; window.wpm = WebpackModules; window.bdsettings = Settings; diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js index d0ad9266..034bd3f9 100644 --- a/client/src/modules/contentmanager.js +++ b/client/src/modules/contentmanager.js @@ -195,7 +195,7 @@ export default class { userConfig.enabled = readUserConfig.enabled || false; userConfig.config.merge({ settings: readUserConfig.config }); userConfig.config.setSaved(); - userConfig.css = readUserConfig.css || null; + userConfig.data = readUserConfig.data || undefined; } catch (err) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/ console.info(`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`); console.error(err); diff --git a/client/src/modules/csseditor.js b/client/src/modules/csseditor.js index a72a1cc0..2fd25508 100644 --- a/client/src/modules/csseditor.js +++ b/client/src/modules/csseditor.js @@ -11,6 +11,7 @@ import { ClientIPC } from 'common'; import Settings from './settings'; import { DOM } from 'ui'; +import filewatcher from 'filewatcher'; /** * Custom css editor communications @@ -29,6 +30,12 @@ export default class { await this.updateScss(scss); await this.save(); }); + + this.filewatcher = filewatcher(); + this.filewatcher.on('change', (file, stat) => { + // Recompile SCSS + this.updateScss(this.scss); + }); } /** @@ -54,9 +61,10 @@ export default class { } return new Promise((resolve, reject) => { - this.compile(scss).then(css => { - this.css = css; + this.compile(scss).then(result => { + this.css = result.css.toString(); this._scss = scss; + this.files = result.stats.includedFiles; this.sendToEditor('scss-error', null); resolve(); }).catch(err => { @@ -87,7 +95,9 @@ export default class { * @param {String} scss scss string */ static async compile(scss) { - return await ClientIPC.send('bd-compileSass', { data: scss }); + const result = await ClientIPC.send('bd-compileSass', { data: scss }); + console.log('Custom CSS SCSS compiler result:', result, '- CSS:', result.css.toString()); + return result; } /** @@ -121,4 +131,33 @@ export default class { DOM.injectStyle(css, 'bd-customcss'); } + /** + * An array of files that are being watched for changes. + * @returns {Array} Files being watched + */ + static get files() { + return this._files || (this._files = []); + } + + /** + * Sets all files to be watched. + * @param {Array} files Files to watch + */ + static set files(files) { + for (let file of files) { + if (!this.files.includes(file)) { + this.filewatcher.add(file); + this.files.push(file); + } + } + + for (let index in this.files) { + const file = this.files[index]; + if (!files.includes(file)) { + this.filewatcher.remove(file); + this.files.splice(index, 1); + } + } + } + } diff --git a/client/src/modules/plugin.js b/client/src/modules/plugin.js index f1c67f9d..d407d7f7 100644 --- a/client/src/modules/plugin.js +++ b/client/src/modules/plugin.js @@ -68,6 +68,7 @@ export default class Plugin { get settings() { return this.userConfig.config } get config() { return this.settings.settings } get pluginConfig() { return this.config } + get data() { return this.userConfig.data } get exports() { return this._exports ? this._exports : (this._exports = this.getExports()) } get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new PluginEvents(this)) } @@ -96,7 +97,8 @@ export default class Plugin { try { await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({ enabled: this.enabled, - config: this.settings.strip().settings + config: this.settings.strip().settings, + data: this.data })); this.settings.setSaved(); diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js index 6f07d70d..1609c4f3 100644 --- a/client/src/modules/pluginapi.js +++ b/client/src/modules/pluginapi.js @@ -115,7 +115,7 @@ export default class PluginApi { async injectSass(id, scss, options) { // In most cases a plugin's styles should be precompiled instead of using this if (id && !scss && !options) scss = id, id = undefined; - const css = await this.compileSass(scss, options); + const css = (await this.compileSass(scss, options)).css.toString(); this.injectStyle(id, css, options); } deleteStyle(id) { diff --git a/client/src/modules/settings.js b/client/src/modules/settings.js index d863cc7b..d88f7657 100644 --- a/client/src/modules/settings.js +++ b/client/src/modules/settings.js @@ -27,7 +27,7 @@ export default new class Settings { const settingsPath = path.resolve(this.dataPath, 'user.settings.json'); const user_config = await FileUtils.readJsonFromFile(settingsPath); - const { settings, scss, css_editor_bounds } = user_config; + const { settings, scss, css_editor_bounds, css_editor_files } = user_config; this.settings = defaultSettings.map(set => { const newSet = new SettingsSet(set); @@ -48,6 +48,7 @@ export default new class Settings { CssEditor.updateScss(scss, true); CssEditor.editor_bounds = css_editor_bounds || {}; + CssEditor.files = css_editor_files || []; } catch (err) { // There was an error loading settings // This probably means that the user doesn't have any settings yet @@ -68,7 +69,8 @@ export default new class Settings { height: CssEditor.editor_bounds.height, x: CssEditor.editor_bounds.x, y: CssEditor.editor_bounds.y - } + }, + css_editor_files: CssEditor.files }); for (let set of this.getSettings) { diff --git a/client/src/modules/theme.js b/client/src/modules/theme.js index 3f25426a..7208b217 100644 --- a/client/src/modules/theme.js +++ b/client/src/modules/theme.js @@ -13,6 +13,7 @@ import { EventEmitter } from 'events'; import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs'; import { DOM, Modals } from 'ui'; import { Utils, FileUtils, ClientIPC, ClientLogger as Logger } from 'common'; +import filewatcher from 'filewatcher'; class ThemeEvents { constructor(theme) { @@ -45,6 +46,15 @@ export default class Theme { this.settings.on('setting-updated', event => this.events.emit('setting-updated', event)); this.settings.on('settings-updated', event => this.events.emit('settings-updated', event)); this.settings.on('settings-updated', event => this.recompile()); + + this.filewatcher = filewatcher(); + const files = this.files; + this.data.files = []; + this.files = files; + this.filewatcher.on('change', (file, stat) => { + // Recompile SCSS + this.recompile(); + }); } get configs() { return this.__themeInternals.configs } @@ -68,7 +78,8 @@ export default class Theme { get settings() { return this.userConfig.config } get config() { return this.settings.settings } get themeConfig() { return this.config } - get css() { return this.userConfig.css } + get data() { return this.userConfig.data || (this.userConfig.data = {}) } + get css() { return this.data.css } get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ThemeEvents(this)) } showSettingsModal() { @@ -90,7 +101,7 @@ export default class Theme { await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({ enabled: this.enabled, config: this.settings.strip().settings, - css: this.css + data: this.data })); this.settings.setSaved(); @@ -116,29 +127,36 @@ export default class Theme { async compile() { console.log('Compiling CSS'); - let css = ''; if (this.info.type === 'sass') { const config = await ThemeManager.getConfigAsSCSS(this.settings); - css = await ClientIPC.send('bd-compileSass', { + const result = await ClientIPC.send('bd-compileSass', { data: config, path: this.paths.mainPath.replace(/\\/g, '/') }); + console.log('SCSS compiler result:', result); Logger.log(this.name, ['Finished compiling theme', new class Info { get SCSS_variables() { console.log(config); } get Compiled_SCSS() { console.log(css); } + get Result() { console.log(result); } }]); - } else { - css = await FileUtils.readFile(this.paths.mainPath); - } - return css; + return { + css: result.css.toString(), + files: result.stats.includedFiles + }; + } else { + return { + css: FileUtils.readFile(this.paths.mainPath) + }; + } } async recompile() { - const css = await this.compile(); - this.userConfig.css = css; + const data = await this.compile(); + this.data.css = data.css; + this.files = data.files; await this.saveConfiguration(); @@ -148,4 +166,37 @@ export default class Theme { } } + /** + * An array of files that are being watched for changes. + * @returns {Array} Files being watched + */ + get files() { + return this.data.files || (this.data.files = []); + } + + /** + * Sets all files to be watched. + * @param {Array} files Files to watch + */ + set files(files) { + if (!files) files = []; + + for (let file of files) { + if (!this.files.includes(file)) { + this.filewatcher.add(file); + this.files.push(file); + Logger.log(this.name, `Watching file ${file} for changes`); + } + } + + for (let index in this.files) { + const file = this.files[index]; + if (!files.includes(file)) { + this.filewatcher.remove(file); + this.files.splice(index, 1); + Logger.log(this.name, `No longer watching file ${file} for changes`); + } + } + } + } diff --git a/core/src/main.js b/core/src/main.js index 8a4414f1..96c42507 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -84,7 +84,7 @@ class Comms { o.reply({ err }); return; } - o.reply(result.css.toString()); + o.reply(result); }); }); } diff --git a/package-lock.json b/package-lock.json index ef0acbc3..b6bca9da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2423,6 +2423,12 @@ "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", "dev": true }, + "debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", + "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3439,6 +3445,15 @@ "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", "dev": true }, + "filewatcher": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/filewatcher/-/filewatcher-3.0.1.tgz", + "integrity": "sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ=", + "dev": true, + "requires": { + "debounce": "1.1.0" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", diff --git a/package.json b/package.json index 9b8d83e5..7cd77276 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "eslint": "^4.16.0", "eslint-plugin-vue": "^4.3.0", "file-type": "^7.6.0", + "filewatcher": "^3.0.1", "gulp": "^3.9.1", "gulp-babel": "^7.0.0", "gulp-plumber": "^1.2.0", diff --git a/tests/example-scss-file.scss b/tests/example-scss-file.scss new file mode 100644 index 00000000..494f1876 --- /dev/null +++ b/tests/example-scss-file.scss @@ -0,0 +1,6 @@ +// Import this file in the custom CSS editor with +// @import '{path-to-betterdiscord}/tests/example-scss-file.scss'; +// and when it is changed your custom CSS will be recompiled +.bd-settings { + height: 50%; +}