Skip to content

Commit

Permalink
Make StyleManager epic
Browse files Browse the repository at this point in the history
  • Loading branch information
cyyynthia committed Apr 10, 2019
1 parent b9449d4 commit d785f62
Show file tree
Hide file tree
Showing 25 changed files with 354 additions and 81 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ src/Powercord/plugins/*
!src/Powercord/plugins/pc-*

src/Powercord/plugins/pc-styleManager/themes/*
!src/Powercord/plugins/pc-styleManager/themes/readme.txt
src/Powercord/themes/*
!src/Powercord/themes/.exists

settings/*.json

Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
"homepage": "https://github.com/powercord-org/powercord#readme",
"dependencies": {
"buble": "0.19.6",
"chokidar": "^2.0.4",
"sass": "^1.15.1"
"less": "^3.9.0",
"node-watch": "^0.6.1",
"sass": "^1.18.0",
"stylus": "^0.54.5"
},
"devDependencies": {
"eslint": "^5.11.1",
"eslint-plugin-react": "^7.12.0"
"eslint": "^5.16.0",
"eslint-plugin-react": "^7.12.4"
}
}
11 changes: 7 additions & 4 deletions src/Powercord/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { sleep } = require('powercord/util');
const { WEBSITE } = require('powercord/constants');
const modules = require('./modules');
const PluginManager = require('./managers/plugins');
const StyleManager = require('./managers/styles');
const APIManager = require('./managers/apis');

module.exports = class Powercord extends EventEmitter {
Expand All @@ -12,6 +13,7 @@ module.exports = class Powercord extends EventEmitter {

this.api = {};
this.initialized = false;
this.styleManager = new StyleManager();
this.pluginManager = new PluginManager();
this.apiManager = new APIManager();
this.account = null;
Expand Down Expand Up @@ -56,7 +58,8 @@ module.exports = class Powercord extends EventEmitter {
await this.apiManager.startAPIs();
this.settings = powercord.api.settings.getCategory('pc-general');

// Style Manager @todo
// Style Manager
this.styleManager.loadThemes();

// Plugins
await this.pluginManager.startPlugins();
Expand All @@ -67,12 +70,12 @@ module.exports = class Powercord extends EventEmitter {
// Powercord shutdown
async shutdown () {
this.initialized = false;

// Style Manager @todo

// Plugins
await this.pluginManager.shutdownPlugins();

// Style Manager
this.styleManager.unloadThemes();

// APIs
await this.apiManager.unload();
}
Expand Down
4 changes: 2 additions & 2 deletions src/Powercord/managers/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ module.exports = class PluginManager {

enable (pluginID) {
if (!this.get(pluginID)) {
throw new Error(`Tried to unload a non installed plugin (${pluginID})`);
throw new Error(`Tried to enable a non installed plugin (${pluginID})`);
}

powercord.settings.set(
Expand All @@ -203,7 +203,7 @@ module.exports = class PluginManager {
const plugin = this.get(pluginID);

if (!plugin) {
throw new Error(`Tried to unload a non installed plugin (${pluginID})`);
throw new Error(`Tried to disable a non installed plugin (${pluginID})`);
}

powercord.settings.set('disabledPlugins', [
Expand Down
3 changes: 3 additions & 0 deletions src/Powercord/managers/styles/css/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import 'contextMenu';
@import 'keybindRecorder';
@import 'settings';
128 changes: 128 additions & 0 deletions src/Powercord/managers/styles/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const { resolve } = require('path');
const { readdirSync } = require('fs');
const { lstat } = require('fs').promises;

const Theme = require('./theme');

module.exports = class StyleManager {
constructor () {
this.themesDir = resolve(__dirname, '..', '..', 'themes');
this.themes = new Map();

this.manifestKeys = [ 'name', 'version', 'description', 'author', 'license', 'theme' ];
}

// Getters
get (themeID) {
return this.themes.get(themeID);
}

getThemes () {
return [ ...this.themes.keys() ];
}

isInstalled (theme) {
return this.themes.has(theme);
}

isEnabled (theme) {
return !powercord.settings.get('disabledThemes', []).includes(theme);
}

enable (themeID) {
if (!this.get(themeID)) {
throw new Error(`Tried to enable a non installed theme (${themeID})`);
}

powercord.settings.set(
'disabledThemes',
powercord.settings.get('disabledThemes', []).filter(p => p !== themeID)
);

this.themes.get(themeID).apply();
}

disable (themeID) {
const plugin = this.get(themeID);
if (!plugin) {
throw new Error(`Tried to disable a non installed theme (${themeID})`);
}

powercord.settings.set('disabledThemes', [
...powercord.settings.get('disabledThemes', []),
themeID
]);

this.themes.get(themeID).remove();
}

/*
* @todo
* async install (pluginID) {
* await exec(`git clone https://github.com/powercord-org/${pluginID}`, this.pluginDir);
* this.mount(pluginID);
* }
*
* async uninstall (pluginID) {
* if (pluginID.startsWith('pc-')) {
* throw new Error(`You cannot uninstall an internal plugin. (Tried to uninstall ${pluginID})`);
* }
*
* await this.unmount(pluginID);
* await rmdirRf(resolve(this.pluginDir, pluginID));
* }
*/

// Plugin CSS
loadPluginCSS (themeID, file) {
const theme = Theme.fromFile(themeID, file);
this.themes.set(themeID, theme);
theme.apply();
}

// Start/Stop
async loadThemes () {
this.loadPluginCSS('powercord-core', resolve(__dirname, 'css', 'index.scss'));

const files = readdirSync(this.themesDir);
for (const filename of files) {
if (filename === '.exists') {
continue;
}

const themeID = filename.split('.').shift().toLowerCase();
const stat = await lstat(resolve(this.themesDir, filename));
let theme;

try {
if (stat.isFile()) {
theme = Theme.fromFile(themeID, filename);
} else {
const manifest = require(resolve(this.themesDir, filename, 'powercord_manifest.json'));
if (!this.manifestKeys.every(key => manifest.hasOwnProperty(key))) {
console.error('%c[Powercord]', 'color: #257dd4', `Theme "${themeID}" doesn't have a valid manifest - Skipping`);
continue;
}

theme = new Theme(themeID, {
...manifest,
theme: resolve(resolve(this.themesDir, filename, manifest.theme))
});
}
} catch (e) {
console.error('%c[Powercord]', 'color: #257dd4', `Theme "${themeID}" doesn't have a valid manifest or is not a valid file - Skipping`);
continue;
}

this.themes.set(themeID, theme);

if (!powercord.settings.get('disabledThemes', []).includes(themeID)) {
theme.apply();
}
}
}

unloadThemes () {
[ ...this.themes.values() ].forEach(t => t.remove());
}
};
156 changes: 156 additions & 0 deletions src/Powercord/managers/styles/theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
const { createElement } = require('powercord/util');
const { resolve, dirname } = require('path');
const { readFile } = require('fs').promises;
const { existsSync } = require('fs');
const watch = require('node-watch');
const { render } = require('sass');

const regex = /\.((s?c|le)ss|styl)$/;

module.exports = class Theme {
constructor (themeID, manifest) {
// @todo: Validate more than actual theme. Not needed for now as plugins key is useless
if (!regex.test(manifest.theme)) {
throw new Error('Invalid theme file!');
}

this.themeID = themeID;
this.manifest = manifest;
this.trackedFiles = [];
}

apply () {
const element = document.head.querySelector(`#powercord-css-${this.themeID}`);
if (!element) {
document.head.appendChild(
createElement('style', { id: `powercord-css-${this.themeID}` })
);
}

this.refresh();
}

async refresh () {
const element = document.head.querySelector(`#powercord-css-${this.themeID}`);
if (!element) {
return this.apply();
}

const stylesheet = await this._compileStylesheet();

// Update CSS
element.innerHTML = stylesheet.data;

// Filter no longer used watchers
this.trackedFiles = this.trackedFiles.filter(tf => {
if (!stylesheet.includes.includes(tf.file)) {
// noinspection JSPrimitiveTypeWrapperUsage
stylesheet.includes = stylesheet.includes.filter(i => i !== tf.file);
tf.watcher.close();
return false;
}
return true;
});

// Add new watchers
stylesheet.includes.forEach(file => {
const watcher = watch(file, this._handleUpdate.bind(this));
this.trackedFiles.push({
file,
watcher
});
});
}

remove () {
const element = document.head.querySelector(`#powercord-css-${this.themeID}`);
if (element) {
element.remove();
}
}

async _compileStylesheet () {
let stylesheet = (await readFile(this.manifest.theme)).toString();
switch (this.manifest.theme.split('.').pop()) {
case 'scss':
stylesheet = await this._renderSCSS(stylesheet);
break;
case 'less':
stylesheet = await this._renderLess(stylesheet);
break;
case 'styl':
stylesheet = await this._renderStylus(stylesheet);
break;
default:
stylesheet = {
data: stylesheet,
includes: [ this.manifest.theme ]
};
}

// @todo: Process the file and remove dynamic selectors
return stylesheet;
}

_renderSCSS (scss) {
return new Promise((res, rej) => {
render({
data: scss,
includePaths: [ dirname(this.manifest.theme) ],
importer: (url, prev) => {
url = url.replace('file:///', '');
if (existsSync(url)) {
return { file: url };
}

const prevFile = prev === 'stdin' ? this.manifest.theme : prev.replace(/https?:\/\/(?:[a-z]+\.)?discordapp\.com/i, '');
return {
file: resolve(dirname(decodeURI(prevFile)), url).replace(/\\/g, '/')
};
}
}, (err, compiled) => {
if (err) {
return rej(err);
}

res({
data: compiled.css.toString(),
includes: [
this.manifest.theme,
...compiled.stats.includedFiles.map(f => decodeURI(f).replace(/\\/g, '/'))
]
});
});
});
}

_renderLess (less) {
// @todo
return less;
}

_renderStylus (stylus) {
// @todo
return stylus;
}

// eslint-disable-next-line no-unused-vars
_handleUpdate (evt, _) {
if (evt === 'update') {
this.refresh();
} else if (evt === 'remove') {
this.remove();
}
}

static fromFile (themeID, file) {
return new Theme(themeID, {
name: themeID,
version: '1.0.0',
description: 'No description provided',
author: 'Unknown',
license: 'Unknown',
theme: file
});
}
};
1 change: 0 additions & 1 deletion src/Powercord/plugins/pc-badges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ module.exports = class Badges extends Plugin {
}

pluginWillUnload () {
this.unloadCSS();
uninject('pc-badges-users');
uninject('pc-badges-guilds-header');
}
Expand Down
1 change: 0 additions & 1 deletion src/Powercord/plugins/pc-codeblocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ module.exports = class Codeblocks extends Plugin {
}

pluginWillUnload () {
this.unloadCSS();
uninject('pc-message-codeblock');

for (const codeblock of document.querySelectorAll('.powercord-codeblock-copy-btn')) {
Expand Down
Loading

0 comments on commit d785f62

Please sign in to comment.