mirror of
https://github.com/dkmstr/openuds-gui.git
synced 2025-03-11 04:58:21 +03:00
fixing and refactoring frontend
This commit is contained in:
parent
6bb8239e84
commit
23389fca6a
@ -5,8 +5,7 @@
|
|||||||
# You can see what browsers were selected by your queries by running:
|
# You can see what browsers were selected by your queries by running:
|
||||||
# npx browserslist
|
# npx browserslist
|
||||||
|
|
||||||
> 0.2%
|
> 0.2% and not dead
|
||||||
last 2 versions
|
last 2 versions
|
||||||
last 4 years
|
last 4 years
|
||||||
Firefox ESR
|
Firefox ESR
|
||||||
not dead
|
|
||||||
|
7
.hintrc
7
.hintrc
@ -7,5 +7,10 @@
|
|||||||
"axe/text-alternatives": "off",
|
"axe/text-alternatives": "off",
|
||||||
"button-type": "off",
|
"button-type": "off",
|
||||||
"typescript-config/consistent-casing": "off"
|
"typescript-config/consistent-casing": "off"
|
||||||
}
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"defaults",
|
||||||
|
"not ie 11",
|
||||||
|
"not ie <= 10"
|
||||||
|
]
|
||||||
}
|
}
|
@ -51,7 +51,7 @@ export class ModalComponent implements OnInit {
|
|||||||
this.extra = ' (' + Math.floor(miliseconds / 1000) + ' ' + django.gettext('seconds') + ') ';
|
this.extra = ' (' + Math.floor(miliseconds / 1000) + ' ' + django.gettext('seconds') + ') ';
|
||||||
}
|
}
|
||||||
|
|
||||||
async initAlert(): Promise<void> {
|
async initAlert() {
|
||||||
const autoclose = this.data.autoclose || 0;
|
const autoclose = this.data.autoclose || 0;
|
||||||
if (autoclose > 0) {
|
if (autoclose > 0) {
|
||||||
this.dialogRef.afterClosed().subscribe((res) => {
|
this.dialogRef.afterClosed().subscribe((res) => {
|
||||||
|
@ -15,7 +15,7 @@ export class Plugin {
|
|||||||
this.delay = api.config.launcher_wait_time;
|
this.delay = api.config.launcher_wait_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchURL(url: string): Promise<void> {
|
async launchURL(url: string) {
|
||||||
// If uds url...
|
// If uds url...
|
||||||
if (url.substring(0, 7) === 'udsa://') {
|
if (url.substring(0, 7) === 'udsa://') {
|
||||||
await this.processUDSUrl(url);
|
await this.processUDSUrl(url);
|
||||||
@ -45,27 +45,7 @@ export class Plugin {
|
|||||||
* @param url uds url to be lauhcned
|
* @param url uds url to be lauhcned
|
||||||
*/
|
*/
|
||||||
private launchUDSUrl(url: string) {
|
private launchUDSUrl(url: string) {
|
||||||
let elem: HTMLIFrameElement = document.getElementById(
|
this.api.download(url);
|
||||||
'hiddenUdsLauncherIFrame'
|
|
||||||
) as HTMLIFrameElement;
|
|
||||||
if (elem === null) {
|
|
||||||
const i = document.createElement('div');
|
|
||||||
i.id = 'testID';
|
|
||||||
i.innerHTML =
|
|
||||||
'<iframe id="hiddenUdsLauncherIFrame" src="about:blank" style="display:none"></iframe>';
|
|
||||||
document.body.appendChild(i);
|
|
||||||
elem = document.getElementById(
|
|
||||||
'hiddenUdsLauncherIFrame'
|
|
||||||
) as HTMLIFrameElement;
|
|
||||||
}
|
|
||||||
// Ensure all is ok
|
|
||||||
if (elem === null) {
|
|
||||||
throw new Error('Unable to create hidden iframe');
|
|
||||||
}
|
|
||||||
if (elem.contentWindow === null) {
|
|
||||||
throw new Error('Unable to get content window');
|
|
||||||
}
|
|
||||||
elem.contentWindow.location.href = url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +54,7 @@ export class Plugin {
|
|||||||
* @param url uds url (udsa://serviceId/transportId)
|
* @param url uds url (udsa://serviceId/transportId)
|
||||||
* @returns nothing
|
* @returns nothing
|
||||||
*/
|
*/
|
||||||
private async processUDSUrl(url: string): Promise<void> {
|
private async processUDSUrl(url: string) {
|
||||||
// Extract params from url, serviceId and transportId
|
// Extract params from url, serviceId and transportId
|
||||||
const params = url.split('//')[1].split('/');
|
const params = url.split('//')[1].split('/');
|
||||||
if (params.length !== 2) {
|
if (params.length !== 2) {
|
||||||
@ -96,7 +76,7 @@ export class Plugin {
|
|||||||
// Connect close dialog to "cancel" variable
|
// Connect close dialog to "cancel" variable
|
||||||
toPromise(dialog.afterClosed()).then(() => (cancel = true));
|
toPromise(dialog.afterClosed()).then(() => (cancel = true));
|
||||||
|
|
||||||
let readyTime = -1;
|
let readySinceTime = -1;
|
||||||
try {
|
try {
|
||||||
// Enable service
|
// Enable service
|
||||||
const enabledData = await this.api.enabler(serviceId, transportId);
|
const enabledData = await this.api.enabler(serviceId, transportId);
|
||||||
@ -119,7 +99,7 @@ export class Plugin {
|
|||||||
while (!cancel) {
|
while (!cancel) {
|
||||||
const data = await this.api.status(serviceId, transportId);
|
const data = await this.api.status(serviceId, transportId);
|
||||||
// Wait 5 times the default delay before notifying that client is not installed
|
// Wait 5 times the default delay before notifying that client is not installed
|
||||||
if (readyTime > 0 && Date.now() - readyTime > this.delay * 5) {
|
if (readySinceTime > 0 && Date.now() - readySinceTime > this.delay * 5) {
|
||||||
dialog.componentInstance.data.title =
|
dialog.componentInstance.data.title =
|
||||||
django.gettext('Service ready') +
|
django.gettext('Service ready') +
|
||||||
' - ' +
|
' - ' +
|
||||||
@ -137,9 +117,9 @@ export class Plugin {
|
|||||||
'</a>';
|
'</a>';
|
||||||
}
|
}
|
||||||
if (data.status === 'ready') {
|
if (data.status === 'ready') {
|
||||||
if (readyTime === -1) {
|
if (readySinceTime === -1) {
|
||||||
// Service is ready, wait for client, update dialog text
|
// Service is ready, wait for client, update dialog text
|
||||||
readyTime = Date.now(); // Milisecodns
|
readySinceTime = Date.now(); // Milisecodns
|
||||||
dialog.componentInstance.data.title =
|
dialog.componentInstance.data.title =
|
||||||
django.gettext('Service ready');
|
django.gettext('Service ready');
|
||||||
dialog.componentInstance.data.body = django.gettext(
|
dialog.componentInstance.data.body = django.gettext(
|
||||||
@ -167,7 +147,7 @@ export class Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processExternalUrl(url: string): Promise<void> {
|
private async processExternalUrl(url: string) {
|
||||||
const dialog = await this.showAlert(
|
const dialog = await this.showAlert(
|
||||||
django.gettext('Please wait until the service is launched.'),
|
django.gettext('Please wait until the service is launched.'),
|
||||||
django.gettext(
|
django.gettext(
|
||||||
@ -281,7 +261,7 @@ export class Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async notifyError(error?: any): Promise<void> {
|
private async notifyError(error?: any) {
|
||||||
let msg: string = django.gettext(
|
let msg: string = django.gettext(
|
||||||
'Error communicating with your service. Please, retry again.'
|
'Error communicating with your service. Please, retry again.'
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<div class="about">
|
<div class="about">
|
||||||
<h1>Universal Desktop Services {{ api.config.version }} build {{ api.config.version_stamp }}</h1>
|
<h1>Universal Desktop Services {{ api.config.version }} build {{ api.config.version_stamp }}</h1>
|
||||||
<h3><a href="http://www.udsenterprise.com" target="_blank"> © 2012-{{ year }} Virtual Cable S.L.U.</a></h3>
|
<h3><a rel="noopener noreferrer" href="http://www.udsenterprise.com" target="_blank"> © 2012-{{ year }} Virtual Cable S.L.U.</a></h3>
|
||||||
<h4>
|
<h4>
|
||||||
<uds-translate>You can access UDS Open Source code at</uds-translate> <a href="https://github.com/dkmstr/openuds"
|
<uds-translate>You can access UDS Open Source code at</uds-translate> <a rel="noopener noreferrer" href="https://github.com/dkmstr/openuds"
|
||||||
target="_blank">OpenUDS github repository</a>
|
target="_blank">OpenUDS github repository</a>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="components">
|
<div class="components">
|
||||||
@ -10,15 +10,15 @@
|
|||||||
<uds-translate>UDS has been developed using these components:</uds-translate>
|
<uds-translate>UDS has been developed using these components:</uds-translate>
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://www.python.org/">Python</a></li>
|
<li><a rel="noopener noreferrer" href="https://www.python.org/" target="_blank">Python</a></li>
|
||||||
<li><a href="https://www.typescriptlang.org/" target="_blank">TypeScript</a></li>
|
<li><a rel="noopener noreferrer" href="https://www.typescriptlang.org/" target="_blank">TypeScript</a></li>
|
||||||
<li><a href="https://www.djangoproject.com/" target="_blank">Django</a></li>
|
<li><a rel="noopener noreferrer" href="https://www.djangoproject.com/" target="_blank">Django</a></li>
|
||||||
<li><a href="https://angular.io" target="_blank">Angular</a></li>
|
<li><a rel="noopener noreferrer" href="https://angular.io" target="_blank">Angular</a></li>
|
||||||
<li><a href="https://guac-dev.org/" target="_blank">Guacamole</a></li>
|
<li><a rel="noopener noreferrer" href="https://guac-dev.org/" target="_blank">Guacamole</a></li>
|
||||||
<li><a href="https://weasyprint.org/" target="_blank">weasyprint</a></li>
|
<li><a rel="noopener noreferrer" href="https://weasyprint.org/" target="_blank">weasyprint</a></li>
|
||||||
<li><a href="https://kde-look.org/content/show.php/Crystal+Project?content=60475)" target="_blank">Crystal project
|
<li><a rel="noopener noreferrer" href="https://kde-look.org/content/show.php/Crystal+Project?content=60475)" target="_blank">Crystal project
|
||||||
icons</a></li>
|
icons</a></li>
|
||||||
<li><a href="https://github.com/NitruxSA/flattr-icons" target="_blank">Flattr Icons</a></li>
|
<li><a rel="noopener noreferrer" href="https://github.com/NitruxSA/flattr-icons" target="_blank">Flattr Icons</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><small>* <uds-translate>If you find that we missed any component, please let us know</uds-translate></small></p>
|
<p><small>* <uds-translate>If you find that we missed any component, please let us know</uds-translate></small></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="plugins">
|
<div class="plugins">
|
||||||
<div [class]="css(p)" *ngFor="let p of api.plugins" (click)="download(p.url)">
|
<div [class]="css(p)" *ngFor="let p of api.plugins" (click)="api.download(p.url)">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img [src]="img(p.name)">
|
<img [src]="img(p.name)">
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,10 +15,6 @@ export class ClientDownloadComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
download(url: string) {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
img(image: string) {
|
img(image: string) {
|
||||||
return this.api.staticURL( 'modern/img/' + image + '.png');
|
return this.api.staticURL( 'modern/img/' + image + '.png');
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actors">
|
<div class="actors">
|
||||||
<div [class]="css(p.name)" *ngFor="let p of actors" (click)="download(p.url)">
|
<div [class]="css(p.name)" *ngFor="let p of actors" (click)="api.download(p.url)">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img [src]="img(p.name)">
|
<img [src]="img(p.name)">
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,31 +5,27 @@ import { Downloadable } from '../../types/config';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'uds-downloads',
|
selector: 'uds-downloads',
|
||||||
templateUrl: './downloads.component.html',
|
templateUrl: './downloads.component.html',
|
||||||
styleUrls: ['./downloads.component.scss']
|
styleUrls: ['./downloads.component.scss'],
|
||||||
})
|
})
|
||||||
export class DownloadsComponent implements OnInit {
|
export class DownloadsComponent implements OnInit {
|
||||||
|
|
||||||
actors: Downloadable[] = [];
|
actors: Downloadable[] = [];
|
||||||
|
|
||||||
constructor(public api: UDSApiService) { }
|
constructor(public api: UDSApiService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.actors = []; // Put legacy at end of downloadables...
|
// Sort legacy actors to the end of the list
|
||||||
|
this.actors = []; // Put legacy at end of downloadables...
|
||||||
const legacy: Downloadable[] = [];
|
const legacy: Downloadable[] = [];
|
||||||
this.api.actors.forEach(a => {
|
for (const a of this.api.actors) {
|
||||||
if (a.name.includes('legacy')) {
|
if (a.name.includes('legacy')) {
|
||||||
legacy.push(a);
|
legacy.push(a);
|
||||||
} else {
|
} else {
|
||||||
this.actors.push(a);
|
this.actors.push(a);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
legacy.forEach(l => {
|
for (const l of legacy) {
|
||||||
this.actors.push(l);
|
this.actors.push(l);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
download(url: string) {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img(filename: string) {
|
img(filename: string) {
|
||||||
@ -52,5 +48,4 @@ export class DownloadsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export class ErrorComponent implements OnInit {
|
|||||||
await this.getError();
|
await this.getError();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getError(): Promise<void> {
|
async getError() {
|
||||||
const id = this.route.snapshot.paramMap.get('id') || '-1';
|
const id = this.route.snapshot.paramMap.get('id') || '-1';
|
||||||
if (id === '19') { // 19 is MFA error, return to MFA
|
if (id === '19') { // 19 is MFA error, return to MFA
|
||||||
this.returnUrl = '/mfa';
|
this.returnUrl = '/mfa';
|
||||||
|
@ -42,6 +42,9 @@ export interface UDSApiServiceType {
|
|||||||
/* Executes logout */
|
/* Executes logout */
|
||||||
logout(): void;
|
logout(): void;
|
||||||
|
|
||||||
|
/* Download file/launches a custom uro */
|
||||||
|
download(url: string): Promise<void>;
|
||||||
|
|
||||||
/* sleep milliseconds */
|
/* sleep milliseconds */
|
||||||
sleep(ms: number): Promise<void>;
|
sleep(ms: number): Promise<void>;
|
||||||
/**
|
/**
|
||||||
|
@ -19,7 +19,6 @@ import { UDSApiServiceType } from './uds-api.service-type';
|
|||||||
|
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
|
|
||||||
const DARK_THEME = 'dark-theme';
|
const DARK_THEME = 'dark-theme';
|
||||||
const LIGHT_THEME = 'light-theme';
|
const LIGHT_THEME = 'light-theme';
|
||||||
const TIMEOUT = 10000;
|
const TIMEOUT = 10000;
|
||||||
@ -32,14 +31,10 @@ const toPromise = <T>(observable: Observable<T>, wait?: number): Promise<T> => {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class UDSApiService implements UDSApiServiceType {
|
export class UDSApiService implements UDSApiServiceType {
|
||||||
readonly user: User;
|
readonly user: User;
|
||||||
transportsWindow: Window|null = null;
|
transportsWindow: Window | null = null;
|
||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
|
|
||||||
constructor(
|
constructor(private http: HttpClient, public gui: UDSGuiService, public router: Router) {
|
||||||
private http: HttpClient,
|
|
||||||
public gui: UDSGuiService,
|
|
||||||
public router: Router
|
|
||||||
) {
|
|
||||||
this.user = new User(udsData.profile);
|
this.user = new User(udsData.profile);
|
||||||
this.plugin = new Plugin(this);
|
this.plugin = new Plugin(this);
|
||||||
}
|
}
|
||||||
@ -91,32 +86,20 @@ export class UDSApiService implements UDSApiServiceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Client enabler */
|
/* Client enabler */
|
||||||
async enabler(
|
async enabler(serviceId: string, transportId: string): Promise<JSONEnabledService> {
|
||||||
serviceId: string,
|
const enabler = this.config.urls.enabler.replace('param1', serviceId).replace('param2', transportId);
|
||||||
transportId: string
|
|
||||||
): Promise<JSONEnabledService> {
|
|
||||||
const enabler = this.config.urls.enabler
|
|
||||||
.replace('param1', serviceId)
|
|
||||||
.replace('param2', transportId);
|
|
||||||
return toPromise(this.http.get<JSONEnabledService>(enabler));
|
return toPromise(this.http.get<JSONEnabledService>(enabler));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check userService status */
|
/* Check userService status */
|
||||||
async status(
|
async status(serviceId: string, transportId: string): Promise<JSONStatusService> {
|
||||||
serviceId: string,
|
const status = this.config.urls.status.replace('param1', serviceId).replace('param2', transportId);
|
||||||
transportId: string
|
|
||||||
): Promise<JSONStatusService> {
|
|
||||||
const status = this.config.urls.status
|
|
||||||
.replace('param1', serviceId)
|
|
||||||
.replace('param2', transportId);
|
|
||||||
return toPromise(this.http.get<JSONStatusService>(status));
|
return toPromise(this.http.get<JSONStatusService>(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Services resetter */
|
/* Services resetter */
|
||||||
async action(action: string, serviceId: string): Promise<JSONService> {
|
async action(action: string, serviceId: string): Promise<JSONService> {
|
||||||
const actionURL = this.config.urls.action
|
const actionURL = this.config.urls.action.replace('param1', serviceId).replace('param2', action);
|
||||||
.replace('param1', serviceId)
|
|
||||||
.replace('param2', action);
|
|
||||||
return toPromise(this.http.get<JSONService>(actionURL));
|
return toPromise(this.http.get<JSONService>(actionURL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,9 +114,7 @@ export class UDSApiService implements UDSApiServiceType {
|
|||||||
password: string,
|
password: string,
|
||||||
domain: string
|
domain: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const url = this.config.urls.updateTransportTicket
|
const url = this.config.urls.updateTransportTicket.replace('param1', ticketId).replace('param2', scrambler);
|
||||||
.replace('param1', ticketId)
|
|
||||||
.replace('param2', scrambler);
|
|
||||||
return toPromise(
|
return toPromise(
|
||||||
this.http.post<any>(url, {
|
this.http.post<any>(url, {
|
||||||
username,
|
username,
|
||||||
@ -164,20 +145,14 @@ export class UDSApiService implements UDSApiServiceType {
|
|||||||
* Gets services information
|
* Gets services information
|
||||||
*/
|
*/
|
||||||
async getServicesInformation(): Promise<JSONServicesInformation> {
|
async getServicesInformation(): Promise<JSONServicesInformation> {
|
||||||
return toPromise(
|
return toPromise(this.http.get<JSONServicesInformation>(this.config.urls.services));
|
||||||
this.http.get<JSONServicesInformation>(this.config.urls.services)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets error string from a code
|
* Gets error string from a code
|
||||||
*/
|
*/
|
||||||
async getErrorInformation(errorCode: string): Promise<JSONErrorInformation> {
|
async getErrorInformation(errorCode: string): Promise<JSONErrorInformation> {
|
||||||
return toPromise(
|
return toPromise(this.http.get<JSONErrorInformation>(this.config.urls.error.replace('9999', errorCode)));
|
||||||
this.http.get<JSONErrorInformation>(
|
|
||||||
this.config.urls.error.replace('9999', errorCode)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,6 +175,28 @@ export class UDSApiService implements UDSApiServiceType {
|
|||||||
window.location.href = this.config.urls.logout;
|
window.location.href = this.config.urls.logout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async download(url: string): Promise<void> {
|
||||||
|
// Launch the download
|
||||||
|
// Create an iframe and set the src to the url
|
||||||
|
// This will trigger the download
|
||||||
|
// First, loof for an existing download iframe
|
||||||
|
let iframe = document.getElementById('download') as HTMLIFrameElement;
|
||||||
|
// If not found, create it
|
||||||
|
if (!iframe) {
|
||||||
|
iframe = document.createElement('iframe');
|
||||||
|
iframe.id = 'download';
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
}
|
||||||
|
// Set the src to the url
|
||||||
|
iframe.src = url;
|
||||||
|
// onload in iframe will only be triggered if an html page is downloaded. If it is loaded, it's not a download
|
||||||
|
iframe.onload = () => {
|
||||||
|
alert('Error downloading file. Please try again later.');
|
||||||
|
};
|
||||||
|
//window.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
sleep(ms: number): Promise<void> {
|
sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@ -215,9 +212,7 @@ export class UDSApiService implements UDSApiServiceType {
|
|||||||
* @returns Observable
|
* @returns Observable
|
||||||
*/
|
*/
|
||||||
async getAuthCustomJavascript(authId: string): Promise<string> {
|
async getAuthCustomJavascript(authId: string): Promise<string> {
|
||||||
return toPromise(
|
return toPromise(this.http.get(this.config.urls.customAuth + authId, { responseType: 'text' }));
|
||||||
this.http.get(this.config.urls.customAuth + authId, {responseType: 'text'})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch dark/light theme
|
// Switch dark/light theme
|
||||||
|
Loading…
x
Reference in New Issue
Block a user