update theme manager, allowing for BD and Powercord themes to be used directly now

pull/95/head
dperolio 2 years ago
parent 901185c4ba
commit b957ad8afb
No known key found for this signature in database
GPG Key ID: 4191689562D51409

@ -1,20 +1,56 @@
import { existsSync } from 'fs';
import { join } from 'path';
import { existsSync, readdirSync, readFileSync } from 'fs';
import { Directories } from '@vizality/constants';
import { join } from 'path';
import manifestEntries from './theme_manifest.json';
import Theme from '../entities/Theme';
import AddonManager from './Addon';
const fileRegex = /\.((s?c)ss)$/;
const typeOf = what => Array.isArray(what)
? 'array'
: what === null
? 'null'
: typeof what;
const listFormat = new Intl.ListFormat('en', { type: 'disjunction' });
const MANIFEST_VALIDATION = [
{
prop: 'name',
types: [ 'string' ],
type: 'required'
},
{
prop: 'description',
types: [ 'string' ],
type: 'required'
},
{
prop: 'version',
types: [ 'string' ],
type: 'required'
},
{
prop: 'author',
types: [ 'string', 'object' ],
type: 'required'
},
{
prop: 'theme',
types: [ 'string' ],
type: 'required'
},
{
prop: 'license',
types: [ 'string' ],
type: 'optional'
},
{
prop: 'discord',
types: [ 'string' ],
type: 'optional'
}
];
export default class ThemeManager extends AddonManager {
constructor (type, dir) {
type = 'theme';
@ -22,80 +58,124 @@ export default class ThemeManager extends AddonManager {
super(type, dir);
}
async mount (addonId) {
const manifestFile = join(this.dir, addonId, 'manifest.json');
async _getManifest (themeId) {
try {
/**
* Check if it's a normal Vizality theme.
*/
let manifest;
let manifestFile = join(this.dir, themeId, 'manifest.json');
if (existsSync(manifestFile)) {
manifest = await import(manifestFile);
} else {
/**
* Check if it's a Powercord theme.
*/
manifestFile = join(this.dir, themeId, 'powercord_manifest.json');
if (existsSync(manifestFile)) {
manifest = await import(manifestFile);
}
if (!existsSync(manifestFile)) {
return this._error(`no manifest found`);
}
/**
* Let's check if it's a BetterDiscord theme.
* Credits to creatable for most of this.
* @see {@link https://github.com/Cr3atable/Powerconvert}
* @author creatable
* @license Apache-2.0
*/
if (!manifest) {
const themeDirectory = readdirSync(join(this.dir, themeId));
const themeFile = themeDirectory?.filter(file => file.match(/\.(theme).((s?c)ss)$/))?.[0];
if (themeFile) {
const themeFileContents = readFileSync(join(this.dir, themeId, themeFile), 'utf8');
const splitRegex = /[^\S\r\n]*?\r?\n[^\S\r\n]*?\*[^\S\r\n]?/;
const escapedAtRegex = /^\\@/;
function parseNewMeta (fileContent) {
const block = fileContent?.split('/**', 2)?.[1]?.split('*/', 1)?.[0];
const out = {};
let field = '';
let accum = '';
for (const line of block?.split(splitRegex)) {
if (line.length === 0) {
continue;
}
if (line.charAt(0) === '@' && line.charAt(1) !== ' ') {
out[field] = accum;
const l = line.indexOf(' ');
field = line.substr(1, l - 1);
accum = line.substr(l + 1);
} else {
accum += ` ${line.replace('\\n', '\n').replace(escapedAtRegex, '@')}`;
}
}
out[field] = accum.trim();
delete out[''];
return out;
}
function extractMeta (fileContent) {
const firstLine = fileContent?.split('\n')?.[0];
const hasNewMeta = firstLine?.includes('/**');
if (hasNewMeta) {
return parseNewMeta(fileContent);
}
throw new Error('META was not found.');
}
manifest = extractMeta(themeFileContents);
manifest = {
name: manifest.name,
description: manifest.description,
version: manifest.version,
author: manifest.author,
theme: themeFile
};
}
}
}
let manifest;
try {
manifest = await import(manifestFile);
if (!manifest) {
throw new Error(`Failed to load manifest for "${themeId}"`);
}
return manifest;
} catch (err) {
return this._error('Failed to load manifest');
return this._error(err);
}
}
async mount (themeId) {
const manifest = await this._getManifest(themeId);
console.log(manifest);
const errors = this._validateManifest(manifest);
if (errors.length > 0) {
return this._error(`Invalid manifest; Detected the following errors:\n\t${errors.join('\n\t')}`);
}
if (window.__SPLASH__ && manifest.splashTheme) {
manifest.effectiveTheme = manifest.splashTheme;
} else if (window.__OVERLAY__ && manifest.overlayTheme) {
manifest.effectiveTheme = manifest.overlayTheme;
} else if (!window.__OVERLAY__ && !window.__SPLASH__ && manifest.theme) {
manifest.effectiveTheme = manifest.theme;
} else {
return this._warn(`Theme "${addonId}" is not meant to run on that environment. Initialization aborted.`);
}
manifest.effectiveTheme = join(this.dir, addonId, manifest.effectiveTheme);
this._setAddonIcon(addonId, manifest);
this._items.set(addonId, new Theme(addonId, manifest));
manifest.effectiveTheme = join(this.dir, themeId, manifest.theme);
this._setAddonIcon(themeId, manifest);
this._items.set(themeId, new Theme(themeId, manifest));
}
_validateOverlayTheme () { return []; }
_validatePlugins () { return []; }
_validateManifest (manifest) {
const errors = [];
for (const entry of manifestEntries) {
console.log(manifest);
for (const entry of MANIFEST_VALIDATION) {
const isValid = entry.types.some(type => typeOf(manifest[entry.prop]) === type);
if (!isValid && entry.type === 'required') {
errors.push(`Invalid ${entry.prop}: expexted a ${entry.types.length > 1 ? listFormat.format(entry.types) : entry.types[0]} but got ${typeOf(manifest[entry.prop])} instead.`);
} else if (entry.validate) {
const errorsFound = this[entry.validate](manifest);
if (errorsFound.length && entry.type === 'required') {
errors.push(...errorsFound);
}
}
}
return errors;
}
_validateFileExtension (manifest) {
if (typeof manifest.theme !== 'string') return [ `Invalid theme: expected a string but got ${typeOf(manifest.theme)}` ];
const matches = fileRegex.test(manifest.theme);
if (!matches) return [ 'Invalid theme: unsupported file extension' ];
return [];
}
_validateSettings ({ settings }) {
if (!Array.isArray(settings)) return [ `Invalid settings: expected an array got ${typeof settings}` ];
const errors = [];
for (const setting of settings) {
const currentIndex = settings.indexOf(setting);
if (typeof setting.type === 'undefined') errors.push(`Invalid settings: Setting at position ${currentIndex}; property "type" was not found.`);
if ([ 'divider', 'category' ].indexOf(setting.type) > -1) continue;
if (typeof setting !== 'object') errors.push(`Invalid settings: Setting at position ${currentIndex}; expected a object but got ${typeOf(setting)}`);
if (typeof setting.id !== 'string') errors.push(`Invalid settings: Setting at position ${currentIndex}; property "id" expected a string but got ${typeOf(setting.id)}`);
if (typeof setting.defaultValue === 'undefined') errors.push(`Invalid settings: Setting at position ${currentIndex}; property "defaultValue" expected a string but got ${typeOf(setting.defaultValue)}`);
if (typeof manifest.theme !== 'string') {
errors.push(`Invalid theme: expected a string but got ${typeOf(manifest.theme)}`);
}
const scssFileRegex = /\.((s?c)ss)$/;
const matches = scssFileRegex.test(manifest.theme);
if (!matches) {
errors.push('Invalid theme: unsupported file extension');
}
return errors;
}
}

@ -1,69 +0,0 @@
[
{
"prop": "name",
"types": [
"string"
],
"type": "required"
},
{
"prop": "description",
"types": [
"string"
],
"type": "required"
},
{
"prop": "version",
"types": [
"string"
],
"type": "required"
},
{
"prop": "author",
"types": [
"string", "object"
],
"type": "required"
},
{
"prop": "theme",
"types": [
"string"
],
"type": "required",
"validate": "_validateFileExtension"
},
{
"prop": "overlayTheme",
"types": [
"string"
],
"type": "optional",
"validate": "_validateOverlayTheme"
},
{
"prop": "discord",
"types": [
"string"
],
"type": "optional"
},
{
"prop": "plugins",
"types": [
"array"
],
"type": "optional",
"validate": "_validatePlugins"
},
{
"prop": "settings",
"types": [
"array"
],
"type": "optional",
"validate": "_validateSettings"
}
]
Loading…
Cancel
Save