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.
499 lines
16 KiB
499 lines
16 KiB
/**
|
|
*
|
|
* @module Vizality
|
|
* @namespace Vizality
|
|
*/
|
|
|
|
import { log as _log, warn as _warn, error as _error, deprecate as _deprecate } from '@vizality/util/logger';
|
|
import { initialize as initializeWebpackModules, getModule } from '@vizality/webpack';
|
|
import { Directories, Developers, Events, Protocols } from '@vizality/constants';
|
|
import { sleep } from '@vizality/util/time';
|
|
import { resolveCompiler } from '@vizality/compilers';
|
|
import { createElement } from '@vizality/util/dom';
|
|
import { toPlural } from '@vizality/util/string';
|
|
import { isArray } from '@vizality/util/array';
|
|
import { Updatable } from '@vizality/entities';
|
|
import { debounce } from 'lodash';
|
|
import { promisify } from 'util';
|
|
import cp from 'child_process';
|
|
import { promises } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
const { readFile, writeFile } = promises;
|
|
const exec = promisify(cp.exec);
|
|
|
|
/**
|
|
* @extends Updatable
|
|
* @extends Events
|
|
*/
|
|
export default class Vizality extends Updatable {
|
|
constructor () {
|
|
super(Directories.ROOT, '', 'vizality');
|
|
/**
|
|
* This is a leftover prop from Updatable, not needed for Vizality's core.
|
|
*/
|
|
delete this.addonId;
|
|
|
|
/**
|
|
* @note Copy over VizalityNative to vizality.native and then delete
|
|
* VizalityNative, so that we are staying consistent and only have
|
|
* one top level global variable.
|
|
*/
|
|
this.native = window.VizalityNative;
|
|
delete window.VizalityNative;
|
|
|
|
/**
|
|
* Set up the git info, defaulting to ???
|
|
*/
|
|
this.git = {
|
|
upstream: '???',
|
|
branch: '???',
|
|
revision: '???'
|
|
};
|
|
|
|
/**
|
|
* Labels used for console logging purposes.
|
|
*/
|
|
this._labels = [ 'Vizality', 'Core' ];
|
|
|
|
/**
|
|
* Wait for the document to be loaded and then initialize Vizality.
|
|
* @todo There should be a better way to do this, look into it more in the future.
|
|
*/
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => this.initialize());
|
|
} else {
|
|
this.initialize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize Vizality.
|
|
*/
|
|
async initialize () {
|
|
try {
|
|
/**
|
|
* Get rid of Discord's "Hold Up" dev tools warning.
|
|
*/
|
|
DiscordNative?.window?.setDevtoolsCallbacks(null, null);
|
|
|
|
/**
|
|
* Initialize the webpack modules.
|
|
*/
|
|
await initializeWebpackModules();
|
|
|
|
/**
|
|
* Set up a connectStoresAsync FLux method.
|
|
* @note This has to be after webpack modules have been initialized.
|
|
*/
|
|
const Flux = await getModule('Store', 'PersistedStore', true);
|
|
Flux.connectStoresAsync = (stores, fn) => Component =>
|
|
import('./components').AsyncComponent.from((async () => {
|
|
const awaitedStores = await Promise.all(stores);
|
|
return Flux.connectStores(awaitedStores, props => fn(awaitedStores, props))(Component);
|
|
})());
|
|
|
|
/**
|
|
* Instantiate the managers.
|
|
* @note We're doing this down here so that we can utilize webpack modules and
|
|
* components inside of the managers.
|
|
*/
|
|
this.manager = {};
|
|
const managers = [ 'API', 'Builtin', 'Plugin', 'Theme', 'Community' ];
|
|
for (const manager of managers) {
|
|
/**
|
|
* Make the manager names on the global object plural, except for Community.
|
|
*/
|
|
const formatted = manager === 'Community' ? manager : toPlural(manager);
|
|
this.manager[formatted.toLowerCase()] = new (await import(`./managers/${manager}`))();
|
|
}
|
|
|
|
await this.start();
|
|
|
|
/**
|
|
* Get and assign the newly updated git info.
|
|
*/
|
|
this.git = await this.manager.builtins.get('updater')?.getGitInfo();
|
|
|
|
/**
|
|
* Token manipulation stuff. Helps prevent unwanted logouts.
|
|
*/
|
|
if (this.settings.get('hideToken', true)) {
|
|
const tokenModule = getModule('hideToken');
|
|
tokenModule.hideToken = () => void 0;
|
|
setImmediate(() => tokenModule.showToken());
|
|
}
|
|
|
|
/**
|
|
* Enables/disables Discord Experiments.
|
|
*/
|
|
if (this.settings.get('discordExperiments', false)) {
|
|
const experimentsModule = getModule(user => user.isDeveloper !== void 0);
|
|
Object.defineProperty(experimentsModule, 'isDeveloper', {
|
|
get: () => true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger an event indicating that Vizality has been initialized.
|
|
*/
|
|
this.emit(Events.VIZALITY_READY);
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts up the core functionality of Vizality, including APIs, builtins, plugins, and themes.
|
|
*/
|
|
async start () {
|
|
/**
|
|
* Clean up console by clearing it first, then log our startup banner.
|
|
*/
|
|
console.clear();
|
|
console.log('%c ', `background: url('${Protocols.ASSETS}/images/console-banner.gif') no-repeat center / contain; padding: 110px 350px; font-size: 1px; margin: 10px 0;`);
|
|
|
|
/**
|
|
* Set up the modules for the global vizality object.
|
|
*/
|
|
this.modules = {};
|
|
const modules = await import('./modules');
|
|
Object.assign(this.modules, modules);
|
|
|
|
/**
|
|
* Set up a shorthand for Vizality's Discord module.
|
|
* Make sure it doesn't exist already, just in case Discord ever uses the same global namespace itself.
|
|
*/
|
|
if (!window.$discord) {
|
|
const discord = await import('./modules/discord');
|
|
window.$discord = Object.assign({}, discord);
|
|
window.$discord.constants = Object.assign({}, { ...window.$discord.constants, ...window.$discord.constants.default });
|
|
}
|
|
|
|
/**
|
|
* Perform some cleanup, removing unnecessary properties.
|
|
*/
|
|
delete this.modules.discord;
|
|
delete window.$discord.constants.default;
|
|
delete window.$discord.default;
|
|
|
|
this.deprecate('The global namespace object "discord" will be removed on May 24, 2022. Please use "$discord" instead.');
|
|
this.deprecate('The global namespace object "vizality" will be removed on May 24, 2022. Please use "$vz" instead. "$vz" has a slightly different data structure, but provides the same information.');
|
|
|
|
/**
|
|
* Initialize the APIs.
|
|
*/
|
|
this.api = {};
|
|
await this.manager.apis.initialize();
|
|
|
|
/**
|
|
* Set up a shorthand vizality global object with the namespace $vz.
|
|
*/
|
|
window.$vz = Object.assign({}, this.manager, this.api, this.modules);
|
|
|
|
/**
|
|
* Set up and initialize Vizality's core settings.
|
|
*/
|
|
this.settings = this.api.settings._buildCategoryObject('settings');
|
|
|
|
/**
|
|
* Check if the current user is a Vizality Developer.
|
|
* @note This is going before the settings ready event below, because we check this in
|
|
* the Icon component after settings ready event has triggered.
|
|
*/
|
|
|
|
/*
|
|
* while (!$discord.users.getCurrentUser()?.id) {
|
|
* sleep(1);
|
|
* }
|
|
*/
|
|
|
|
this.settings.set('developer', true);
|
|
/*
|
|
* console.log($discord.users.getCurrentUser()?.id);
|
|
* if (Developers.some(developer => developer.id === $discord.users.getCurrentUser()?.id)) {
|
|
* console.log('okay');
|
|
* this.settings.set('developer', true);
|
|
* } else {
|
|
* this.settings.set('developer', false);
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Trigger an event indicating that Vizality's settings are ready.
|
|
*/
|
|
this.emit(Events.VIZALITY_SETTINGS_READY);
|
|
|
|
/**
|
|
* Inject core Vizality styles.
|
|
*/
|
|
this._injectCoreStyles();
|
|
|
|
/**
|
|
* Patch Discord's stylized console logs.
|
|
* @note This has to be after settings have been initialized.
|
|
*/
|
|
this._patchDiscordLogs();
|
|
|
|
/**
|
|
* Initialize builtins, plugins, and themes.
|
|
*/
|
|
await this.manager.builtins.initialize();
|
|
this.manager.themes.initialize();
|
|
this.manager.plugins.initialize();
|
|
this.manager.community.initialize();
|
|
}
|
|
|
|
/**
|
|
* Shuts down Vizality's APIs, builtins, plugins, and themes.
|
|
*/
|
|
async stop () {
|
|
/**
|
|
* Most importantly here is to stop entities in the order of plugins -> builtins -> apis
|
|
* to ensure there aren't any problems shutting down something that relies on
|
|
* something else.
|
|
*/
|
|
this.manager.themes.stop();
|
|
this.manager.plugins.stop();
|
|
await this.manager.builtins.stop();
|
|
await this.manager.apis.stop();
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
async _patchDiscordLogs () {
|
|
try {
|
|
const { setLogFn } = await getModule('setLogFn', true);
|
|
if (!this.settings.get('showDiscordConsoleLogs', false)) {
|
|
/**
|
|
* Removes Discord's logs entirely... except for the logs that don't use the
|
|
* setLogFn function (i.e. normal console methods).
|
|
*/
|
|
setLogFn(() => void 0);
|
|
} else {
|
|
/**
|
|
* Patch Discord's logs to adhere to Vizality's log styles.
|
|
*/
|
|
setLogFn((submodule, type, ...message) => {
|
|
switch (type) {
|
|
case 'info':
|
|
case 'log':
|
|
return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
|
|
case 'error':
|
|
case 'trace':
|
|
return _error({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
|
|
case 'warn':
|
|
return _warn({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
|
|
default:
|
|
return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message });
|
|
}
|
|
});
|
|
}
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
_patchWebSocket () {
|
|
const _this = this;
|
|
window.WebSocket = class PatchedWebSocket extends window.WebSocket {
|
|
constructor (url) {
|
|
super(url);
|
|
this.addEventListener('message', data => {
|
|
_this.emit(`webSocketMessage:${data.origin.slice(6)}`, data);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {boolean} [force=false] Whether to
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async _update (force = false) {
|
|
try {
|
|
const success = await super._update(force);
|
|
/**
|
|
*
|
|
*/
|
|
if (success) {
|
|
try {
|
|
await exec('npm install --only=prod --legacy-peer-deps', { cwd: this.dir });
|
|
} catch (err) {
|
|
return this.error(`An error occurred while updating Vizality's dependencies!`, err);
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
if (!document.querySelector(`#vz-updater-update-complete, [vz-route='updater']`)) {
|
|
this.api.notifications.sendToast({
|
|
id: 'VIZALITY_UPDATER_UPDATE_COMPLETE',
|
|
header: 'Update complete!',
|
|
content: `Please click 'Reload' to complete the final stages of this Vizality update.`,
|
|
icon: 'CloudDone',
|
|
buttons: [
|
|
{
|
|
text: 'Reload',
|
|
color: 'green',
|
|
look: 'ghost',
|
|
onClick: () => DiscordNative?.app?.relaunch()
|
|
},
|
|
{
|
|
text: 'Postpone',
|
|
color: 'grey',
|
|
look: 'outlined',
|
|
onClick: () => this.api.notifications.closeToast('VIZALITY_UPDATER_UPDATE_COMPLETE')
|
|
}
|
|
]
|
|
});
|
|
}
|
|
this.manager.builtins.get('updater').settings.set('awaitingReload', true);
|
|
}
|
|
return success;
|
|
} catch (err) {
|
|
return this.error(`An error occurred while updating Vizality!`, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Injects a style element containing Vizality's core styles.
|
|
* @note Includes a file watcher and CSS file generator only for Vizality developers.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async _injectCoreStyles () {
|
|
try {
|
|
const id = 'vizality-core-styles';
|
|
/**
|
|
* Check if the user is a Vizality developer.
|
|
*/
|
|
if (this.settings.get('developer', false)) {
|
|
const path = join(Directories.STYLES, 'main.scss');
|
|
const compiler = resolveCompiler(path);
|
|
const style = createElement('style', { id, 'vz-style': '' });
|
|
document.head.appendChild(style);
|
|
|
|
/**
|
|
* Compiles the Sass and then writes it into a main.css file.
|
|
* @param {boolean} [showLogs=true] Whether to show log messages
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const compile = debounce(async (showLogs = true) => {
|
|
try {
|
|
const before = performance.now();
|
|
const compiled = await compiler.compile();
|
|
style.innerHTML = compiled;
|
|
if (showLogs) {
|
|
const after = performance.now();
|
|
const time = parseFloat((after - before).toFixed()).toString().replace(/^0+/, '') || 0;
|
|
/**
|
|
* Let's format the milliseconds to seconds.
|
|
*/
|
|
let formattedTime = Math.round((time / 1000 + Number.EPSILON) * 100) / 100;
|
|
/**
|
|
* If it ends up being so fast that it rounds to 0, let's show formatting
|
|
* to 3 decimal places, otherwise show 2 decimal places.
|
|
*/
|
|
if (formattedTime === 0) {
|
|
formattedTime = Math.round((time / 1000 + Number.EPSILON) * 1000) / 1000;
|
|
}
|
|
/**
|
|
* If it is still 0, let's just say it's fast.
|
|
*/
|
|
if (formattedTime === 0) {
|
|
this.log(`Core styles compiled. Compilation was nearly instant!`);
|
|
} else {
|
|
this.log(`Core styles compiled. Compilation took ${formattedTime} seconds!`);
|
|
}
|
|
}
|
|
await writeFile(join(Directories.STYLES, 'main.css'), compiled);
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}, 300);
|
|
|
|
/**
|
|
* Set up the watcher for the compiler.
|
|
*/
|
|
compiler.enableWatcher();
|
|
compiler.on('src-update', compile);
|
|
this[`__compileStylesheet_${id}`] = compile;
|
|
this[`__compiler_${id}`] = compiler;
|
|
return compile(false);
|
|
}
|
|
|
|
/**
|
|
* Create a style element and inject the CSS into it.
|
|
*/
|
|
const path = join(__dirname, 'styles', 'main.css');
|
|
const style = createElement('style', { id, 'vz-style': '' });
|
|
document.head.appendChild(style);
|
|
const css = await readFile(path, 'utf8');
|
|
style.innerHTML = css;
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} message Message to log
|
|
* @returns {void}
|
|
*/
|
|
log (...message) {
|
|
// In case the addon wants to provide their own labels
|
|
if (isArray(message[0])) {
|
|
const _message = message.slice(1);
|
|
return _log({ labels: message[0], message: _message });
|
|
}
|
|
return _log({ labels: this._labels, message });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} message Message to log
|
|
* @returns {void}
|
|
*/
|
|
warn (...message) {
|
|
// In case the addon wants to provide their own labels
|
|
if (isArray(message[0])) {
|
|
const _message = message.slice(1);
|
|
return _warn({ labels: message[0], message: _message });
|
|
}
|
|
return _warn({ labels: this._labels, message });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} message Message to log
|
|
* @returns {void}
|
|
*/
|
|
error (...message) {
|
|
// In case the addon wants to provide their own labels
|
|
if (isArray(message[0])) {
|
|
const _message = message.slice(1);
|
|
return _error({ labels: message[0], message: _message });
|
|
}
|
|
return _error({ labels: this._labels, message });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {any} message Message to log
|
|
* @returns {void}
|
|
*/
|
|
deprecate (...message) {
|
|
// In case the addon wants to provide their own labels
|
|
if (isArray(message[0])) {
|
|
const _message = message.slice(1);
|
|
return _deprecate({ labels: message[0], message: _message });
|
|
}
|
|
return _deprecate({ labels: this._labels, message });
|
|
}
|
|
}
|