CSS editor improvements

This commit is contained in:
Samuel Elliott 2018-03-03 23:36:17 +00:00
parent 976aecd8f2
commit 91275f4332
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
18 changed files with 390 additions and 110 deletions

View File

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

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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)) }
}

View File

@ -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)) }

View File

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

View File

@ -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) {

View File

@ -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];
}
}
}

View File

@ -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 || [];
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -10,3 +10,4 @@
@import './profilebadges.scss';
@import './discordoverrides.scss';
@import './helpers.scss';

View File

@ -31,7 +31,7 @@
</div>
</div>
<ContentColumn slot="content">
<div v-for="set in Settings.settings" v-if="activeContent(set.id) || animatingContent(set.id)" :class="{active: activeContent(set.id), animating: animatingContent(set.id)}">
<div v-for="set in Settings.settings" v-if="!set.hidden && activeContent(set.id) || animatingContent(set.id)" :class="{active: activeContent(set.id), animating: animatingContent(set.id)}">
<SettingsWrapper :headertext="set.headertext">
<SettingsPanel :settings="set" :schemes="set.schemes" />
</SettingsWrapper>

View File

@ -11,58 +11,97 @@
<template>
<SettingsWrapper headertext="CSS Editor">
<div class="bd-css-editor">
<div v-if="CssEditor.error" class="bd-form-item">
<h5 style="margin-bottom: 10px;">Compiler error</h5>
<div class="bd-err bd-pre-wrap"><div class="bd-pre">{{ CssEditor.error.formatted }}</div></div>
<div class="bd-form-divider"></div>
</div>
<div class="bd-form-item">
<h5>Custom Editor</h5>
<div class="bd-form-warning">
<div class="bd-text">Custom Editor is not installed!</div>
<FormButton>Install</FormButton>
</div>
<span style="color: #FFF; font-size: 12px; font-weight: 700;">*This is displayed if editor is not installed</span>
<FormButton :onClick="openInternalEditor">Open</FormButton>
<FormButton v-if="internalEditorIsInstalled" :onClick="openInternalEditor">Open</FormButton>
<template v-else>
<div class="bd-form-warning">
<div class="bd-text">Custom Editor is not installed!</div>
<FormButton>Install</FormButton>
</div>
<span style="color: #fff; font-size: 12px; font-weight: 700;">* This is displayed if editor is not installed</span>
</template>
</div>
<div class="bd-form-divider"></div>
<Setting :setting="liveUpdateSetting" :change="enabled => liveUpdateSetting.value = enabled" />
<div class="bd-form-item">
<h5>System Editor</h5>
<FormButton>
Open
</FormButton>
<FormButton :onClick="openSystemEditor">Open</FormButton>
<p class="bd-hint">This will open {{ systemEditorPath }} in your system's default editor.</p>
</div>
<div class="bd-form-divider"></div>
<FormButton :onClick="() => {}">Enabled</FormButton>
<FormButton :disabled="true"><span>Disabled</span></FormButton>
<FormButton :loading="true" />
<div class="bd-form-item">
<h5 style="margin-bottom: 10px;">Settings</h5>
</div>
<SettingsPanel :settings="settingsset" />
<!-- <Setting :setting="liveUpdateSetting" />
<Setting :setting="watchFilesSetting" /> -->
<FormButton :onClick="recompile" :loading="compiling">Recompile</FormButton>
</div>
</SettingsWrapper>
</template>
<script>
// Imports
import { CssEditor } from 'modules';
import { Settings, CssEditor } from 'modules';
import { SettingsWrapper } from './';
import { FormButton } from '../common';
import SettingsPanel from './SettingsPanel.vue';
import Setting from './setting/Setting.vue';
export default {
components: {
SettingsWrapper,
SettingsPanel,
Setting,
FormButton
},
data() {
return {
liveUpdateSetting: {
id: "live-update",
type: "bool",
text: "Live Update",
hint: "Automatically update client css when saved.",
value: true
}
CssEditor
};
},
computed: {
error() {
return this.CssEditor.error;
},
compiling() {
return this.CssEditor.compiling;
},
systemEditorPath() {
return this.CssEditor.filePath;
},
liveUpdateSetting() {
return Settings.getSetting('css', 'default', 'live-update');
},
watchFilesSetting() {
return Settings.getSetting('css', 'default', 'watch-files');
},
settingsset() {
return Settings.css;
},
internalEditorIsInstalled() {
return true;
}
},
methods: {
openInternalEditor() {
CssEditor.show();
this.CssEditor.show();
},
openSystemEditor() {
this.CssEditor.openSystemEditor();
},
recompile() {
this.CssEditor.recompile();
}
}
}

View File

@ -116,33 +116,39 @@ export default class DOM {
return document.querySelector(e);
}
static getElements(e) {
return document.querySelectorAll(e);
}
static createElement(tag = 'div', className = null, id = null) {
return new BdNode(tag, className, id);
}
static deleteStyle(id) {
const exists = this.getElement(`bd-styles > #${id}`);
const exists = Array.from(this.bdStyles.children).find(e => e.id === id);
if (exists) exists.remove();
}
static injectStyle(css, id) {
this.deleteStyle(id);
this.bdStyles.append(this.createStyle(css, id));
const style = Array.from(this.bdStyles.children).find(e => e.id === id) || this.createElement('style', null, id).element;
style.textContent = css;
this.bdStyles.append(style);
}
static getStyleCss(id) {
const exists = this.getElement(`bd-styles > #${id}`);
const exists = this.bdStyles.children.find(e => e.id === id);
return exists ? exists.textContent : '';
}
static deleteTheme(id) {
const exists = this.getElement(`bd-themes > #${id}`);
const exists = Array.from(this.bdThemes.children).find(e => e.id === id);
if (exists) exists.remove();
}
static injectTheme(css, id) {
this.deleteTheme(id);
this.bdThemes.append(this.createStyle(css, id));
const style = Array.from(this.bdThemes.children).find(e => e.id === id) || this.createElement('style', null, id).element;
style.textContent = css;
this.bdThemes.append(style);
}
static createStyle(css, id) {

View File

@ -25,7 +25,7 @@
<button @click="update">Update</button>
<div class="flex-spacer"></div>
<div id="chkboxLiveUpdate">
<input type="checkbox" @click="toggleLiveUpdate" :checked="liveUpdate" /><span>Live Update</span>
<label><input type="checkbox" @click="toggleLiveUpdate" v-model="liveUpdate" /><span>Live Update</span></label>
</div>
</div>
</div>
@ -130,8 +130,7 @@
return this.$refs.mycm;
}
},
mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp);
created() {
BDIpc.on('set-scss', (_, data) => {
if (data.error) {
console.log(data.error);
@ -141,14 +140,24 @@
this.setScss(data.scss);
});
BDIpc.sendToDiscord('get-scss');
BDIpc.on('scss-error', (_, err) => {
this.error = err;
this.$forceUpdate();
if (err)
console.error('SCSS parse error:', err);
});
BDIpc.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate);
},
mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp);
BDIpc.sendToDiscord('get-scss');
BDIpc.sendToDiscord('get-liveupdate');
},
watch: {
liveUpdate(liveUpdate) {
BDIpc.sendToDiscord('set-liveupdate', liveUpdate);
}
},
methods: {
save() {

View File

@ -38,11 +38,10 @@
width: 25px;
font-size: 12px;
font-weight: 600;
/*background: #263238;*/
background: #36393f;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
transition: background-color .2s ease;
transition: background-color .2s ease, color .2s ease;
cursor: default;
border: 0;
height: 25px;

View File

@ -3,7 +3,7 @@
background: #292b2f;
border-top: 1px solid hsla(218,5%,47%,.3);
color: #d84040;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
font-family: monospace;
white-space: pre-wrap;
font-size: 12px;
}
@ -30,7 +30,7 @@
background: #36393f;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
transition: background-color .2s ease;
transition: background-color .2s ease, color .2s ease;
cursor: pointer;
border: 0;
margin-right: 4px;
@ -47,6 +47,10 @@
line-height: 22px;
flex: 0 0 auto;
label {
cursor: pointer;
}
input[type="checkbox"] {
margin: 0 6px 0 0;
cursor: pointer;
@ -57,7 +61,6 @@
font-weight: 500;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
cursor: default;
}
}
}