From 91275f433237e4d8bb143c290a550210c869dc30 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sat, 3 Mar 2018 23:36:17 +0000 Subject: [PATCH] CSS editor improvements --- client/src/data/user.settings.default.json | 26 ++ client/src/modules/contentmanager.js | 2 +- client/src/modules/csseditor.js | 230 ++++++++++++++---- client/src/modules/extmodule.js | 1 + client/src/modules/plugin.js | 2 +- client/src/modules/pluginapi.js | 4 + client/src/modules/settings.js | 12 +- client/src/modules/theme.js | 63 +++-- client/src/structs/settings/settingsset.js | 4 + .../styles/partials/generic/forms/main.scss | 4 +- client/src/styles/partials/helpers.scss | 15 ++ client/src/styles/partials/index.scss | 1 + client/src/ui/components/BdSettings.vue | 2 +- client/src/ui/components/bd/CssEditor.vue | 83 +++++-- client/src/ui/dom.js | 20 +- csseditor/src/Editor.vue | 19 +- csseditor/src/styles/titlebar.scss | 3 +- csseditor/src/styles/tools.scss | 9 +- 18 files changed, 390 insertions(+), 110 deletions(-) create mode 100644 client/src/styles/partials/helpers.scss diff --git a/client/src/data/user.settings.default.json b/client/src/data/user.settings.default.json index 8d9b89a1..fc35312f 100644 --- a/client/src/data/user.settings.default.json +++ b/client/src/data/user.settings.default.json @@ -76,6 +76,32 @@ "headertext": "Emote Settings", "settings": [] }, + { + "id": "css", + "text": "CSS Editor", + "hidden": true, + "settings": [ + { + "category": "default", + "settings": [ + { + "id": "live-update", + "type": "bool", + "text": "Live update", + "hint": "Automatically recompile custom CSS when typing in the custom CSS editor.", + "value": true + }, + { + "id": "watch-files", + "type": "bool", + "text": "Watch included files", + "hint": "Automatically recompile theme and custom CSS when a file it imports is changed.", + "value": true + } + ] + } + ] + }, { "id": "security", "text": "Security", diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js index 034bd3f9..d726722e 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.data = readUserConfig.data || undefined; + userConfig.data = readUserConfig.data || {}; } 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 2fd25508..a430bcbc 100644 --- a/client/src/modules/csseditor.js +++ b/client/src/modules/csseditor.js @@ -8,20 +8,33 @@ * LICENSE file in the root directory of this source tree. */ -import { ClientIPC } from 'common'; +import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common'; import Settings from './settings'; import { DOM } from 'ui'; import filewatcher from 'filewatcher'; +import path from 'path'; +import electron from 'electron'; /** * Custom css editor communications */ -export default class { +export default new class { + + constructor() { + this._scss = ''; + this._css = ''; + this._error = undefined; + this.editor_bounds = undefined; + this._files = undefined; + this._filewatcher = undefined; + this._watchfiles = undefined; + this.compiling = false; + } /** * Init css editor */ - static init() { + init() { ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss })); ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss)); ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds)); @@ -31,17 +44,25 @@ export default class { await this.save(); }); - this.filewatcher = filewatcher(); - this.filewatcher.on('change', (file, stat) => { - // Recompile SCSS - this.updateScss(this.scss); + this.liveupdate = Settings.getSetting('css', 'default', 'live-update'); + this.liveupdate.on('setting-updated', event => { + this.sendToEditor('set-liveupdate', event.value); + }); + + ClientIPC.on('bd-get-liveupdate', () => this.sendToEditor('set-liveupdate', this.liveupdate.value)); + ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value); + + this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files'); + this.watchfilessetting.on('setting-updated', event => { + if (event.value) this.watchfiles = this.files; + else this.watchfiles = []; }); } /** * Show css editor, flashes if already visible */ - static async show() { + async show() { await ClientIPC.send('openCssEditor', this.editor_bounds); } @@ -50,34 +71,35 @@ export default class { * @param {String} scss scss to compile * @param {bool} sendSource send to css editor instance */ - static updateScss(scss, sendSource) { + async updateScss(scss, sendSource) { if (sendSource) this.sendToEditor('set-scss', { scss }); if (!scss) { this._scss = this.css = ''; this.sendToEditor('scss-error', null); - return Promise.resolve(); + return; } - return new Promise((resolve, reject) => { - 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 => { - this.sendToEditor('scss-error', err); - reject(err); - }); - }); + try { + this.compiling = true; + const result = await this.compile(scss); + this.css = result.css.toString(); + this._scss = scss; + this.files = result.stats.includedFiles; + this.error = null; + this.compiling = false; + } catch (err) { + this.compiling = false; + this.error = err; + throw err; + } } /** * Save css to file */ - static async save() { + async save() { Settings.saveSettings(); } @@ -85,7 +107,7 @@ export default class { * Save current editor bounds * @param {Rectangle} bounds editor bounds */ - static saveEditorBounds(bounds) { + saveEditorBounds(bounds) { this.editor_bounds = bounds; Settings.saveSettings(); } @@ -94,70 +116,192 @@ export default class { * Send scss to core for compilation * @param {String} scss scss string */ - static async compile(scss) { - const result = await ClientIPC.send('bd-compileSass', { data: scss }); - console.log('Custom CSS SCSS compiler result:', result, '- CSS:', result.css.toString()); - return result; + async compile(scss) { + return await ClientIPC.send('bd-compileSass', { + data: scss, + path: await this.fileExists() ? this.filePath : undefined + }); } /** - * Send css to open editor + * Recompile the current SCSS + * @return {Promise} + */ + async recompile() { + return await this.updateScss(this.scss); + } + + /** + * Send data to open editor * @param {any} channel * @param {any} data */ - static async sendToEditor(channel, data) { + async sendToEditor(channel, data) { return await ClientIPC.send('sendToCssEditor', { channel, data }); } + /** + * Opens an SCSS file in a system editor + */ + async openSystemEditor() { + try { + await FileUtils.fileExists(this.filePath); + } catch (err) { + // File doesn't exist + // Create it + await FileUtils.writeFile(this.filePath, ''); + } + + Logger.log('CSS Editor', `Opening file ${this.filePath} in the user's default editor.`); + + // For some reason this doesn't work + // if (!electron.shell.openItem(this.filePath)) + if (!electron.shell.openExternal('file://' + this.filePath)) + throw {message: 'Failed to open system editor.'}; + } + + /** Set current state + * @param {String} scss Current uncompiled SCSS + * @param {String} css Current compiled CSS + * @param {String} files Files imported in the SCSS + * @param {String} err Current compiler error + */ + setState(scss, css, files, err) { + this._scss = scss; + this.sendToEditor('set-scss', { scss }); + this.css = css; + this.files = files; + this.error = err; + } + /** * Current uncompiled scss */ - static get scss() { + get scss() { return this._scss || ''; } /** * Set current scss */ - static set scss(scss) { + set scss(scss) { this.updateScss(scss, true); } + /** + * Current compiled css + */ + get css() { + return this._css || ''; + } + /** * Inject compiled css to head * {DOM} */ - static set css(css) { + set css(css) { + this._css = css; DOM.injectStyle(css, 'bd-customcss'); } /** - * An array of files that are being watched for changes. - * @returns {Array} Files being watched + * Current error */ - static get files() { + get error() { + return this._error || undefined; + } + + /** + * Set current error + * {DOM} + */ + set error(err) { + this._error = err; + this.sendToEditor('scss-error', err); + } + + /** + * An array of files that are imported in custom CSS. + * @return {Array} Files being watched + */ + get files() { return this._files || (this._files = []); } + /** + * Sets all files that are imported in custom CSS. + * @param {Array} files Files to watch + */ + set files(files) { + this._files = files; + if (Settings.get('css', 'default', 'watch-files')) + this.watchfiles = files; + } + + /** + * A filewatcher instance. + */ + get filewatcher() { + if (this._filewatcher) return this._filewatcher; + this._filewatcher = filewatcher(); + this._filewatcher.on('change', (file, stat) => { + // Recompile SCSS + this.recompile(); + }); + return this._filewatcher; + } + + /** + * An array of files that are being watched for changes. + * @return {Array} Files being watched + */ + get watchfiles() { + return this._watchfiles || (this._watchfiles = []); + } + /** * Sets all files to be watched. * @param {Array} files Files to watch */ - static set files(files) { + set watchfiles(files) { for (let file of files) { - if (!this.files.includes(file)) { + if (!this.watchfiles.includes(file)) { this.filewatcher.add(file); - this.files.push(file); + this.watchfiles.push(file); + Logger.log('CSS Editor', `Watching file ${file} for changes`); } } - for (let index in this.files) { - const file = this.files[index]; - if (!files.includes(file)) { + for (let index in this.watchfiles) { + let file = this.watchfiles[index]; + while (file && !files.find(f => f === file)) { this.filewatcher.remove(file); - this.files.splice(index, 1); + this.watchfiles.splice(index, 1); + Logger.log('CSS Editor', `No longer watching file ${file} for changes`); + file = this.watchfiles[index]; } } } + /** + * The path of the file the system editor should save to. + * @return {String} + */ + get filePath() { + return path.join(Settings.dataPath, 'user.scss'); + } + + /** + * Checks if the system editor's file exists. + * @return {Boolean} + */ + async fileExists() { + try { + await FileUtils.fileExists(this.filePath); + return true; + } catch (err) { + return false; + } + } + } diff --git a/client/src/modules/extmodule.js b/client/src/modules/extmodule.js index 1c25a342..66fbfafd 100644 --- a/client/src/modules/extmodule.js +++ b/client/src/modules/extmodule.js @@ -56,6 +56,7 @@ export default class ExtModule { get dirName() { return this.paths.dirName } get enabled() { return true } get config() { return this.userConfig.config || [] } + get data() { return this.userConfig.data || (this.userConfig.data = {}) } get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ExtModuleEvents(this)) } } diff --git a/client/src/modules/plugin.js b/client/src/modules/plugin.js index d407d7f7..f3e2efb0 100644 --- a/client/src/modules/plugin.js +++ b/client/src/modules/plugin.js @@ -68,7 +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 data() { return this.userConfig.data || (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)) } diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js index 1609c4f3..6977046c 100644 --- a/client/src/modules/pluginapi.js +++ b/client/src/modules/pluginapi.js @@ -105,6 +105,9 @@ export default class PluginApi { getConfigAsSCSS(settingsset) { return ThemeManager.getConfigAsSCSS(settingsset ? settingsset : this.plugin.settings); } + getConfigAsSCSSMap(settingsset) { + return ThemeManager.getConfigAsSCSSMap(settingsset ? settingsset : this.plugin.settings); + } injectStyle(id, css) { if (id && !css) css = id, id = undefined; this.deleteStyle(id); @@ -132,6 +135,7 @@ export default class PluginApi { return { compileSass: this.compileSass.bind(this), getConfigAsSCSS: this.getConfigAsSCSS.bind(this), + getConfigAsSCSSMap: this.getConfigAsSCSSMap.bind(this), injectStyle: this.injectStyle.bind(this), injectSass: this.injectSass.bind(this), deleteStyle: this.deleteStyle.bind(this), diff --git a/client/src/modules/settings.js b/client/src/modules/settings.js index d88f7657..49e0dc91 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, css_editor_files } = user_config; + const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds } = user_config; this.settings = defaultSettings.map(set => { const newSet = new SettingsSet(set); @@ -46,9 +46,8 @@ export default new class Settings { return newSet; }); - CssEditor.updateScss(scss, true); + CssEditor.setState(scss, css, css_editor_files, scss_error); 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 @@ -64,13 +63,15 @@ export default new class Settings { await FileUtils.writeJsonToFile(settingsPath, { settings: this.settings.map(set => set.strip()), scss: CssEditor.scss, + css: CssEditor.css, + css_editor_files: CssEditor.files, + scss_error: CssEditor.error, css_editor_bounds: { width: CssEditor.editor_bounds.width, 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) { @@ -90,6 +91,7 @@ export default new class Settings { get core() { return this.getSet('core') } get ui() { return this.getSet('ui') } get emotes() { return this.getSet('emotes') } + get css() { return this.getSet('css') } get security() { return this.getSet('security') } getCategory(set_id, category_id) { diff --git a/client/src/modules/theme.js b/client/src/modules/theme.js index 9471972c..9d1bf496 100644 --- a/client/src/modules/theme.js +++ b/client/src/modules/theme.js @@ -8,6 +8,7 @@ * LICENSE file in the root directory of this source tree. */ +import Settings from './settings'; import ThemeManager from './thememanager'; import { EventEmitter } from 'events'; import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs'; @@ -47,13 +48,11 @@ export default class Theme { 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(); + const watchfiles = Settings.getSetting('css', 'default', 'watch-files'); + if (watchfiles.value) this.watchfiles = this.files; + watchfiles.on('setting-updated', event => { + if (event.value) this.watchfiles = this.files; + else this.watchfiles = []; }); } @@ -166,34 +165,64 @@ export default class Theme { } /** - * An array of files that are being watched for changes. - * @returns {Array} Files being watched + * An array of files that are imported in custom CSS. + * @return {Array} Files being watched */ get files() { return this.data.files || (this.data.files = []); } /** - * Sets all files to be watched. + * Sets all files that are imported in custom CSS. * @param {Array} files Files to watch */ set files(files) { - if (!files) files = []; + this.data.files = files; + if (Settings.get('css', 'default', 'watch-files')) + this.watchfiles = files; + } + /** + * A filewatcher instance. + */ + get filewatcher() { + if (this._filewatcher) return this._filewatcher; + this._filewatcher = filewatcher(); + this._filewatcher.on('change', (file, stat) => { + // Recompile SCSS + this.recompile(); + }); + return this._filewatcher; + } + + /** + * An array of files that are being watched for changes. + * @return {Array} Files being watched + */ + get watchfiles() { + return this._watchfiles || (this._watchfiles = []); + } + + /** + * Sets all files to be watched. + * @param {Array} files Files to watch + */ + set watchfiles(files) { for (let file of files) { - if (!this.files.includes(file)) { + if (!this.watchfiles.includes(file)) { this.filewatcher.add(file); - this.files.push(file); + this.watchfiles.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)) { + for (let index in this.watchfiles) { + let file = this.watchfiles[index]; + while (file && !files.find(f => f === file)) { this.filewatcher.remove(file); - this.files.splice(index, 1); + this.watchfiles.splice(index, 1); Logger.log(this.name, `No longer watching file ${file} for changes`); + file = this.watchfiles[index]; } } } diff --git a/client/src/structs/settings/settingsset.js b/client/src/structs/settings/settingsset.js index 4b6d731a..8e21566a 100644 --- a/client/src/structs/settings/settingsset.js +++ b/client/src/structs/settings/settingsset.js @@ -50,6 +50,10 @@ export default class SettingsSet { return this.args.headertext || `${this.text} Settings`; } + get hidden() { + return this.args.hidden || false; + } + get categories() { return this.args.settings || []; } diff --git a/client/src/styles/partials/generic/forms/main.scss b/client/src/styles/partials/generic/forms/main.scss index 3f153e30..9ac4e8ce 100644 --- a/client/src/styles/partials/generic/forms/main.scss +++ b/client/src/styles/partials/generic/forms/main.scss @@ -23,10 +23,8 @@ .bd-hint { flex: 1 1 auto; - color: #72767d; - font-size: 14px; - font-weight: 500; margin-top: 5px; + margin-bottom: 0; line-height: 20px; border-bottom: 0px solid rgba(114, 118, 126, 0.1); } diff --git a/client/src/styles/partials/helpers.scss b/client/src/styles/partials/helpers.scss new file mode 100644 index 00000000..475688f9 --- /dev/null +++ b/client/src/styles/partials/helpers.scss @@ -0,0 +1,15 @@ +.bd-err { + color: $colerr; +} + +.bd-p { + color: $coldimwhite; + font-size: 14px; + font-weight: 500; + margin: 10px 0; +} + +.bd-hint { + @extend .bd-p; + color: #72767d; +} diff --git a/client/src/styles/partials/index.scss b/client/src/styles/partials/index.scss index bbe120da..ab9381d0 100644 --- a/client/src/styles/partials/index.scss +++ b/client/src/styles/partials/index.scss @@ -10,3 +10,4 @@ @import './profilebadges.scss'; @import './discordoverrides.scss'; +@import './helpers.scss'; diff --git a/client/src/ui/components/BdSettings.vue b/client/src/ui/components/BdSettings.vue index 9b8de2f7..da976a86 100644 --- a/client/src/ui/components/BdSettings.vue +++ b/client/src/ui/components/BdSettings.vue @@ -31,7 +31,7 @@ -
+
diff --git a/client/src/ui/components/bd/CssEditor.vue b/client/src/ui/components/bd/CssEditor.vue index f813b8d9..43840997 100644 --- a/client/src/ui/components/bd/CssEditor.vue +++ b/client/src/ui/components/bd/CssEditor.vue @@ -11,58 +11,97 @@