You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vizality/renderer/src/managers/Theme.js

180 lines
5.2 KiB

import { existsSync, readdirSync, readFileSync } from 'fs';
import { Directories } from '@vizality/constants';
import { join } from 'path';
import Theme from '../entities/Theme';
import AddonManager from './Addon';
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';
dir = Directories.THEMES;
super(type, dir);
}
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);
}
/**
* 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
};
}
}
}
if (!manifest) {
throw new Error(`Failed to load manifest for "${themeId}"`);
}
return manifest;
} catch (err) {
return this._error(err);
}
}
async mount (themeId) {
const manifest = await this._getManifest(themeId);
const errors = this._validateManifest(manifest);
if (errors.length > 0) {
return this._error(`Invalid manifest; Detected the following errors:\n\t${errors.join('\n\t')}`);
}
manifest.effectiveTheme = join(this.dir, themeId, manifest.theme);
this._setAddonIcon(themeId, manifest);
this._items.set(themeId, new Theme(themeId, manifest));
}
_validateManifest (manifest) {
const errors = [];
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.`);
}
}
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;
}
}