mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:20:43 +00:00
refactor: move selection rendering to dedicated UILayer
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { EventBus, GameEvent } from "../core/EventBus";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { Game } from "../core/game/Game";
|
||||
import { UnitView } from "../core/game/GameView";
|
||||
|
||||
export class MouseUpEvent implements GameEvent {
|
||||
constructor(
|
||||
@@ -8,6 +10,16 @@ export class MouseUpEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitted when a unit is selected or deselected
|
||||
*/
|
||||
export class UnitSelectionEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly unit: UnitView | null,
|
||||
public readonly isSelected: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class MouseDownEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ControlPanel } from "./layers/ControlPanel";
|
||||
import { UIState } from "./UIState";
|
||||
import { BuildMenu } from "./layers/BuildMenu";
|
||||
import { UnitLayer } from "./layers/UnitLayer";
|
||||
import { UILayer } from "./layers/UILayer";
|
||||
import { StructureLayer } from "./layers/StructureLayer";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { consolex } from "../../core/Consolex";
|
||||
@@ -118,6 +119,7 @@ export function createRenderer(
|
||||
new TerritoryLayer(game, eventBus),
|
||||
new StructureLayer(game, eventBus),
|
||||
new UnitLayer(game, eventBus, clientID, transformHandler),
|
||||
new UILayer(game, eventBus, clientID, transformHandler),
|
||||
new NameLayer(game, transformHandler, clientID),
|
||||
eventsDisplay,
|
||||
buildMenu,
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { EventBus, GameEvent } from "../../core/EventBus";
|
||||
import { UnitType } from "../../core/game/Game";
|
||||
import { UnitView } from "../../core/game/GameView";
|
||||
|
||||
/**
|
||||
* Event emitted when a unit is selected or deselected
|
||||
*/
|
||||
export class UnitSelectionEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly unit: UnitView | null,
|
||||
public readonly isSelected: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the currently selected units in the game
|
||||
*/
|
||||
export class SelectedUnits {
|
||||
private selectedUnit: UnitView | null = null;
|
||||
|
||||
constructor(private eventBus: EventBus) {}
|
||||
|
||||
/**
|
||||
* Select a unit. Deselects any previously selected unit.
|
||||
* @param unit The unit to select
|
||||
* @returns true if the selection changed, false otherwise
|
||||
*/
|
||||
selectUnit(unit: UnitView): boolean {
|
||||
if (this.selectedUnit === unit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.selectedUnit) {
|
||||
this.deselectCurrentUnit();
|
||||
}
|
||||
|
||||
this.selectedUnit = unit;
|
||||
this.eventBus.emit(new UnitSelectionEvent(unit, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect the currently selected unit, if any
|
||||
* @returns true if a unit was deselected, false otherwise
|
||||
*/
|
||||
deselectCurrentUnit(): boolean {
|
||||
if (!this.selectedUnit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unit = this.selectedUnit;
|
||||
this.selectedUnit = null;
|
||||
this.eventBus.emit(new UnitSelectionEvent(unit, false));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle selection for the given unit
|
||||
* @param unit The unit to toggle selection for
|
||||
* @returns true if the unit is now selected, false if it was deselected
|
||||
*/
|
||||
toggleUnitSelection(unit: UnitView): boolean {
|
||||
if (this.selectedUnit === unit) {
|
||||
this.deselectCurrentUnit();
|
||||
return false;
|
||||
} else {
|
||||
this.selectUnit(unit);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected unit, if any
|
||||
*/
|
||||
getSelectedUnit(): UnitView | null {
|
||||
return this.selectedUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given unit is currently selected
|
||||
* @param unit The unit to check
|
||||
*/
|
||||
isSelected(unit: UnitView): boolean {
|
||||
return this.selectedUnit === unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a unit of the specified type is currently selected
|
||||
* @param type The unit type to check for
|
||||
*/
|
||||
hasSelectedUnitOfType(type: UnitType): boolean {
|
||||
return this.selectedUnit !== null && this.selectedUnit.type() === type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Colord } from "colord";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { Layer } from "./Layer";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { UnitSelectionEvent } from "../../InputHandler";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
|
||||
/**
|
||||
* Layer responsible for drawing UI elements that overlay the game
|
||||
* such as selection boxes, health bars, etc.
|
||||
*/
|
||||
export class UILayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
|
||||
private theme: Theme = null;
|
||||
private selectionAnimTime = 0;
|
||||
|
||||
// Keep track of currently selected unit
|
||||
private selectedUnit: UnitView | null = null;
|
||||
|
||||
// Keep track of previous selection box position for cleanup
|
||||
private lastSelectionBoxCenter: {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
} | null = null;
|
||||
|
||||
// Visual settings for selection
|
||||
private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship)
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private clientID: ClientID,
|
||||
private transformHandler: TransformHandler,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
// Update the selection animation time
|
||||
this.selectionAnimTime = (this.selectionAnimTime + 1) % 60;
|
||||
|
||||
// If there's a selected warship, redraw to update the selection box animation
|
||||
if (this.selectedUnit && this.selectedUnit.type() === UnitType.Warship) {
|
||||
this.drawSelectionBox(this.selectedUnit);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelection(e));
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
|
||||
redraw() {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the unit selection event
|
||||
*/
|
||||
private onUnitSelection(event: UnitSelectionEvent) {
|
||||
if (event.isSelected) {
|
||||
this.selectedUnit = event.unit;
|
||||
if (event.unit && event.unit.type() === UnitType.Warship) {
|
||||
this.drawSelectionBox(event.unit);
|
||||
}
|
||||
} else {
|
||||
if (this.selectedUnit === event.unit) {
|
||||
// Clear the selection box
|
||||
if (this.lastSelectionBoxCenter) {
|
||||
const { x, y, size } = this.lastSelectionBoxCenter;
|
||||
this.clearSelectionBox(x, y, size);
|
||||
this.lastSelectionBoxCenter = null;
|
||||
}
|
||||
this.selectedUnit = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selection box at a specific position
|
||||
*/
|
||||
private clearSelectionBox(x: number, y: number, size: number) {
|
||||
for (let px = x - size; px <= x + size; px++) {
|
||||
for (let py = y - size; py <= y + size; py++) {
|
||||
if (
|
||||
px === x - size ||
|
||||
px === x + size ||
|
||||
py === y - size ||
|
||||
py === y + size
|
||||
) {
|
||||
this.clearCell(px, py);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a selection box around the given unit
|
||||
*/
|
||||
public drawSelectionBox(unit: UnitView) {
|
||||
if (!unit || !unit.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the configured selection box size
|
||||
const selectionSize = this.SELECTION_BOX_SIZE;
|
||||
|
||||
// Calculate pulsating effect based on animation time (25% variation in opacity)
|
||||
const baseOpacity = 200;
|
||||
const pulseAmount = 55;
|
||||
const opacity =
|
||||
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
|
||||
|
||||
// Get the unit's owner color for the box
|
||||
const ownerColor = this.theme.territoryColor(unit.owner().info());
|
||||
|
||||
// Create a brighter version of the owner color for the selection
|
||||
const selectionColor = ownerColor.lighten(0.2);
|
||||
|
||||
// Get current center position
|
||||
const center = unit.tile();
|
||||
const centerX = this.game.x(center);
|
||||
const centerY = this.game.y(center);
|
||||
|
||||
// Clear previous selection box if it exists and is different from current position
|
||||
if (
|
||||
this.lastSelectionBoxCenter &&
|
||||
(this.lastSelectionBoxCenter.x !== centerX ||
|
||||
this.lastSelectionBoxCenter.y !== centerY)
|
||||
) {
|
||||
const lastSize = this.lastSelectionBoxCenter.size;
|
||||
const lastX = this.lastSelectionBoxCenter.x;
|
||||
const lastY = this.lastSelectionBoxCenter.y;
|
||||
|
||||
// Clear the previous selection box
|
||||
this.clearSelectionBox(lastX, lastY, lastSize);
|
||||
}
|
||||
|
||||
// Draw the selection box
|
||||
for (let x = centerX - selectionSize; x <= centerX + selectionSize; x++) {
|
||||
for (let y = centerY - selectionSize; y <= centerY + selectionSize; y++) {
|
||||
// Only draw if it's on the border (not inside or outside the box)
|
||||
if (
|
||||
x === centerX - selectionSize ||
|
||||
x === centerX + selectionSize ||
|
||||
y === centerY - selectionSize ||
|
||||
y === centerY + selectionSize
|
||||
) {
|
||||
// Create a dashed effect by only drawing some pixels
|
||||
const dashPattern = (x + y) % 2 === 0;
|
||||
if (dashPattern) {
|
||||
this.paintCell(x, y, selectionColor, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store current selection box position for next cleanup
|
||||
this.lastSelectionBoxCenter = {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
size: selectionSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw health bar for a unit (placeholder for future implementation)
|
||||
*/
|
||||
public drawHealthBar(unit: UnitView) {
|
||||
// This is a placeholder for future health bar implementation
|
||||
// It would draw a health bar above units that have health
|
||||
}
|
||||
|
||||
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
||||
this.clearCell(x, y);
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
clearCell(x: number, y: number) {
|
||||
this.context.clearRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import { Theme } from "../../../core/configuration/Config";
|
||||
import { Unit, UnitType, Player } from "../../../core/game/Game";
|
||||
import { Layer } from "./Layer";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { AlternateViewEvent, MouseUpEvent } from "../../InputHandler";
|
||||
import {
|
||||
AlternateViewEvent,
|
||||
MouseUpEvent,
|
||||
UnitSelectionEvent,
|
||||
} from "../../InputHandler";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import {
|
||||
@@ -12,7 +16,6 @@ import {
|
||||
TileRef,
|
||||
} from "../../../core/game/GameMap";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { SelectedUnits, UnitSelectionEvent } from "../SelectedUnits";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
|
||||
enum Relationship {
|
||||
@@ -35,10 +38,11 @@ export class UnitLayer implements Layer {
|
||||
|
||||
private oldShellTile = new Map<UnitView, TileRef>();
|
||||
|
||||
private selectedUnits: SelectedUnits;
|
||||
private selectionAnimTime = 0;
|
||||
private transformHandler: TransformHandler;
|
||||
|
||||
// Selected unit property as suggested in the review comment
|
||||
private selectedUnit: UnitView | null = null;
|
||||
|
||||
// Configuration for unit selection
|
||||
private readonly WARSHIP_SELECTION_RADIUS = 3; // Radius in game cells for warship selection hit zone
|
||||
|
||||
@@ -49,7 +53,6 @@ export class UnitLayer implements Layer {
|
||||
transformHandler: TransformHandler,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
this.selectedUnits = new SelectedUnits(eventBus);
|
||||
this.transformHandler = transformHandler;
|
||||
}
|
||||
|
||||
@@ -64,20 +67,12 @@ export class UnitLayer implements Layer {
|
||||
this.game.updatesSinceLastTick()?.[GameUpdateType.Unit]?.forEach((unit) => {
|
||||
this.onUnitEvent(this.game.unit(unit.id));
|
||||
});
|
||||
|
||||
// Update the selection animation time
|
||||
this.selectionAnimTime = (this.selectionAnimTime + 1) % 60;
|
||||
|
||||
// If there's a selected warship, redraw to update the selection box animation
|
||||
if (this.selectedUnits.hasSelectedUnitOfType(UnitType.Warship)) {
|
||||
this.redrawSelectedUnit(this.selectedUnits.getSelectedUnit());
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(AlternateViewEvent, (e) => this.onAlternativeViewEvent(e));
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e));
|
||||
this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelection(e));
|
||||
this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e));
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
@@ -116,48 +111,29 @@ export class UnitLayer implements Layer {
|
||||
const nearbyWarships = this.findWarshipsNearCell(cell);
|
||||
|
||||
if (nearbyWarships.length > 0) {
|
||||
// Select/deselect the closest warship
|
||||
this.selectedUnits.toggleUnitSelection(nearbyWarships[0]);
|
||||
} else if (this.selectedUnits.getSelectedUnit()) {
|
||||
// Toggle selection of the closest warship
|
||||
const clickedUnit = nearbyWarships[0];
|
||||
if (this.selectedUnit === clickedUnit) {
|
||||
// Deselect if already selected
|
||||
this.eventBus.emit(new UnitSelectionEvent(clickedUnit, false));
|
||||
} else {
|
||||
// Select the new unit
|
||||
this.eventBus.emit(new UnitSelectionEvent(clickedUnit, true));
|
||||
}
|
||||
} else if (this.selectedUnit) {
|
||||
// If clicked elsewhere and there's a selection, deselect it
|
||||
this.selectedUnits.deselectCurrentUnit();
|
||||
this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selection box at a specific position
|
||||
* Handle unit selection changes
|
||||
*/
|
||||
private clearSelectionBox(x: number, y: number, size: number) {
|
||||
for (let px = x - size; px <= x + size; px++) {
|
||||
for (let py = y - size; py <= y + size; py++) {
|
||||
if (
|
||||
px === x - size ||
|
||||
px === x + size ||
|
||||
py === y - size ||
|
||||
py === y + size
|
||||
) {
|
||||
this.clearCell(px, py);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onUnitSelection(event: UnitSelectionEvent) {
|
||||
if (event.unit && event.unit.type() === UnitType.Warship) {
|
||||
if (event.isSelected) {
|
||||
// Highlight the selected warship
|
||||
this.redrawSelectedUnit(event.unit);
|
||||
} else {
|
||||
// Remove the highlight
|
||||
this.onUnitEvent(event.unit);
|
||||
|
||||
// Also clear any lingering selection box
|
||||
if (this.lastSelectionBoxCenter) {
|
||||
const { x, y, size } = this.lastSelectionBoxCenter;
|
||||
this.clearSelectionBox(x, y, size);
|
||||
this.lastSelectionBoxCenter = null;
|
||||
}
|
||||
}
|
||||
private onUnitSelectionChange(event: UnitSelectionEvent) {
|
||||
if (event.isSelected) {
|
||||
this.selectedUnit = event.unit;
|
||||
} else if (this.selectedUnit === event.unit) {
|
||||
this.selectedUnit = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,24 +142,8 @@ export class UnitLayer implements Layer {
|
||||
* If the selected unit is removed from the game, deselect it
|
||||
*/
|
||||
private handleUnitDeactivation(unit: UnitView) {
|
||||
if (this.selectedUnits.isSelected(unit) && !unit.isActive()) {
|
||||
// Clear the selection box before deselecting
|
||||
if (
|
||||
this.lastSelectionBoxCenter &&
|
||||
this.lastSelectionBoxCenter.unit === unit
|
||||
) {
|
||||
const { x, y, size } = this.lastSelectionBoxCenter;
|
||||
this.clearSelectionBox(x, y, size);
|
||||
this.lastSelectionBoxCenter = null;
|
||||
}
|
||||
|
||||
this.selectedUnits.deselectCurrentUnit();
|
||||
}
|
||||
}
|
||||
|
||||
private redrawSelectedUnit(unit: UnitView) {
|
||||
if (unit && unit.type() === UnitType.Warship && unit.isActive()) {
|
||||
this.onUnitEvent(unit);
|
||||
if (this.selectedUnit === unit && !unit.isActive()) {
|
||||
this.eventBus.emit(new UnitSelectionEvent(unit, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,98 +268,6 @@ export class UnitLayer implements Layer {
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
// If this is a selected warship, draw the selection box
|
||||
if (this.selectedUnits.isSelected(unit)) {
|
||||
this.drawSelectionBox(unit);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of previous selection box positions for cleanup
|
||||
private lastSelectionBoxCenter: {
|
||||
unit: UnitView;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
} | null = null;
|
||||
|
||||
// Visual settings for selection
|
||||
private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship)
|
||||
|
||||
/**
|
||||
* Draw a selection box around the warship
|
||||
*/
|
||||
private drawSelectionBox(unit: UnitView) {
|
||||
// Use the configured selection box size
|
||||
const selectionSize = this.SELECTION_BOX_SIZE;
|
||||
|
||||
// Calculate pulsating effect based on animation time (25% variation in opacity)
|
||||
const baseOpacity = 200;
|
||||
const pulseAmount = 55;
|
||||
const opacity =
|
||||
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
|
||||
|
||||
// Get the warship's owner color for the box
|
||||
const ownerColor = this.theme.territoryColor(unit.owner().info());
|
||||
|
||||
// Create a brighter version of the owner color for the selection
|
||||
const selectionColor = ownerColor.lighten(0.2);
|
||||
|
||||
// Get current center position
|
||||
const center = unit.tile();
|
||||
const centerX = this.game.x(center);
|
||||
const centerY = this.game.y(center);
|
||||
|
||||
// Clear previous selection box if it exists and is different from current position
|
||||
if (
|
||||
this.lastSelectionBoxCenter &&
|
||||
(this.lastSelectionBoxCenter.x !== centerX ||
|
||||
this.lastSelectionBoxCenter.y !== centerY ||
|
||||
this.lastSelectionBoxCenter.unit !== unit)
|
||||
) {
|
||||
const lastSize = this.lastSelectionBoxCenter.size;
|
||||
const lastX = this.lastSelectionBoxCenter.x;
|
||||
const lastY = this.lastSelectionBoxCenter.y;
|
||||
|
||||
// Clear the previous selection box
|
||||
this.clearSelectionBox(lastX, lastY, lastSize);
|
||||
|
||||
// We don't need to redraw the territory since the unit layer sits on top of the territory layer
|
||||
// and clearing just the selection box pixels won't affect the territory underneath
|
||||
}
|
||||
|
||||
// Draw the selection box
|
||||
for (let x = centerX - selectionSize; x <= centerX + selectionSize; x++) {
|
||||
for (let y = centerY - selectionSize; y <= centerY + selectionSize; y++) {
|
||||
// Only draw if it's on the border (not inside or outside the box)
|
||||
if (
|
||||
x === centerX - selectionSize ||
|
||||
x === centerX + selectionSize ||
|
||||
y === centerY - selectionSize ||
|
||||
y === centerY + selectionSize
|
||||
) {
|
||||
// Create a dashed effect by only drawing some pixels
|
||||
const dashPattern = (x + y) % 2 === 0;
|
||||
if (dashPattern) {
|
||||
this.paintCell(
|
||||
x,
|
||||
y,
|
||||
this.relationship(unit),
|
||||
selectionColor,
|
||||
opacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store current selection box position for next cleanup
|
||||
this.lastSelectionBoxCenter = {
|
||||
unit,
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
size: selectionSize,
|
||||
};
|
||||
}
|
||||
|
||||
private handleShellEvent(unit: UnitView) {
|
||||
|
||||
Reference in New Issue
Block a user