adds "boardhost" to handle composition of ...
... breadboard, board, wires, and definition allocation.
This commit is contained in:
parent
af7c51b954
commit
cd9589e562
@ -17,6 +17,9 @@ namespace pxsim {
|
|||||||
radioState: RadioState;
|
radioState: RadioState;
|
||||||
neopixelState: NeoPixelState;
|
neopixelState: NeoPixelState;
|
||||||
|
|
||||||
|
// updates
|
||||||
|
updateSubscribers: (() => void)[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.id = "b" + Math_.random(2147483647);
|
this.id = "b" + Math_.random(2147483647);
|
||||||
@ -33,6 +36,12 @@ namespace pxsim {
|
|||||||
this.lightSensorState = new LightSensorState();
|
this.lightSensorState = new LightSensorState();
|
||||||
this.compassState = new CompassState();
|
this.compassState = new CompassState();
|
||||||
this.neopixelState = new NeoPixelState();
|
this.neopixelState = new NeoPixelState();
|
||||||
|
|
||||||
|
// updates
|
||||||
|
this.updateSubscribers = []
|
||||||
|
this.updateView = () => {
|
||||||
|
this.updateSubscribers.forEach(sub => sub());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveMessage(msg: SimulatorMessage) {
|
receiveMessage(msg: SimulatorMessage) {
|
||||||
@ -62,34 +71,20 @@ namespace pxsim {
|
|||||||
initAsync(msg: SimulatorRunMessage): Promise<void> {
|
initAsync(msg: SimulatorRunMessage): Promise<void> {
|
||||||
let options = (msg.options || {}) as RuntimeOptions;
|
let options = (msg.options || {}) as RuntimeOptions;
|
||||||
|
|
||||||
let boardDef = ARDUINO_ZERO; //TODO: read from pxt.json/pxttarget.json
|
//TODO: read from pxt.json/pxttarget.json
|
||||||
|
let boardDef = MICROBIT_DEF;
|
||||||
|
// let boardDef = ARDUINO_ZERO;
|
||||||
|
// let boardDef = SPARKFUN_PHOTON;
|
||||||
|
// let boardDef = RASPBERRYPI_MODELB;
|
||||||
|
|
||||||
let cmpsList = msg.parts;
|
let cmpsList = msg.parts;
|
||||||
cmpsList.sort();
|
|
||||||
let cmpDefs = COMPONENT_DEFINITIONS; //TODO: read from pxt.json/pxttarget.json
|
let cmpDefs = COMPONENT_DEFINITIONS; //TODO: read from pxt.json/pxttarget.json
|
||||||
let fnArgs = msg.fnArgs;
|
let fnArgs = msg.fnArgs;
|
||||||
|
|
||||||
let mb = true;
|
let viewHost = new visuals.BoardHost(this, boardDef, cmpsList, cmpDefs, fnArgs);
|
||||||
let view: visuals.GenericBoardSvg | visuals.MicrobitBoardSvg;
|
|
||||||
if (mb) {
|
|
||||||
view = new visuals.MicrobitBoardSvg({
|
|
||||||
runtime: runtime,
|
|
||||||
theme: visuals.randomTheme(),
|
|
||||||
activeComponents: cmpsList,
|
|
||||||
fnArgs: fnArgs,
|
|
||||||
disableTilt: false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
view = new visuals.GenericBoardSvg({
|
|
||||||
boardDef: boardDef,
|
|
||||||
activeComponents: cmpsList,
|
|
||||||
componentDefinitions: cmpDefs,
|
|
||||||
runtime: runtime,
|
|
||||||
fnArgs: fnArgs
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.innerHTML = ""; // clear children
|
document.body.innerHTML = ""; // clear children
|
||||||
document.body.appendChild(view.hostElement);
|
document.body.appendChild(viewHost.getView());
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ namespace pxsim {
|
|||||||
groundPins: string[],
|
groundPins: string[],
|
||||||
threeVoltPins: string[],
|
threeVoltPins: string[],
|
||||||
attachPowerOnRight?: boolean,
|
attachPowerOnRight?: boolean,
|
||||||
|
onboardComponents?: string[]
|
||||||
}
|
}
|
||||||
export interface FactoryFunctionPinAlloc {
|
export interface FactoryFunctionPinAlloc {
|
||||||
type: "factoryfunction",
|
type: "factoryfunction",
|
||||||
@ -92,6 +93,7 @@ namespace pxsim {
|
|||||||
groundPins: ["GND"],
|
groundPins: ["GND"],
|
||||||
threeVoltPins: ["+3v3"],
|
threeVoltPins: ["+3v3"],
|
||||||
attachPowerOnRight: true,
|
attachPowerOnRight: true,
|
||||||
|
onboardComponents: ["buttonpair", "ledmatrix"],
|
||||||
}
|
}
|
||||||
export const RASPBERRYPI_MODELB: BoardDefinition = {
|
export const RASPBERRYPI_MODELB: BoardDefinition = {
|
||||||
visual: {
|
visual: {
|
||||||
|
133
sim/microbit.ts
133
sim/microbit.ts
@ -102,8 +102,7 @@ namespace pxsim.visuals {
|
|||||||
leave: "mouseleave"
|
leave: "mouseleave"
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MicrobitBoardSvg {
|
export class MicrobitBoardSvg implements BoardView {
|
||||||
public hostElement: SVGSVGElement;
|
|
||||||
public element: SVGSVGElement;
|
public element: SVGSVGElement;
|
||||||
private style: SVGStyleElement;
|
private style: SVGStyleElement;
|
||||||
private defs: SVGDefsElement;
|
private defs: SVGDefsElement;
|
||||||
@ -132,131 +131,38 @@ namespace pxsim.visuals {
|
|||||||
private shakeButton: SVGCircleElement;
|
private shakeButton: SVGCircleElement;
|
||||||
private shakeText: SVGTextElement;
|
private shakeText: SVGTextElement;
|
||||||
public board: pxsim.DalBoard;
|
public board: pxsim.DalBoard;
|
||||||
|
|
||||||
//EXPERIMENTAl
|
|
||||||
private wireFactory: WireFactory;
|
|
||||||
private breadboard: Breadboard;
|
|
||||||
private components: IBoardComponent<any>[] = [];
|
|
||||||
private pinNmToCoord: Map<Coord> = {};
|
private pinNmToCoord: Map<Coord> = {};
|
||||||
private fromBBCoord: (xy: Coord) => Coord;
|
|
||||||
private fromMBCoord: (xy: Coord) => Coord;
|
|
||||||
|
|
||||||
constructor(public props: IBoardProps) {
|
constructor(public props: IBoardProps) {
|
||||||
this.board = this.props.runtime.board as pxsim.DalBoard;
|
this.board = this.props.runtime.board as pxsim.DalBoard;
|
||||||
this.board.updateView = () => this.updateState();
|
this.board.updateSubscribers.push(() => this.updateState());
|
||||||
|
|
||||||
//EXPERIMENTAl
|
|
||||||
let boardDef = MICROBIT_DEF;
|
|
||||||
let cmpsDef: Map<ComponentDefinition> = COMPONENT_DEFINITIONS;
|
|
||||||
this.breadboard = new Breadboard();
|
|
||||||
this.buildDom();
|
|
||||||
this.hostElement = this.element;
|
|
||||||
this.recordPinCoords();
|
this.recordPinCoords();
|
||||||
let cmps = props.activeComponents.filter(a => a === "neopixel");
|
this.buildDom();
|
||||||
if (0 < cmps.length) {
|
|
||||||
let compRes = composeSVG({
|
|
||||||
el1: {el: this.element, y: 0, x: 0, w: MB_WIDTH, h: MB_HEIGHT},
|
|
||||||
scaleUnit1: littlePinDist * 1.7,
|
|
||||||
el2: this.breadboard.getSVGAndSize(),
|
|
||||||
scaleUnit2: this.breadboard.getPinDist(),
|
|
||||||
margin: [0, 0, 10, 0],
|
|
||||||
middleMargin: 80,
|
|
||||||
maxWidth: 299,
|
|
||||||
maxHeight: 433,
|
|
||||||
});
|
|
||||||
let under = compRes.under;
|
|
||||||
let over = compRes.over;
|
|
||||||
this.hostElement = compRes.host;
|
|
||||||
let edges = compRes.edges;
|
|
||||||
this.fromMBCoord = compRes.toHostCoord1;
|
|
||||||
this.fromBBCoord = compRes.toHostCoord2;
|
|
||||||
let pinDist = compRes.scaleUnit;
|
|
||||||
|
|
||||||
this.wireFactory = new WireFactory(under, over, edges, this.style, this.getLocCoord.bind(this));
|
|
||||||
let allocRes = allocateDefinitions({
|
|
||||||
boardDef: boardDef,
|
|
||||||
cmpDefs: cmpsDef,
|
|
||||||
fnArgs: this.props.fnArgs,
|
|
||||||
getBBCoord: this.getBBCoord.bind(this),
|
|
||||||
cmpList: cmps,
|
|
||||||
});
|
|
||||||
this.addAll(allocRes);
|
|
||||||
} else {
|
|
||||||
svg.hydrate(this.hostElement, {
|
|
||||||
width: 299,
|
|
||||||
height: 433,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTheme();
|
this.updateTheme();
|
||||||
this.updateState();
|
this.updateState();
|
||||||
this.attachEvents();
|
this.attachEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
//EXPERIMENTAl
|
public getView(): SVGAndSize<SVGSVGElement> {
|
||||||
private getBoardPinCoord(pinNm: string): Coord {
|
return {
|
||||||
let coord = this.pinNmToCoord[pinNm];
|
el: this.element,
|
||||||
return this.fromMBCoord(coord);
|
y: 0,
|
||||||
|
x: 0,
|
||||||
|
w: MB_WIDTH,
|
||||||
|
h: MB_HEIGHT
|
||||||
|
};
|
||||||
}
|
}
|
||||||
private getBBCoord(rowCol: BBRowCol): Coord {
|
|
||||||
let bbCoord = this.breadboard.getCoord(rowCol);
|
public getCoord(pinNm: string): Coord {
|
||||||
if (!bbCoord)
|
return this.pinNmToCoord[pinNm];
|
||||||
return null;
|
|
||||||
return this.fromBBCoord(bbCoord);
|
|
||||||
}
|
}
|
||||||
public getLocCoord(loc: Loc): Coord {
|
|
||||||
let coord: Coord;
|
public getPinDist(): number {
|
||||||
if (loc.type === "breadboard") {
|
return littlePinDist * 1.7;
|
||||||
let rowCol = (<BBLoc>loc).rowCol;
|
|
||||||
coord = this.getBBCoord(rowCol);
|
|
||||||
} else {
|
|
||||||
let pinNm = (<BoardLoc>loc).pin;
|
|
||||||
coord = this.getBoardPinCoord(pinNm);
|
|
||||||
}
|
|
||||||
if (!coord) {
|
|
||||||
console.error("Unknown location: " + name)
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
return coord;
|
|
||||||
}
|
|
||||||
public addWire(inst: WireInst): Wire {
|
|
||||||
return this.wireFactory.addWire(inst.start, inst.end, inst.color, true);
|
|
||||||
}
|
|
||||||
public addAll(basicWiresAndCmpsAndWires: AllocatorResult) {
|
|
||||||
let {powerWires, components} = basicWiresAndCmpsAndWires;
|
|
||||||
powerWires.forEach(w => this.addWire(w));
|
|
||||||
components.forEach((cAndWs, idx) => {
|
|
||||||
let {component, wires} = cAndWs;
|
|
||||||
wires.forEach(w => this.addWire(w));
|
|
||||||
this.addComponent(component);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
public addComponent(cmpDesc: CmpInst): IBoardComponent<any> {
|
|
||||||
let cmp: IBoardComponent<any> = null;
|
|
||||||
if (typeof cmpDesc.visual === "string") {
|
|
||||||
let builtinVisual = cmpDesc.visual as string;
|
|
||||||
let cnstr = builtinComponentSimVisual[builtinVisual];
|
|
||||||
let stateFn = builtinComponentSimState[builtinVisual];
|
|
||||||
let cmp = cnstr();
|
|
||||||
cmp.init(this.board.bus, stateFn(this.board), this.element, cmpDesc.microbitPins, cmpDesc.otherArgs);
|
|
||||||
this.components.push(cmp);
|
|
||||||
this.hostElement.appendChild(cmp.element);
|
|
||||||
if (cmp.defs)
|
|
||||||
cmp.defs.forEach(d => this.defs.appendChild(d));
|
|
||||||
this.style.textContent += cmp.style || "";
|
|
||||||
let rowCol = <BBRowCol>[`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`];
|
|
||||||
let coord = this.getBBCoord(rowCol);
|
|
||||||
cmp.moveToCoord(coord);
|
|
||||||
let getCmpClass = (type: string) => `sim-${type}-cmp`;
|
|
||||||
let cls = getCmpClass(name);
|
|
||||||
svg.addClass(cmp.element, cls);
|
|
||||||
svg.addClass(cmp.element, "sim-cmp");
|
|
||||||
cmp.updateTheme();
|
|
||||||
cmp.updateState();
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
return cmp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public recordPinCoords() {
|
public recordPinCoords() {
|
||||||
const pinsY = 356.7 + 40;
|
const pinsY = 356.7 + 40;
|
||||||
pinNames.forEach((nm, i) => {
|
pinNames.forEach((nm, i) => {
|
||||||
@ -311,9 +217,6 @@ namespace pxsim.visuals {
|
|||||||
|
|
||||||
if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale");
|
if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale");
|
||||||
else svg.removeClass(this.element, "grayscale");
|
else svg.removeClass(this.element, "grayscale");
|
||||||
|
|
||||||
//EXPERIMENTAl
|
|
||||||
this.components.forEach(c => c.updateState());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateGestures() {
|
private updateGestures() {
|
||||||
|
@ -202,4 +202,10 @@ namespace pxsim.visuals {
|
|||||||
export type SVGElAndSize = SVGAndSize<SVGElement>;
|
export type SVGElAndSize = SVGAndSize<SVGElement>;
|
||||||
|
|
||||||
export const PIN_DIST = 15;
|
export const PIN_DIST = 15;
|
||||||
|
|
||||||
|
export interface BoardView {
|
||||||
|
getView(): SVGAndSize<SVGSVGElement>;
|
||||||
|
getCoord(pinNm: string): Coord;
|
||||||
|
getPinDist(): number;
|
||||||
|
}
|
||||||
}
|
}
|
160
sim/visuals/boardhost.ts
Normal file
160
sim/visuals/boardhost.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
namespace pxsim.visuals {
|
||||||
|
export class BoardHost {
|
||||||
|
private components: IBoardComponent<any>[] = [];
|
||||||
|
private wireFactory: WireFactory;
|
||||||
|
private breadboard: Breadboard;
|
||||||
|
private fromBBCoord: (xy: Coord) => Coord;
|
||||||
|
private fromMBCoord: (xy: Coord) => Coord;
|
||||||
|
private boardView: BoardView;
|
||||||
|
private view: SVGSVGElement;
|
||||||
|
private style: SVGStyleElement;
|
||||||
|
private defs: SVGDefsElement;
|
||||||
|
private state: DalBoard;
|
||||||
|
|
||||||
|
constructor(state: DalBoard, boardDef: BoardDefinition, cmpsList: string[], cmpDefs: Map<ComponentDefinition>, fnArgs: any) {
|
||||||
|
this.state = state;
|
||||||
|
let onboardCmps = boardDef.onboardComponents || [];
|
||||||
|
let activeComponents = cmpsList.filter(c => onboardCmps.indexOf(c) < 0);
|
||||||
|
activeComponents.sort();
|
||||||
|
|
||||||
|
if (boardDef.visual === "microbit") {
|
||||||
|
this.boardView = new visuals.MicrobitBoardSvg({
|
||||||
|
runtime: runtime,
|
||||||
|
theme: visuals.randomTheme(),
|
||||||
|
activeComponents: activeComponents,
|
||||||
|
fnArgs: fnArgs,
|
||||||
|
disableTilt: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//TODO: port Arduino/generic board
|
||||||
|
// this.boardView = new visuals.GenericBoardSvg({
|
||||||
|
// boardDef: boardDef,
|
||||||
|
// activeComponents: activeComponents,
|
||||||
|
// componentDefinitions: cmpDefs,
|
||||||
|
// runtime: runtime,
|
||||||
|
// fnArgs: fnArgs
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
const VIEW_WIDTH = 299;
|
||||||
|
const VIEW_HEIGHT = 433;
|
||||||
|
|
||||||
|
if (0 < activeComponents.length) {
|
||||||
|
this.breadboard = new Breadboard();
|
||||||
|
let composition = composeSVG({
|
||||||
|
el1: this.boardView.getView(),
|
||||||
|
scaleUnit1: this.boardView.getPinDist(),
|
||||||
|
el2: this.breadboard.getSVGAndSize(),
|
||||||
|
scaleUnit2: this.breadboard.getPinDist(),
|
||||||
|
margin: [0, 0, 10, 0],
|
||||||
|
middleMargin: 80,
|
||||||
|
maxWidth: VIEW_WIDTH,
|
||||||
|
maxHeight: VIEW_HEIGHT,
|
||||||
|
});
|
||||||
|
let under = composition.under;
|
||||||
|
let over = composition.over;
|
||||||
|
this.view = composition.host;
|
||||||
|
let edges = composition.edges;
|
||||||
|
this.fromMBCoord = composition.toHostCoord1;
|
||||||
|
this.fromBBCoord = composition.toHostCoord2;
|
||||||
|
let pinDist = composition.scaleUnit;
|
||||||
|
|
||||||
|
this.style = <SVGStyleElement>svg.child(this.view, "style", {});
|
||||||
|
this.defs = <SVGDefsElement>svg.child(this.view, "defs", {});
|
||||||
|
|
||||||
|
this.wireFactory = new WireFactory(under, over, edges, this.style, this.getLocCoord.bind(this));
|
||||||
|
|
||||||
|
let allocRes = allocateDefinitions({
|
||||||
|
boardDef: boardDef,
|
||||||
|
cmpDefs: cmpDefs,
|
||||||
|
fnArgs: fnArgs,
|
||||||
|
getBBCoord: this.breadboard.getCoord.bind(this.breadboard),
|
||||||
|
cmpList: activeComponents,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addAll(allocRes);
|
||||||
|
} else {
|
||||||
|
let el = this.boardView.getView().el;
|
||||||
|
this.view = el;
|
||||||
|
svg.hydrate(this.view, {
|
||||||
|
width: VIEW_WIDTH,
|
||||||
|
height: VIEW_HEIGHT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.updateSubscribers.push(() => this.updateState());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getView(): SVGElement {
|
||||||
|
return this.view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateState() {
|
||||||
|
this.components.forEach(c => c.updateState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBBCoord(rowCol: BBRowCol) {
|
||||||
|
let bbCoord = this.breadboard.getCoord(rowCol);
|
||||||
|
return this.fromBBCoord(bbCoord);
|
||||||
|
}
|
||||||
|
private getPinCoord(pin: string) {
|
||||||
|
let boardCoord = this.boardView.getCoord(pin);
|
||||||
|
return this.fromMBCoord(boardCoord);
|
||||||
|
}
|
||||||
|
public getLocCoord(loc: Loc): Coord {
|
||||||
|
let coord: Coord;
|
||||||
|
if (loc.type === "breadboard") {
|
||||||
|
let rowCol = (<BBLoc>loc).rowCol;
|
||||||
|
coord = this.getBBCoord(rowCol);
|
||||||
|
} else {
|
||||||
|
let pinNm = (<BoardLoc>loc).pin;
|
||||||
|
coord = this.getPinCoord(pinNm);
|
||||||
|
}
|
||||||
|
if (!coord) {
|
||||||
|
console.error("Unknown location: " + name)
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
return coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addComponent(cmpDesc: CmpInst): IBoardComponent<any> {
|
||||||
|
let cmp: IBoardComponent<any> = null;
|
||||||
|
if (typeof cmpDesc.visual === "string") {
|
||||||
|
let builtinVisual = cmpDesc.visual as string;
|
||||||
|
let cnstr = builtinComponentSimVisual[builtinVisual];
|
||||||
|
let stateFn = builtinComponentSimState[builtinVisual];
|
||||||
|
let cmp = cnstr();
|
||||||
|
cmp.init(this.state.bus, stateFn(this.state), this.view, cmpDesc.microbitPins, cmpDesc.otherArgs);
|
||||||
|
this.components.push(cmp);
|
||||||
|
this.view.appendChild(cmp.element);
|
||||||
|
if (cmp.defs)
|
||||||
|
cmp.defs.forEach(d => this.defs.appendChild(d));
|
||||||
|
this.style.textContent += cmp.style || "";
|
||||||
|
let rowCol = <BBRowCol>[`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`];
|
||||||
|
let coord = this.getBBCoord(rowCol);
|
||||||
|
cmp.moveToCoord(coord);
|
||||||
|
let getCmpClass = (type: string) => `sim-${type}-cmp`;
|
||||||
|
let cls = getCmpClass(name);
|
||||||
|
svg.addClass(cmp.element, cls);
|
||||||
|
svg.addClass(cmp.element, "sim-cmp");
|
||||||
|
cmp.updateTheme();
|
||||||
|
cmp.updateState();
|
||||||
|
} else {
|
||||||
|
//TODO: support generic parts
|
||||||
|
}
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
public addWire(inst: WireInst): Wire {
|
||||||
|
return this.wireFactory.addWire(inst.start, inst.end, inst.color, true);
|
||||||
|
}
|
||||||
|
public addAll(basicWiresAndCmpsAndWires: AllocatorResult) {
|
||||||
|
let {powerWires, components} = basicWiresAndCmpsAndWires;
|
||||||
|
powerWires.forEach(w => this.addWire(w));
|
||||||
|
components.forEach((cAndWs, idx) => {
|
||||||
|
let {component, wires} = cAndWs;
|
||||||
|
wires.forEach(w => this.addWire(w));
|
||||||
|
this.addComponent(component);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -180,7 +180,7 @@ namespace pxsim.visuals {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
let nextBoardId = 0;
|
let nextBoardId = 0;
|
||||||
export class GenericBoardSvg {
|
export class GenericBoardSvg /*TODO: implements BoardView*/ {
|
||||||
public hostElement: SVGSVGElement;
|
public hostElement: SVGSVGElement;
|
||||||
private style: SVGStyleElement;
|
private style: SVGStyleElement;
|
||||||
private defs: SVGDefsElement;
|
private defs: SVGDefsElement;
|
||||||
|
Loading…
Reference in New Issue
Block a user