BetterDiscordApp-v2/client/src/modules/csseditor.js

319 lines
8.3 KiB
JavaScript

/**
* BetterDiscord CSS 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 { DOM } from 'ui';
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import path from 'path';
import electron from 'electron';
import filewatcher from 'filewatcher';
import Settings from './settings';
/**
* Custom css editor communications
*/
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.
*/
init() {
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);
await this.save();
}, true);
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.liveupdate.value, true);
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.
*/
async show() {
await ClientIPC.send('openCssEditor', this.editor_bounds);
}
/**
* Update css in client.
* @param {String} scss SCSS to compile
* @param {bool} sendSource Whether to send to css editor instance
*/
async updateScss(scss, sendSource) {
if (sendSource)
this.sendToEditor('set-scss', scss);
if (!scss && !await this.fileExists()) {
this._scss = this.css = '';
this.sendToEditor('scss-error', null);
return;
}
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.
* @return {Promise}
*/
save() {
return Settings.saveSettings();
}
/**
* Save current editor bounds.
* @param {Rectangle} bounds Editor bounds
* @return {Promise}
*/
saveEditorBounds(bounds) {
this.editor_bounds = bounds;
return Settings.saveSettings();
}
/**
* Send SCSS to core for compilation.
* @param {String} scss SCSS string
*/
async compile(scss) {
return ClientIPC.send('bd-compileSass', {
data: scss,
path: await this.fileExists() ? this.filePath : undefined
});
}
/**
* Recompile the current SCSS.
* @return {Promise}
*/
async recompile() {
return this.updateScss(this.scss);
}
/**
* Send data to open editor.
* @param {String} channel
* @param {Any} data
* @return {Promise}
*/
async sendToEditor(channel, data) {
return ClientIPC.sendToCssEditor(channel, data);
}
/**
* Opens an SCSS file in a system editor.
* @return {Promise}
*/
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.
*/
get scss() {
return this._scss || '';
}
/**
* Set current scss.
*/
set scss(scss) {
this.updateScss(scss, true);
}
/**
* Current compiled css.
*/
get css() {
return this._css || '';
}
/**
* Inject compiled css to head.
*/
set css(css) {
this._css = css;
DOM.injectStyle(css, 'bd-customcss');
}
/**
* Current error.
*/
get error() {
return this._error || undefined;
}
/**
* Set current error.
*/
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
*/
set watchfiles(files) {
for (const file of files) {
if (!this.watchfiles.includes(file)) {
this.filewatcher.add(file);
this.watchfiles.push(file);
Logger.log('CSS Editor', `Watching file ${file} for changes`);
}
}
for (const index in this.watchfiles) {
let file = this.watchfiles[index];
while (file && !files.find(f => f === file)) {
this.filewatcher.remove(file);
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 {Promise}
*/
async fileExists() {
try {
await FileUtils.fileExists(this.filePath);
return true;
} catch (err) {
return false;
}
}
}