Extract toolbox buttons into standalone reusable classes
Refactor Toolbox button code into separate, focused classes following Single Responsibility Principle. New Button Classes: - ExitXRButton (src/objects/buttons/ExitXRButton.ts) * Encapsulates exit XR functionality * Takes XR experience, scene, parent, and optional position * Handles click events to exit XR session * Provides dispose() and transform getter - ConfigButton (src/objects/buttons/ConfigButton.ts) * Encapsulates config panel toggle functionality * Uses dependency injection for toggle callback * Configurable position relative to parent * Provides dispose() and transform getter - RenderModeButton (src/objects/buttons/RenderModeButton.ts) * Encapsulates render mode cycling functionality * Internally manages render mode state * Automatically recreates button with new label on mode change * Cycles through all available rendering modes * Provides dispose() and transform getter Toolbox Changes: - Removed createRenderModeButton() and updateRenderModeButton() methods - Simplified setupXRButton() to instantiate button classes - Reduced button-related code by ~120 lines - Added button instance properties for cleanup - Clean, declarative button creation with clear positioning Benefits: - Single Responsibility Principle - each button class has one purpose - Reusability - buttons can be used anywhere in the app - Testability - each button can be tested independently - Cleaner code - Toolbox class focuses on core tool management - Better dependencies - clear interfaces and dependency injection - Easier maintenance - button logic isolated and self-contained 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e329b95f2f
commit
7849bf4eb2
60
src/objects/buttons/ConfigButton.ts
Normal file
60
src/objects/buttons/ConfigButton.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {Scene, TransformNode, Vector3} from "@babylonjs/core";
|
||||||
|
import {Button} from "../Button";
|
||||||
|
import log, {Logger} from "loglevel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button that toggles the VR configuration panel
|
||||||
|
*/
|
||||||
|
export class ConfigButton {
|
||||||
|
private _button: Button;
|
||||||
|
private readonly _logger: Logger = log.getLogger('ConfigButton');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Config button
|
||||||
|
* @param toggleCallback Function to call when button is clicked
|
||||||
|
* @param scene BabylonJS scene
|
||||||
|
* @param parent Parent transform node to attach button to
|
||||||
|
* @param position Position relative to parent (default: bottom-left)
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly toggleCallback: () => void,
|
||||||
|
scene: Scene,
|
||||||
|
parent: TransformNode,
|
||||||
|
position: Vector3 = new Vector3(0.5, -0.35, 0)
|
||||||
|
) {
|
||||||
|
this._button = Button.CreateButton("config", "config", scene, {});
|
||||||
|
|
||||||
|
// Position button
|
||||||
|
this._button.transform.position = position;
|
||||||
|
this._button.transform.rotation.y = Math.PI; // Flip 180° to face correctly
|
||||||
|
this._button.transform.scaling = new Vector3(0.2, 0.2, 0.2);
|
||||||
|
this._button.transform.parent = parent;
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
this._button.onPointerObservable.add((evt) => {
|
||||||
|
if (evt.sourceEvent.type === 'pointerdown') {
|
||||||
|
this._logger.debug('Config button clicked');
|
||||||
|
this.toggleCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._logger.debug('ConfigButton created');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the button transform for external positioning/manipulation
|
||||||
|
*/
|
||||||
|
public get transform(): TransformNode {
|
||||||
|
return this._button.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of the button and clean up resources
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
if (this._button) {
|
||||||
|
this._button.dispose();
|
||||||
|
}
|
||||||
|
this._logger.debug('ConfigButton disposed');
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/objects/buttons/ExitXRButton.ts
Normal file
60
src/objects/buttons/ExitXRButton.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {Scene, TransformNode, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
|
||||||
|
import {Button} from "../Button";
|
||||||
|
import log, {Logger} from "loglevel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button that exits the XR session when clicked
|
||||||
|
*/
|
||||||
|
export class ExitXRButton {
|
||||||
|
private _button: Button;
|
||||||
|
private readonly _logger: Logger = log.getLogger('ExitXRButton');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Exit XR button
|
||||||
|
* @param xr WebXR experience to exit from
|
||||||
|
* @param scene BabylonJS scene
|
||||||
|
* @param parent Parent transform node to attach button to
|
||||||
|
* @param position Position relative to parent (default: bottom-right)
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly xr: WebXRDefaultExperience,
|
||||||
|
scene: Scene,
|
||||||
|
parent: TransformNode,
|
||||||
|
position: Vector3 = new Vector3(-0.5, -0.35, 0)
|
||||||
|
) {
|
||||||
|
this._button = Button.CreateButton("exitXr", "exitXr", scene, {});
|
||||||
|
|
||||||
|
// Position button
|
||||||
|
this._button.transform.position = position;
|
||||||
|
this._button.transform.rotation.y = Math.PI; // Flip 180° to face correctly
|
||||||
|
this._button.transform.scaling = new Vector3(0.2, 0.2, 0.2);
|
||||||
|
this._button.transform.parent = parent;
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
this._button.onPointerObservable.add((evt) => {
|
||||||
|
if (evt.sourceEvent.type === 'pointerdown') {
|
||||||
|
this._logger.debug('Exit XR button clicked');
|
||||||
|
this.xr.baseExperience.exitXRAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._logger.debug('ExitXRButton created');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the button transform for external positioning/manipulation
|
||||||
|
*/
|
||||||
|
public get transform(): TransformNode {
|
||||||
|
return this._button.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of the button and clean up resources
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
if (this._button) {
|
||||||
|
this._button.dispose();
|
||||||
|
}
|
||||||
|
this._logger.debug('ExitXRButton disposed');
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/objects/buttons/RenderModeButton.ts
Normal file
129
src/objects/buttons/RenderModeButton.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import {Color3, Scene, TransformNode, Vector3} from "@babylonjs/core";
|
||||||
|
import {Button} from "../Button";
|
||||||
|
import {LightmapGenerator} from "../../util/lightmapGenerator";
|
||||||
|
import {RenderingMode, RenderingModeLabels} from "../../util/renderingMode";
|
||||||
|
import log, {Logger} from "loglevel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button that cycles through rendering modes
|
||||||
|
*/
|
||||||
|
export class RenderModeButton {
|
||||||
|
private _button: Button;
|
||||||
|
private readonly _logger: Logger = log.getLogger('RenderModeButton');
|
||||||
|
private readonly _scene: Scene;
|
||||||
|
private readonly _parent: TransformNode;
|
||||||
|
private readonly _position: Vector3;
|
||||||
|
private readonly _scaling: Vector3;
|
||||||
|
|
||||||
|
private readonly _modes: RenderingMode[] = [
|
||||||
|
RenderingMode.LIGHTMAP_WITH_LIGHTING,
|
||||||
|
RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE,
|
||||||
|
RenderingMode.FLAT_EMISSIVE,
|
||||||
|
RenderingMode.DIFFUSE_WITH_LIGHTS
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Render Mode button
|
||||||
|
* @param scene BabylonJS scene
|
||||||
|
* @param parent Parent transform node to attach button to
|
||||||
|
* @param position Position relative to parent (default: center below)
|
||||||
|
* @param scaling Scaling for the button (default: 0.4, 0.4, 0.4)
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
scene: Scene,
|
||||||
|
parent: TransformNode,
|
||||||
|
position: Vector3 = new Vector3(0, -0.2, 0),
|
||||||
|
scaling: Vector3 = new Vector3(0.4, 0.4, 0.4)
|
||||||
|
) {
|
||||||
|
this._scene = scene;
|
||||||
|
this._parent = parent;
|
||||||
|
this._position = position;
|
||||||
|
this._scaling = scaling;
|
||||||
|
|
||||||
|
this.createButton();
|
||||||
|
this._logger.debug('RenderModeButton created');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or recreate the button with current mode label
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private createButton(): void {
|
||||||
|
const currentMode = LightmapGenerator.getRenderingMode();
|
||||||
|
|
||||||
|
this._button = Button.CreateButton(
|
||||||
|
`Mode: ${RenderingModeLabels[currentMode]}`,
|
||||||
|
'renderModeButton',
|
||||||
|
this._scene,
|
||||||
|
{
|
||||||
|
width: 0.5,
|
||||||
|
height: 0.2,
|
||||||
|
background: Color3.FromHexString("#333333"),
|
||||||
|
color: Color3.White(),
|
||||||
|
fontSize: 240
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Position button
|
||||||
|
this._button.transform.position = this._position;
|
||||||
|
this._button.transform.rotation.y = Math.PI;
|
||||||
|
this._button.transform.scaling = this._scaling;
|
||||||
|
this._button.transform.parent = this._parent;
|
||||||
|
|
||||||
|
// Add click handler to cycle through modes
|
||||||
|
this._button.onPointerObservable.add((evt) => {
|
||||||
|
if (evt.sourceEvent.type === 'pointerdown') {
|
||||||
|
this.cycleRenderMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle to the next rendering mode
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private cycleRenderMode(): void {
|
||||||
|
const currentMode = LightmapGenerator.getRenderingMode();
|
||||||
|
const currentIndex = this._modes.indexOf(currentMode);
|
||||||
|
const nextIndex = (currentIndex + 1) % this._modes.length;
|
||||||
|
const nextMode = this._modes[nextIndex];
|
||||||
|
|
||||||
|
this._logger.info(`Cycling to rendering mode: ${nextMode}`);
|
||||||
|
LightmapGenerator.updateAllMaterials(this._scene, nextMode);
|
||||||
|
|
||||||
|
// Recreate button with new label
|
||||||
|
this.updateButton(nextMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update button with new rendering mode label
|
||||||
|
* @param mode New rendering mode
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private updateButton(mode: RenderingMode): void {
|
||||||
|
// Dispose old button
|
||||||
|
if (this._button) {
|
||||||
|
this._button.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new button with updated text
|
||||||
|
this.createButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the button transform for external positioning/manipulation
|
||||||
|
*/
|
||||||
|
public get transform(): TransformNode {
|
||||||
|
return this._button.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of the button and clean up resources
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
if (this._button) {
|
||||||
|
this._button.dispose();
|
||||||
|
}
|
||||||
|
this._logger.debug('RenderModeButton disposed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,10 +3,11 @@ import {buildColor} from "./functions/buildColor";
|
|||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import {Handle} from "../objects/handle";
|
import {Handle} from "../objects/handle";
|
||||||
import {DefaultScene} from "../defaultScene";
|
import {DefaultScene} from "../defaultScene";
|
||||||
import {Button} from "../objects/Button";
|
|
||||||
import {LightmapGenerator} from "../util/lightmapGenerator";
|
|
||||||
import {RenderingMode, RenderingModeLabels} from "../util/renderingMode";
|
|
||||||
import {AnimatedLineTexture} from "../util/animatedLineTexture";
|
import {AnimatedLineTexture} from "../util/animatedLineTexture";
|
||||||
|
import {ExitXRButton} from "../objects/buttons/ExitXRButton";
|
||||||
|
import {ConfigButton} from "../objects/buttons/ConfigButton";
|
||||||
|
import {RenderModeButton} from "../objects/buttons/RenderModeButton";
|
||||||
|
import {LightmapGenerator} from "../util/lightmapGenerator";
|
||||||
|
|
||||||
const colors: string[] = [
|
const colors: string[] = [
|
||||||
"#222222", "#8b4513", "#006400", "#778899",
|
"#222222", "#8b4513", "#006400", "#778899",
|
||||||
@ -30,9 +31,13 @@ export class Toolbox {
|
|||||||
private readonly _handle: Handle;
|
private readonly _handle: Handle;
|
||||||
private readonly _scene: Scene;
|
private readonly _scene: Scene;
|
||||||
private _xr?: WebXRDefaultExperience;
|
private _xr?: WebXRDefaultExperience;
|
||||||
private _renderModeDisplay?: Button;
|
|
||||||
private _diagramMenuManager?: any; // Import would create circular dependency
|
private _diagramMenuManager?: any; // Import would create circular dependency
|
||||||
|
|
||||||
|
// Button instances
|
||||||
|
private _exitXRButton?: ExitXRButton;
|
||||||
|
private _configButton?: ConfigButton;
|
||||||
|
private _renderModeButton?: RenderModeButton;
|
||||||
|
|
||||||
constructor(readyObservable: Observable<boolean>) {
|
constructor(readyObservable: Observable<boolean>) {
|
||||||
this._scene = DefaultScene.Scene;
|
this._scene = DefaultScene.Scene;
|
||||||
this._toolboxBaseNode = new TransformNode("toolbox", this._scene);
|
this._toolboxBaseNode = new TransformNode("toolbox", this._scene);
|
||||||
@ -145,145 +150,33 @@ export class Toolbox {
|
|||||||
this._xr.baseExperience.onStateChangedObservable.add((state) => {
|
this._xr.baseExperience.onStateChangedObservable.add((state) => {
|
||||||
if (state == 2) { // WebXRState.IN_XR
|
if (state == 2) { // WebXRState.IN_XR
|
||||||
// Create exit XR button
|
// Create exit XR button
|
||||||
const exitButton = Button.CreateButton("exitXr", "exitXr", this._scene, {});
|
this._exitXRButton = new ExitXRButton(
|
||||||
|
this._xr,
|
||||||
|
this._scene,
|
||||||
|
this._toolboxBaseNode,
|
||||||
|
new Vector3(-0.5, -0.35, 0) // Bottom-right
|
||||||
|
);
|
||||||
|
|
||||||
// Position button at bottom-right of toolbox, matching handle size and orientation
|
// Create config button if diagram menu manager is available
|
||||||
exitButton.transform.position.x = -0.5; // Right side
|
|
||||||
exitButton.transform.position.y = -0.35; // Below color grid
|
|
||||||
exitButton.transform.position.z = 0; // Coplanar with toolbox
|
|
||||||
exitButton.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly
|
|
||||||
exitButton.transform.scaling = new Vector3(.2, .2, .2); // Match handle height
|
|
||||||
exitButton.transform.parent = this._toolboxBaseNode;
|
|
||||||
|
|
||||||
exitButton.onPointerObservable.add((evt) => {
|
|
||||||
this._logger.debug(evt);
|
|
||||||
if (evt.sourceEvent.type == 'pointerdown') {
|
|
||||||
this._xr.baseExperience.exitXRAsync();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create config button next to exit button
|
|
||||||
if (this._diagramMenuManager) {
|
if (this._diagramMenuManager) {
|
||||||
const configButton = Button.CreateButton("config", "config", this._scene, {});
|
this._configButton = new ConfigButton(
|
||||||
|
() => this._diagramMenuManager.toggleVRConfigPanel(),
|
||||||
// Position button at bottom-left of toolbox, opposite the exit button
|
this._scene,
|
||||||
configButton.transform.position.x = 0.5; // Left side
|
this._toolboxBaseNode,
|
||||||
configButton.transform.position.y = -0.35; // Below color grid (same as exit)
|
new Vector3(0.5, -0.35, 0) // Bottom-left
|
||||||
configButton.transform.position.z = 0; // Coplanar with toolbox
|
);
|
||||||
configButton.transform.rotation.y = Math.PI; // Flip 180° to face correctly
|
|
||||||
configButton.transform.scaling = new Vector3(.2, .2, .2); // Match exit button size
|
|
||||||
configButton.transform.parent = this._toolboxBaseNode;
|
|
||||||
|
|
||||||
configButton.onPointerObservable.add((evt) => {
|
|
||||||
this._logger.debug('Config button clicked', evt);
|
|
||||||
if (evt.sourceEvent.type == 'pointerdown') {
|
|
||||||
this._diagramMenuManager.toggleVRConfigPanel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create rendering mode button that cycles through modes
|
// Create rendering mode button
|
||||||
this.createRenderModeButton();
|
this._renderModeButton = new RenderModeButton(
|
||||||
|
this._scene,
|
||||||
|
this._toolboxBaseNode,
|
||||||
|
new Vector3(0, -0.2, 0), // Center below grid
|
||||||
|
new Vector3(0.4, 0.4, 0.4)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private createRenderModeButton() {
|
|
||||||
const modes = [
|
|
||||||
RenderingMode.LIGHTMAP_WITH_LIGHTING,
|
|
||||||
RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE,
|
|
||||||
RenderingMode.FLAT_EMISSIVE,
|
|
||||||
RenderingMode.DIFFUSE_WITH_LIGHTS
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentMode = LightmapGenerator.getRenderingMode();
|
|
||||||
|
|
||||||
this._renderModeDisplay = Button.CreateButton(
|
|
||||||
`Mode: ${RenderingModeLabels[currentMode]}`,
|
|
||||||
`renderModeButton`,
|
|
||||||
this._scene,
|
|
||||||
{
|
|
||||||
width: 0.5,
|
|
||||||
height: 0.2,
|
|
||||||
background: Color3.FromHexString("#333333"),
|
|
||||||
color: Color3.White(),
|
|
||||||
fontSize: 240
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Position below the color grid
|
|
||||||
this._renderModeDisplay.transform.position.x = 0;
|
|
||||||
this._renderModeDisplay.transform.position.y = -.2;
|
|
||||||
this._renderModeDisplay.transform.position.z = 0;
|
|
||||||
this._renderModeDisplay.transform.rotation.y = Math.PI;
|
|
||||||
this._renderModeDisplay.transform.scaling = new Vector3(.4, .4, .4);
|
|
||||||
this._renderModeDisplay.transform.parent = this._toolboxBaseNode;
|
|
||||||
|
|
||||||
// Add click handler to cycle through modes
|
|
||||||
this._renderModeDisplay.onPointerObservable.add((evt) => {
|
|
||||||
if (evt.sourceEvent.type == 'pointerdown') {
|
|
||||||
const currentMode = LightmapGenerator.getRenderingMode();
|
|
||||||
const currentIndex = modes.indexOf(currentMode);
|
|
||||||
const nextIndex = (currentIndex + 1) % modes.length;
|
|
||||||
const nextMode = modes[nextIndex];
|
|
||||||
|
|
||||||
this._logger.info(`Cycling to rendering mode: ${nextMode}`);
|
|
||||||
LightmapGenerator.updateAllMaterials(this._scene, nextMode);
|
|
||||||
|
|
||||||
// Update button text
|
|
||||||
this.updateRenderModeButton(nextMode);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateRenderModeButton(mode: RenderingMode) {
|
|
||||||
if (this._renderModeDisplay) {
|
|
||||||
// Dispose old button and create new one with updated text
|
|
||||||
this._renderModeDisplay.dispose();
|
|
||||||
|
|
||||||
this._renderModeDisplay = Button.CreateButton(
|
|
||||||
`Mode: ${RenderingModeLabels[mode]}`,
|
|
||||||
`renderModeButton`,
|
|
||||||
this._scene,
|
|
||||||
{
|
|
||||||
width: 0.5,
|
|
||||||
height: 0.2,
|
|
||||||
background: Color3.FromHexString("#333333"),
|
|
||||||
color: Color3.White(),
|
|
||||||
fontSize: 240
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this._renderModeDisplay.transform.position.x = 0;
|
|
||||||
this._renderModeDisplay.transform.position.y = -.2;
|
|
||||||
this._renderModeDisplay.transform.position.z = 0;
|
|
||||||
this._renderModeDisplay.transform.rotation.y = Math.PI;
|
|
||||||
this._renderModeDisplay.transform.scaling = new Vector3(.15, .15, .15);
|
|
||||||
this._renderModeDisplay.transform.parent = this._toolboxBaseNode;
|
|
||||||
|
|
||||||
// Re-attach the click handler
|
|
||||||
this._renderModeDisplay.onPointerObservable.add((evt) => {
|
|
||||||
if (evt.sourceEvent.type == 'pointerdown') {
|
|
||||||
const modes = [
|
|
||||||
RenderingMode.LIGHTMAP_WITH_LIGHTING,
|
|
||||||
RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE,
|
|
||||||
RenderingMode.FLAT_EMISSIVE,
|
|
||||||
RenderingMode.DIFFUSE_WITH_LIGHTS
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentMode = LightmapGenerator.getRenderingMode();
|
|
||||||
const currentIndex = modes.indexOf(currentMode);
|
|
||||||
const nextIndex = (currentIndex + 1) % modes.length;
|
|
||||||
const nextMode = modes[nextIndex];
|
|
||||||
|
|
||||||
this._logger.info(`Cycling to rendering mode: ${nextMode}`);
|
|
||||||
LightmapGenerator.updateAllMaterials(this._scene, nextMode);
|
|
||||||
|
|
||||||
// Update button text
|
|
||||||
this.updateRenderModeButton(nextMode);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user