@ -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 ;
}
}