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.
395 lines
12 KiB
395 lines
12 KiB
/**
|
|
* The routes API is used for various routing functionality, such as getting the current
|
|
* route, registering custom routes, and generally easier navigation around the app.
|
|
* @module Routes
|
|
* @memberof API
|
|
* @namespace API.Routes
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
import { assertString, toSnakeCase } from '@vizality/util/string';
|
|
import { Sidebar } from '@vizality/components/dashboard';
|
|
import { Events, Regexes } from '@vizality/constants';
|
|
import { assertObject } from '@vizality/util/object';
|
|
import { isComponent } from '@vizality/util/react';
|
|
import { getCaller } from '@vizality/util/file';
|
|
import { getModule } from '@vizality/webpack';
|
|
import { API } from '@vizality/entities';
|
|
import { isValidElement } from 'react';
|
|
|
|
import DashboardRoutes from '@vizality/builtins/dashboard/routes/Routes';
|
|
|
|
|
|
/**
|
|
* Vizality route object.
|
|
* @typedef VizalityRoute
|
|
* @property {string} [id] Route ID
|
|
* @property {string} path Route path
|
|
* @property {ReactElement} render Route renderer
|
|
* @property {ReactElement|undefined} sidebar Sidebar renderer
|
|
*/
|
|
|
|
/**
|
|
* @extends API
|
|
* @extends Events
|
|
*/
|
|
export default class Routes extends API {
|
|
constructor () {
|
|
super();
|
|
/**
|
|
* All currently registered routes.
|
|
*/
|
|
this.routes = [];
|
|
}
|
|
|
|
getAllRoutes () {
|
|
return this.routes;
|
|
}
|
|
|
|
/**
|
|
* Shuts down the API, removing all listeners and stored objects.
|
|
*/
|
|
stop () {
|
|
this.unregisterAllRoutes();
|
|
this.removeAllListeners();
|
|
delete vizality.api.routes;
|
|
}
|
|
|
|
/**
|
|
* Goes back to the last non-Vizality route.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async restorePreviousRoute () {
|
|
try {
|
|
if (this.getLocation()?.pathname?.startsWith('/vizality')) {
|
|
let history = await vizality.native.app.getHistory();
|
|
if (!history) {
|
|
return;
|
|
}
|
|
history = history.reverse();
|
|
history.shift();
|
|
const match = history.find(location => !location.includes('/vizality'));
|
|
const route = match.replace(new RegExp(Regexes.DISCORD), '');
|
|
$discord.modules.router.replaceWith(route);
|
|
}
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('restorePreviousRoute'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a route.
|
|
* @param {VizalityRoute} route Route to register
|
|
* @emits Routes#routeAdd
|
|
*/
|
|
/*
|
|
* registerRoute (route) {
|
|
* try {
|
|
* if (!route?.id) {
|
|
* throw new Error('Route must contain a valid ID!');
|
|
* }
|
|
* if (route.id !== 'dashboard') {
|
|
* if (!route.path) {
|
|
* throw new Error(`Route ID "${route.id}" cannot be registered without a valid path.`);
|
|
* }
|
|
* if (this.routes[route.id]) {
|
|
* throw new Error(`Route ID "${route.id}" is already registered!`);
|
|
* }
|
|
* if (Object.values(this.routes).find(r => r.path === route.path)) {
|
|
* throw new Error(`Route ID "${route.id}" tried to register an already-registered path "${route.path}"!`);
|
|
* }
|
|
* route.caller = getCaller();
|
|
* }
|
|
* this.routes[route.id] = route;
|
|
* if (Object.keys(this.routes)[Object.keys(this.routes).length - 1] !== 'dashboard') {
|
|
* this._reregisterDashboardRoutes();
|
|
* }
|
|
* this.emit('routeAdd', route);
|
|
* } catch (err) {
|
|
* return this.error(err);
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Registers a route.
|
|
* @param {VizalityRoute} route Route to register
|
|
* @emits Routes#Events.VIZALITY_ROUTE_ADD
|
|
*/
|
|
registerRoute (route) {
|
|
try {
|
|
assertObject(route);
|
|
route.caller = route.path === '' ? 'vizality' : getCaller();
|
|
route.id = route.id || `${toSnakeCase(route.caller?.id).toUpperCase()}_ROUTE_${this.getRoutesByCaller(route.caller?.id)?.length + 1 || '1'}`;
|
|
assertString(route.id);
|
|
assertString(route.path);
|
|
if (route.path !== '') {
|
|
if (this.isRoute(route.id)) {
|
|
throw new Error(`Route "${route.id}" is already registered!`);
|
|
}
|
|
if (this.isRoute(route.path)) {
|
|
throw new Error(`Route "${route.id}" tried to register an already-registered path "${route.path}"!`);
|
|
}
|
|
if (!isComponent(route.render) && !isValidElement(route.render)) {
|
|
throw new Error(`Route "${route.id}" did not provide a valid render property value! Must be a React element!`);
|
|
}
|
|
}
|
|
/**
|
|
* If no sidebar property is provided, use the Vizality dashboard sidebar as
|
|
* the default.
|
|
*/
|
|
if (!route.hasOwnProperty('sidebar')) {
|
|
route.sidebar = Sidebar;
|
|
}
|
|
this.routes.push(route);
|
|
if (this.routes[this.routes.length - 1]?.id !== 'dashboard') {
|
|
this._reregisterDashboardRoutes();
|
|
}
|
|
this.emit(Events.VIZALITY_ROUTE_ADD, route.id, route.path);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('registerRoute'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all routes matching a given caller.
|
|
* @param {string} addonId Addon ID
|
|
* @returns {Array<object|null>} Routes matching a given caller
|
|
*/
|
|
getRoutesByCaller (addonId) {
|
|
try {
|
|
assertString(addonId);
|
|
return this.routes.filter(route => route.caller?.id === addonId);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getRoutesByCaller'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all routes found matching a given filter.
|
|
* @param {Function} filter Function to use to filter routes by
|
|
* @returns {Array<object|null>} Routes matching a given filter
|
|
*/
|
|
getRoutes (filter) {
|
|
try {
|
|
if (!filter?.length) return null;
|
|
return this.routes.filter(filter);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('getRoutes'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregisters a route.
|
|
* @param {string} pathOrRouteId Route path or route ID
|
|
* @emits Routes#routeRemove
|
|
*/
|
|
unregisterRoute (pathOrRouteId) {
|
|
try {
|
|
if (!pathOrRouteId) {
|
|
throw new Error(`Invalid route path or route ID provided!`);
|
|
}
|
|
if (pathOrRouteId.startsWith('/')) {
|
|
if (!this.routes.find(r => r.path === pathOrRouteId)) {
|
|
throw new Error(`Route path "${pathOrRouteId}" is not registered, so it cannot be unregistered!`);
|
|
}
|
|
} else if (!this.routes.find(r => r.id === pathOrRouteId)) {
|
|
throw new Error(`Route ID "${pathOrRouteId}" is not registered, so it cannot be unregistered!`);
|
|
}
|
|
this.routes = this.getRoutes(route => route.id !== pathOrRouteId && route.path !== pathOrRouteId);
|
|
this.emit('routeRemove', pathOrRouteId);
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a route is registered.
|
|
* @param {string} routeIdOrPath Route ID or route path
|
|
* @returns {boolean} Whether a route with a given ID or path is registered
|
|
*/
|
|
isRoute (routeIdOrPath) {
|
|
try {
|
|
assertString(routeIdOrPath);
|
|
return this.routes.some(route => route.id === routeIdOrPath || route.path === routeIdOrPath);
|
|
} catch (err) {
|
|
return this.error(this._labels.concat('isRoute'), err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigates to a route.
|
|
* @param {string} pathOrRouteId Route path or route ID
|
|
* @emits Routes#routeNavigate
|
|
*/
|
|
navigateTo (pathOrRouteId) {
|
|
try {
|
|
if (!pathOrRouteId) {
|
|
throw new Error('Invalid route ID or path provided!');
|
|
}
|
|
let path;
|
|
if (!pathOrRouteId.startsWith('/')) {
|
|
switch (pathOrRouteId) {
|
|
case 'private': path = '/channels/@me/'; break;
|
|
case 'discover': path = $discord.constants.Routes.GUILD_DISCOVERY; break;
|
|
case 'friends': path = $discord.constants.Routes.FRIENDS; break;
|
|
case 'library': path = $discord.constants.Routes.APPLICATION_LIBRARY; break;
|
|
case 'nitro': path = $discord.constants.Routes.APPLICATION_STORE; break;
|
|
default: path = `/vizality/${pathOrRouteId}`;
|
|
}
|
|
} else {
|
|
path = pathOrRouteId;
|
|
}
|
|
|
|
/**
|
|
* Check if it's a Vizality route and has a # in the path.
|
|
*/
|
|
if (path.startsWith('/vizality/') && path.includes('#')) {
|
|
$discord.modules.router.transitionTo(path);
|
|
const hash = path.split('#')[1];
|
|
/**
|
|
* This is bad, but currently it's the only way I really know how to do it.
|
|
* We're adding a timeout here so that we can give the new route page elements
|
|
* (topOfElement and scroller) time to load into the DOM.
|
|
*/
|
|
return setTimeout(() => {
|
|
const topOfElement = document.querySelector(`#${hash}`)?.offsetTop;
|
|
const scroller = document.querySelector('.vz-dashboard-scroller');
|
|
/**
|
|
* If the elements don't exist at the check, just load the page normally as if
|
|
* there was no hash.
|
|
*/
|
|
if (!topOfElement || !scroller) {
|
|
return;
|
|
}
|
|
/**
|
|
* Try to set the scroller position to the hash element.
|
|
*/
|
|
return scroller.scroll({ top: topOfElement - 80, behavior: 'smooth' });
|
|
}, 250);
|
|
}
|
|
/**
|
|
* Go to the route.
|
|
*/
|
|
return $discord.modules.router.transitionTo(path);
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregisters all routes.
|
|
* @emits Routes#routeRemoveAll
|
|
*/
|
|
unregisterAllRoutes () {
|
|
try {
|
|
this.routes = {};
|
|
this.emit('routeRemoveAll');
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregisters all routes registered by a given addon.
|
|
* @param {string} addonId Addon ID
|
|
* @emits Routes#routeRemoveAllByAddon
|
|
*/
|
|
unregisterRoutesByAddon (addonId) {
|
|
try {
|
|
this.emit('routeRemoveAllByAddon', addonId);
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Goes back to the previous route.
|
|
* @returns {void}
|
|
*/
|
|
goBack () {
|
|
try {
|
|
const { back } = getModule('transitionTo', 'replaceWith', 'getHistory');
|
|
return back();
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Goes forward to the next route. Only works when a forward route exists.
|
|
* @returns {void}
|
|
*/
|
|
goForward () {
|
|
try {
|
|
const { forward } = getModule('transitionTo', 'replaceWith', 'getHistory');
|
|
return forward();
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets some information for the current route.
|
|
* @returns {object|void}
|
|
*/
|
|
getLocation () {
|
|
try {
|
|
const location = {};
|
|
const routes = {
|
|
private: '/channels/@me/',
|
|
discover: $discord.constants.Routes.GUILD_DISCOVERY,
|
|
friends: $discord.constants.Routes.FRIENDS,
|
|
library: $discord.constants.Routes.APPLICATION_LIBRARY,
|
|
nitro: $discord.constants.Routes.APPLICATION_STORE,
|
|
guild: '/channels/',
|
|
settings: '/vizality/settings',
|
|
plugins: '/vizality/plugins',
|
|
themes: '/vizality/themes',
|
|
snippets: '/vizality/snippets',
|
|
palettes: '/vizality/palettes',
|
|
'quick-code': '/vizality/quick-code',
|
|
development: '/vizality/development',
|
|
docs: '/vizality/docs',
|
|
updater: '/vizality/updater',
|
|
changelog: '/vizality/changelog',
|
|
dashboard: '/vizality'
|
|
};
|
|
|
|
for (const route in routes) {
|
|
if (window.location.pathname.includes(routes[route])) {
|
|
location.pathname = window.location.pathname;
|
|
location.href = window.location.href;
|
|
location.name = route || 'unknown';
|
|
return location;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @note This is a hacky method used to unregister and reregister the main dashboard route
|
|
* so that it doesn't override the plugin and theme routes... Not really sure how to do
|
|
* this in a better way at the moment, but definitely should be addressed in the future.
|
|
* @private
|
|
*/
|
|
_reregisterDashboardRoutes () {
|
|
try {
|
|
if (!this.routes.find(route => route.id === 'dashboard')) {
|
|
return;
|
|
}
|
|
this.unregisterRoute('dashboard');
|
|
this.registerRoute({
|
|
id: 'dashboard',
|
|
path: '',
|
|
render: DashboardRoutes,
|
|
sidebar: Sidebar
|
|
});
|
|
} catch (err) {
|
|
return this.error(err);
|
|
}
|
|
}
|
|
}
|