mirror of
https://github.com/LucasDower/ObjToSchematic.git
synced 2025-12-11 20:15:30 +01:00
Updated action output styling
This commit is contained in:
parent
2881d0f957
commit
c887e7ff5c
@ -1,6 +1,6 @@
|
||||
import { UI } from './ui/layout';
|
||||
import { Renderer } from './renderer';
|
||||
import { StatusHandler } from './status';
|
||||
import { StatusHandler, StatusMessage } from './status';
|
||||
import { UIMessageBuilder } from './ui/misc';
|
||||
import { ArcballCamera } from './camera';
|
||||
|
||||
@ -11,6 +11,7 @@ import { LOG } from './util/log_util';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { EAction } from './util';
|
||||
import { AppConfig } from './config';
|
||||
import { OutputStyle } from './ui/elements/output';
|
||||
|
||||
export class AppContext {
|
||||
private _ui: UI;
|
||||
@ -27,74 +28,90 @@ export class AppContext {
|
||||
this._ui.disable(EAction.Voxelise);
|
||||
|
||||
this._workerController = new WorkerController(path.resolve(__dirname, 'worker_interface.js'));
|
||||
|
||||
|
||||
Renderer.Get.toggleIsAxesEnabled();
|
||||
ArcballCamera.Get.setCameraMode('perspective');
|
||||
ArcballCamera.Get.toggleAngleSnap();
|
||||
}
|
||||
|
||||
public do(action: EAction) {
|
||||
LOG(`Doing ${action}`);
|
||||
this._ui.disable(action + 1);
|
||||
this._ui.disable(action);
|
||||
this._ui.cacheValues(action);
|
||||
StatusHandler.Get.clear();
|
||||
|
||||
const actionCommand = this._getWorkerJob(action);
|
||||
const workerJob = this._getWorkerJob(action);
|
||||
const uiOutput = this._ui.getActionOutput(action);
|
||||
|
||||
const jobCallback = (payload: TFromWorkerMessage) => {
|
||||
if (payload.action === 'KnownError') {
|
||||
uiOutput.setMessage(UIMessageBuilder.fromString(payload.error.message), 'error');
|
||||
} else if (payload.action === 'UnknownError') {
|
||||
uiOutput.setMessage(UIMessageBuilder.fromString('Something went wrong...'), 'error');
|
||||
} else {
|
||||
// The job was successful
|
||||
const infoStatuses = payload.statusMessages
|
||||
.filter(x => x.status === 'info')
|
||||
.map(x => x.message);
|
||||
const hasInfos = infoStatuses.length > 0;
|
||||
|
||||
const warningStatuses = payload.statusMessages
|
||||
.filter(x => x.status === 'warning')
|
||||
.map(x => x.message);
|
||||
const hasWarnings = warningStatuses.length > 0;
|
||||
|
||||
const builder = new UIMessageBuilder();
|
||||
builder.addBold(StatusHandler.Get.getDefaultSuccessMessage(action) + (hasInfos ? ':' : ''));
|
||||
builder.addItem(...infoStatuses);
|
||||
|
||||
if (hasWarnings) {
|
||||
builder.addHeading('There were some warnings:');
|
||||
builder.addItem(...warningStatuses);
|
||||
this._ui.enable(action);
|
||||
switch (payload.action) {
|
||||
case 'KnownError': {
|
||||
const builder = uiOutput.getMessage();
|
||||
{
|
||||
builder.clear('action');
|
||||
builder.addHeading('action', StatusHandler.Get.getDefaultFailureMessage(action), 'error')
|
||||
builder.addItem('action', [ payload.error.message ], 'error');
|
||||
}
|
||||
uiOutput.setMessage(builder);
|
||||
break;
|
||||
}
|
||||
case 'UnknownError': {
|
||||
const builder = uiOutput.getMessage();
|
||||
{
|
||||
builder.clear('action');
|
||||
builder.addHeading('action', StatusHandler.Get.getDefaultFailureMessage(action), 'error')
|
||||
builder.addItem('action', [ 'Something unexpectedly went wrong...' ], 'error');
|
||||
}
|
||||
uiOutput.setMessage(builder);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this._ui.enable(action + 1);
|
||||
|
||||
const { builder, style } = this._getActionMessageBuilder(action, payload.statusMessages);
|
||||
uiOutput.setMessage(builder, style as OutputStyle);
|
||||
this._ui.getActionButton(action)
|
||||
.removeLabelOverride()
|
||||
.stopLoading();
|
||||
|
||||
uiOutput.setMessage(builder, hasWarnings ? 'warning' : 'success');
|
||||
this._ui.getActionButton(action)
|
||||
.removeLabelOverride()
|
||||
.stopLoading();
|
||||
this._ui.enable(action);
|
||||
this._ui.enable(action + 1);
|
||||
|
||||
if (actionCommand.callback) {
|
||||
actionCommand.callback(payload);
|
||||
if (workerJob.callback) {
|
||||
workerJob.callback(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._workerController.addJob({
|
||||
id: actionCommand.id,
|
||||
payload: actionCommand.payload,
|
||||
id: workerJob.id,
|
||||
payload: workerJob.payload,
|
||||
callback: jobCallback,
|
||||
});
|
||||
}
|
||||
|
||||
this._ui.getActionButton(action)
|
||||
.setLabelOverride('Loading...')
|
||||
.startLoading();
|
||||
this._ui.disable(action);
|
||||
private _getActionMessageBuilder(action: EAction, statusMessages: StatusMessage[]) {
|
||||
const infoStatuses = statusMessages
|
||||
.filter(x => x.status === 'info')
|
||||
.map(x => x.message);
|
||||
const hasInfos = infoStatuses.length > 0;
|
||||
|
||||
const warningStatuses = statusMessages
|
||||
.filter(x => x.status === 'warning')
|
||||
.map(x => x.message);
|
||||
const hasWarnings = warningStatuses.length > 0;
|
||||
|
||||
const builder = new UIMessageBuilder();
|
||||
builder.addBold('action', [StatusHandler.Get.getDefaultSuccessMessage(action) + (hasInfos ? ':' : '')], 'success');
|
||||
builder.addItem('action', infoStatuses, 'success');
|
||||
|
||||
if (hasWarnings) {
|
||||
builder.addHeading('action', 'There were some warnings:', 'warning');
|
||||
builder.addItem('action', warningStatuses, 'warning');
|
||||
}
|
||||
|
||||
return { builder: builder, style: hasWarnings ? 'warning' : 'success' };
|
||||
}
|
||||
|
||||
private _getWorkerJob(action: EAction): TWorkerJob {
|
||||
switch(action) {
|
||||
switch (action) {
|
||||
case EAction.Import:
|
||||
return this._import();
|
||||
}
|
||||
@ -106,7 +123,7 @@ export class AppContext {
|
||||
|
||||
const payload: TToWorkerMessage = {
|
||||
action: 'Import',
|
||||
params: {
|
||||
params: {
|
||||
filepath: uiElements.input.getCachedValue()
|
||||
}
|
||||
};
|
||||
@ -118,13 +135,24 @@ export class AppContext {
|
||||
|
||||
if (payload.result.triangleCount < AppConfig.RENDER_TRIANGLE_THRESHOLD) {
|
||||
this._workerController.addJob(this._renderMesh());
|
||||
|
||||
const builder = this._ui.getActionOutput(EAction.Import).getMessage();
|
||||
builder.clear('render');
|
||||
builder.addTask('render', `Rendering mesh...`);
|
||||
this._ui.getActionOutput(EAction.Import).setMessage(builder);
|
||||
} else {
|
||||
this._ui.getActionOutput(EAction.Import)
|
||||
.addMessage(UIMessageBuilder.fromString(`Will not render mesh as its over ${AppConfig.RENDER_TRIANGLE_THRESHOLD} triangles.`))
|
||||
.setStyle('warning');
|
||||
const builder = this._ui.getActionOutput(EAction.Import).getMessage();
|
||||
builder.clear('render');
|
||||
builder.addHeading('render', 'Render', 'warning');
|
||||
builder.addItem('render', [`Will not render mesh as its over ${AppConfig.RENDER_TRIANGLE_THRESHOLD} triangles.`], 'warning');
|
||||
this._ui.getActionOutput(EAction.Import).setMessage(builder);
|
||||
}
|
||||
};
|
||||
|
||||
const builder = new UIMessageBuilder();
|
||||
builder.addTask('action', 'Loading mesh...');
|
||||
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'none');
|
||||
|
||||
return { id: 'Import', payload: payload, callback: callback };
|
||||
}
|
||||
|
||||
@ -138,27 +166,28 @@ export class AppContext {
|
||||
// This callback is not managed through `AppContext::do`, therefore
|
||||
// we need to check the payload is not an error
|
||||
if (payload.action === 'KnownError') {
|
||||
const builder = UIMessageBuilder
|
||||
.create()
|
||||
.addHeading('Could not draw the mesh')
|
||||
.addItem(payload.error.message);
|
||||
const builder = this._ui.getActionOutput(EAction.Import).getMessage()
|
||||
.clear('render')
|
||||
.addHeading('render', 'Could not draw the mesh', 'error')
|
||||
.addItem('render', [payload.error.message], 'error');
|
||||
|
||||
this._ui.getActionOutput(EAction.Import)
|
||||
.addMessage(builder)
|
||||
.setStyle('warning');
|
||||
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'warning');
|
||||
|
||||
} else if (payload.action === 'UnknownError') {
|
||||
const builder = UIMessageBuilder
|
||||
.create()
|
||||
.addBold('Could not draw the mesh')
|
||||
|
||||
this._ui.getActionOutput(EAction.Import)
|
||||
.addMessage(builder)
|
||||
.setStyle('warning');
|
||||
const builder = this._ui.getActionOutput(EAction.Import).getMessage()
|
||||
.clear('render')
|
||||
.addBold('render', ['Could not draw the mesh'], 'error');
|
||||
|
||||
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'warning');
|
||||
} else {
|
||||
const builder = this._ui.getActionOutput(EAction.Import).getMessage()
|
||||
.clear('render')
|
||||
.addBold('render', [ 'Rendered mesh' ], 'success');
|
||||
|
||||
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'success');
|
||||
|
||||
ASSERT(payload.action === 'RenderMesh');
|
||||
Renderer.Get.useMesh(payload.result.buffers);
|
||||
Renderer.Get.useMesh(payload.result);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { RGBA, RGBAUtil } from './colour';
|
||||
import { Texture } from './texture';
|
||||
import { LOG } from './util/log_util';
|
||||
import { TMeshBufferDescription } from './buffer';
|
||||
import { RenderMeshParams } from './worker_types';
|
||||
|
||||
/* eslint-disable */
|
||||
export enum MeshType {
|
||||
@ -166,11 +167,11 @@ export class Renderer {
|
||||
this.setModelToUse(MeshType.None);
|
||||
}
|
||||
|
||||
public useMesh(meshDescription: TMeshBufferDescription[]) {
|
||||
public useMesh(params: RenderMeshParams.Output) {
|
||||
LOG('Using mesh');
|
||||
this._materialBuffers = [];
|
||||
|
||||
for (const { material, buffer, numElements } of meshDescription) {
|
||||
for (const { material, buffer, numElements } of params.buffers) {
|
||||
if (material.type === MaterialType.solid) {
|
||||
this._materialBuffers.push({
|
||||
buffer: twgl.createBufferInfoFromArrays(this._gl, buffer),
|
||||
@ -199,12 +200,9 @@ export class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: const dimensions = mesh.getBounds().getDimensions();
|
||||
const dimensions = new Vector3(1, 1, 1);
|
||||
|
||||
this._gridBuffers.x[MeshType.TriangleMesh] = DebugGeometryTemplates.gridX(dimensions);
|
||||
this._gridBuffers.y[MeshType.TriangleMesh] = DebugGeometryTemplates.gridY(dimensions);
|
||||
this._gridBuffers.z[MeshType.TriangleMesh] = DebugGeometryTemplates.gridZ(dimensions);
|
||||
this._gridBuffers.x[MeshType.TriangleMesh] = DebugGeometryTemplates.gridX(params.dimensions);
|
||||
this._gridBuffers.y[MeshType.TriangleMesh] = DebugGeometryTemplates.gridY(params.dimensions);
|
||||
this._gridBuffers.z[MeshType.TriangleMesh] = DebugGeometryTemplates.gridZ(params.dimensions);
|
||||
|
||||
this._modelsAvailable = 1;
|
||||
this.setModelToUse(MeshType.TriangleMesh);
|
||||
|
||||
@ -46,15 +46,15 @@ export class StatusHandler {
|
||||
public getDefaultSuccessMessage(action: EAction): string {
|
||||
switch (action) {
|
||||
case EAction.Import:
|
||||
return 'Successfully loaded mesh';
|
||||
return 'Loaded mesh';
|
||||
case EAction.Voxelise:
|
||||
return 'Successfully voxelised mesh';
|
||||
return 'Voxelised mesh';
|
||||
case EAction.Assign:
|
||||
return 'Successfully assigned blocks';
|
||||
return 'Assigned blocks';
|
||||
case EAction.Export:
|
||||
return 'Successfully exported mesh';
|
||||
return 'Exported mesh';
|
||||
default:
|
||||
return 'Successfully performed action';
|
||||
return 'Performed action';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
|
||||
public generateHTML() {
|
||||
return `
|
||||
${this._labelElement.generateHTML()}
|
||||
<div class="divider"></div>
|
||||
<div class="prop-right">
|
||||
${this.generateInnerHTML()}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ASSERT } from "../../util/error_util";
|
||||
import { UIMessageBuilder } from '../misc';
|
||||
|
||||
export type OutputStyle = 'success' | 'warning' | 'error';
|
||||
export type OutputStyle = 'success' | 'warning' | 'error' | 'none';
|
||||
|
||||
export class OutputElement {
|
||||
private _id: string;
|
||||
@ -30,7 +30,7 @@ export class OutputElement {
|
||||
|
||||
private _message: UIMessageBuilder;
|
||||
|
||||
public setMessage(message: UIMessageBuilder, style: OutputStyle) {
|
||||
public setMessage(message: UIMessageBuilder, style?: OutputStyle) {
|
||||
const element = document.getElementById(this._id) as HTMLDivElement;
|
||||
ASSERT(element !== null);
|
||||
|
||||
@ -40,17 +40,22 @@ export class OutputElement {
|
||||
element.innerHTML = this._message.toString();
|
||||
switch (style) {
|
||||
case 'success':
|
||||
element.classList.add('border-success');
|
||||
//element.classList.add('border-success');
|
||||
break;
|
||||
case 'warning':
|
||||
element.classList.add('border-warning');
|
||||
//element.classList.add('border-warning');
|
||||
break;
|
||||
case 'error':
|
||||
element.classList.add('border-error');
|
||||
//element.classList.add('border-error');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public getMessage() {
|
||||
return this._message;
|
||||
}
|
||||
|
||||
/*
|
||||
public addMessage(message: UIMessageBuilder) {
|
||||
this._message.join(message);
|
||||
|
||||
@ -60,6 +65,7 @@ export class OutputElement {
|
||||
element.innerHTML = this._message.toString();
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
|
||||
public setStyle(style: OutputStyle) {
|
||||
const element = document.getElementById(this._id) as HTMLDivElement;
|
||||
|
||||
@ -502,7 +502,11 @@ export class UI {
|
||||
}
|
||||
}
|
||||
|
||||
public disable(action: EAction) {
|
||||
public disableAll() {
|
||||
this.disable(EAction.Import, false);
|
||||
}
|
||||
|
||||
public disable(action: EAction, clearOutput: boolean = true) {
|
||||
if (action < 0) {
|
||||
return;
|
||||
}
|
||||
@ -514,7 +518,9 @@ export class UI {
|
||||
group.elements[compName].setEnabled(false);
|
||||
}
|
||||
group.submitButton.setEnabled(false);
|
||||
group.output.clearMessage();
|
||||
if (clearOutput) {
|
||||
group.output.clearMessage();
|
||||
}
|
||||
if (group.postElements) {
|
||||
LOG(group.label, 'has post-element');
|
||||
ASSERT(group.postElementsOrder);
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import { OutputStyle } from './elements/output';
|
||||
|
||||
type TMessage = {
|
||||
groupId: string,
|
||||
body: string,
|
||||
}
|
||||
|
||||
export class UIMessageBuilder {
|
||||
private _messages: string[];
|
||||
private _messages: TMessage[];
|
||||
|
||||
public constructor() {
|
||||
this._messages = [];
|
||||
@ -9,43 +16,80 @@ export class UIMessageBuilder {
|
||||
return new UIMessageBuilder();
|
||||
}
|
||||
|
||||
public addHeading(message: string) {
|
||||
this.addBold(message + ':');
|
||||
public addHeading(groupId: string, message: string, style: OutputStyle) {
|
||||
this.addBold(groupId, [message + ':'], style);
|
||||
return this;
|
||||
}
|
||||
|
||||
public addBold(...messages: string[]) {
|
||||
public addBold(groupId: string, messages: string[], style: OutputStyle) {
|
||||
for (const message of messages) {
|
||||
this._messages.push(`<b>${message}</b>`);
|
||||
const cssColourClass = this._getStatusCSSClass(style);
|
||||
this._messages.push({ groupId: groupId, body: `
|
||||
<div style="display: flex; align-items: center;" ${cssColourClass ? `class="${cssColourClass}"` : ''}>
|
||||
<div style="margin-right: 8px;" class="loader-circle"></div>
|
||||
<b>${message}</b>
|
||||
</div>
|
||||
`});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public add(...messages: string[]) {
|
||||
public addItem(groupId: string, messages: string[], style: OutputStyle) {
|
||||
for (const message of messages) {
|
||||
this._messages.push(message);
|
||||
const cssColourClass = this._getStatusCSSClass(style);
|
||||
this._messages.push({ groupId: groupId, body: `
|
||||
<div style="padding-left: 16px;" ${cssColourClass ? `class="${cssColourClass}"` : ''}> - ${message}</div>
|
||||
`});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public addItem(...messages: string[]) {
|
||||
for (const message of messages) {
|
||||
this._messages.push('• ' + message);
|
||||
}
|
||||
public addTask(groupId: string, message: string) {
|
||||
this._messages.push({ groupId: groupId, body: `
|
||||
<div style="display: flex; align-items: center; color: var(--text-standard)">
|
||||
<div style="margin-right: 8px;" class="loader-circle spin"></div>
|
||||
<b class="spin">${message}</b>
|
||||
</div>
|
||||
`});
|
||||
return this;
|
||||
}
|
||||
|
||||
public clear(groupId: string) {
|
||||
this._messages = this._messages.filter((x) => x.groupId !== groupId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this._messages.join('<br>');
|
||||
// Put together in a flexbox
|
||||
const divs = this._messages
|
||||
.map((x) => x.body)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div style="display: flex; flex-direction: column">
|
||||
${divs}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public static fromString(string: string): UIMessageBuilder {
|
||||
public static fromString(groupId: string, string: string, style: OutputStyle): UIMessageBuilder {
|
||||
const builder = new UIMessageBuilder();
|
||||
builder.addItem(string);
|
||||
builder.addItem(groupId, [string], style);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public join(builder: UIMessageBuilder) {
|
||||
this._messages.push(...builder._messages);
|
||||
}
|
||||
|
||||
private _getStatusCSSClass(status?: OutputStyle) {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'status-success';
|
||||
case 'warning':
|
||||
return 'status-warning';
|
||||
case 'error':
|
||||
return 'status-error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,8 @@ export class WorkerClient {
|
||||
ASSERT(this._loadedMesh !== undefined);
|
||||
|
||||
return {
|
||||
buffers: BufferGenerator.fromMesh(this._loadedMesh)
|
||||
buffers: BufferGenerator.fromMesh(this._loadedMesh),
|
||||
dimensions: this._loadedMesh.getBounds().getDimensions(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { TFromWorkerMessage, TToWorkerMessage } from './worker_types';
|
||||
export type TWorkerJob = {
|
||||
id: string,
|
||||
payload: TToWorkerMessage,
|
||||
callback?: (payload: TFromWorkerMessage) => void, // Called only if the job is successful
|
||||
callback?: (payload: TFromWorkerMessage) => void, // Called with the payload of the next message received by the worker
|
||||
}
|
||||
|
||||
export class WorkerController {
|
||||
|
||||
@ -5,6 +5,6 @@ addEventListener('message', (e) => {
|
||||
// TODO: Remove
|
||||
setTimeout(() => {
|
||||
postMessage(workerInstance.doWork(e.data));
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
//postMessage(workerInstance.doWork(e.data));
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { TMeshBufferDescription } from "./buffer"
|
||||
import { StatusMessage } from "./status"
|
||||
import { AppError } from "./util/error_util"
|
||||
import { Vector3 } from "./vector"
|
||||
|
||||
export namespace ImportParams {
|
||||
export type Input = {
|
||||
@ -19,6 +20,7 @@ export namespace RenderMeshParams {
|
||||
|
||||
export type Output = {
|
||||
buffers: TMeshBufferDescription[],
|
||||
dimensions: Vector3
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
styles.css
42
styles.css
@ -314,21 +314,18 @@ select:disabled {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.border-warning {
|
||||
border: 1.5px solid orange;
|
||||
transition-duration: 1s;
|
||||
.status-warning {
|
||||
transition-duration: 0.5s;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.border-success {
|
||||
border: 1.5px solid green;
|
||||
transition-duration: 1s;
|
||||
color: green;
|
||||
.status-success {
|
||||
transition-duration: 0.5s;
|
||||
color: var(--prop-accent-standard);
|
||||
}
|
||||
|
||||
.border-error {
|
||||
border: 1.5px solid rgb(156, 27, 27);
|
||||
transition-duration: 1s;
|
||||
.status-error {
|
||||
transition-duration: 0.5s;
|
||||
color: rgb(156, 27, 27);
|
||||
}
|
||||
|
||||
@ -553,4 +550,29 @@ svg {
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.loader-circle {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: blinker 0.75s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
0% {
|
||||
opacity: 100%;
|
||||
}
|
||||
50% {
|
||||
opacity: 50%;
|
||||
}
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user