/** * 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 }; } }