mirror of https://github.com/vizality/vizality
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.
180 lines
5.2 KiB
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;
|
|
}
|
|
}
|