[client] add SettingsFrame
This commit is contained in:
@@ -20,10 +20,14 @@ views:
|
||||
$type: ru.m.tankz.frame.GameFrame
|
||||
- id: network
|
||||
$type: ru.m.tankz.frame.NetworkFrame
|
||||
- id: settings
|
||||
$type: ru.m.tankz.frame.SettingsFrame
|
||||
- $type: haxework.gui.LabelView
|
||||
$style: label
|
||||
inLayout: false
|
||||
contentSize: true
|
||||
vAlign: BOTTOM
|
||||
hAlign: RIGHT
|
||||
rightMargin: 10
|
||||
bottomMargin: 10
|
||||
text: "@res:text:version"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ru.m.tankz;
|
||||
|
||||
import ru.m.tankz.storage.SettingsStorage;
|
||||
import haxework.provider.Provider;
|
||||
import haxework.resources.IResources;
|
||||
import haxework.resources.Resources;
|
||||
@@ -47,6 +48,7 @@ class Init {
|
||||
Provider.setFactory(IConfigBundle, ConfigBundle);
|
||||
Provider.setFactory(SaveStorage, SaveStorage);
|
||||
Provider.setFactory(UserStorage, UserStorage);
|
||||
Provider.setFactory(SettingsStorage, SettingsStorage);
|
||||
Provider.setFactory(SoundManager, SoundManager);
|
||||
Provider.setFactory(NetworkManager, NetworkManager);
|
||||
Provider.setFactory(IControlFactory, ClientControlFactory);
|
||||
|
||||
@@ -19,3 +19,14 @@ label:
|
||||
fontFamily: Courirer New
|
||||
fontSize: 16
|
||||
shadowColor: 0x000000
|
||||
|
||||
close:
|
||||
inLayout: false
|
||||
hAlign: LEFT
|
||||
vAlign: BOTTOM
|
||||
leftMargin: 10
|
||||
bottomMargin: 10
|
||||
contentSize: true
|
||||
skin:
|
||||
$type: haxework.gui.skin.ButtonBitmapSkin
|
||||
image: "@asset:image:resources/image/ui/close.png"
|
||||
|
||||
86
src/client/haxe/ru/m/tankz/control/ActionConfig.hx
Normal file
86
src/client/haxe/ru/m/tankz/control/ActionConfig.hx
Normal file
@@ -0,0 +1,86 @@
|
||||
package ru.m.tankz.control;
|
||||
|
||||
import ru.m.geom.Direction;
|
||||
import ru.m.tankz.control.Control.TankAction;
|
||||
import yaml.Parser;
|
||||
import yaml.Renderer;
|
||||
import yaml.Yaml;
|
||||
|
||||
|
||||
typedef ActionItem = {
|
||||
public var action: TankAction;
|
||||
public var key: Int;
|
||||
}
|
||||
|
||||
typedef KeyBinding = Map<Int, TankAction>;
|
||||
|
||||
typedef ActionItemRaw = {
|
||||
public var action: String;
|
||||
public var key: Int;
|
||||
}
|
||||
|
||||
class ActionConfig {
|
||||
|
||||
public var data(default, null): Array<ActionItem>;
|
||||
|
||||
public function new(data: Array<ActionItem>) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public function asKeyBinding(): KeyBinding {
|
||||
var result = new Map<Int, TankAction>();
|
||||
for (item in data) {
|
||||
result[item.key] = item.action;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static function action2string(action: TankAction): String {
|
||||
return switch (action) {
|
||||
case TankAction.SHOT: "SHOT";
|
||||
case TankAction.MOVE(Direction.TOP): "MOVE_TOP";
|
||||
case TankAction.MOVE(Direction.LEFT): "MOVE_LEFT";
|
||||
case TankAction.MOVE(Direction.BOTTOM): "MOVE_BOTTOM";
|
||||
case TankAction.MOVE(Direction.RIGHT): "MOVE_RIGHT";
|
||||
case _: throw 'Unsupported action "${action}"';
|
||||
}
|
||||
}
|
||||
|
||||
public static function string2action(value: String): TankAction {
|
||||
return switch (value) {
|
||||
case "SHOT": TankAction.SHOT;
|
||||
case "MOVE_TOP": TankAction.MOVE(Direction.TOP);
|
||||
case "MOVE_LEFT": TankAction.MOVE(Direction.LEFT);
|
||||
case "MOVE_BOTTOM": TankAction.MOVE(Direction.BOTTOM);
|
||||
case "MOVE_RIGHT": TankAction.MOVE(Direction.RIGHT);
|
||||
case _: throw 'Unsupported value "${value}"';
|
||||
}
|
||||
}
|
||||
|
||||
public function clone(): ActionConfig {
|
||||
return loads(dumps());
|
||||
}
|
||||
|
||||
public function dumps(): String {
|
||||
var raw: Array<ActionItemRaw> = [];
|
||||
for (item in this.data) {
|
||||
raw.push({
|
||||
action: action2string(item.action),
|
||||
key: item.key,
|
||||
});
|
||||
}
|
||||
return Yaml.render(raw, Renderer.options().setFlowLevel(0));
|
||||
}
|
||||
|
||||
public static function loads(value: String): ActionConfig {
|
||||
var raw: Array<ActionItemRaw> = Yaml.parse(value, Parser.options().useObjects());
|
||||
var data: Array<ActionItem> = [];
|
||||
for (item in raw) {
|
||||
data.push({
|
||||
action: string2action(item.action),
|
||||
key: item.key,
|
||||
});
|
||||
}
|
||||
return new ActionConfig(data);
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,23 @@ import flash.events.KeyboardEvent;
|
||||
import flash.Lib;
|
||||
import flash.ui.Keyboard;
|
||||
import haxe.Timer;
|
||||
import ru.m.geom.Direction;
|
||||
import ru.m.tankz.control.ActionConfig;
|
||||
import ru.m.tankz.control.Control;
|
||||
import ru.m.tankz.storage.SettingsStorage;
|
||||
import ru.m.tankz.Type;
|
||||
|
||||
|
||||
typedef KeyBinding = Map<Int, TankAction>;
|
||||
|
||||
class HumanControl extends Control {
|
||||
|
||||
@:provide var settings: SettingsStorage;
|
||||
|
||||
private var keyBinding:KeyBinding;
|
||||
private var moveQueue:Array<Int>;
|
||||
private var shotTimer:Timer;
|
||||
|
||||
public function new(playerId:PlayerId, controlIndex:Int) {
|
||||
super(playerId);
|
||||
this.keyBinding = resolve(controlIndex);
|
||||
this.keyBinding = settings.read(controlIndex).asKeyBinding();
|
||||
moveQueue = new Array<Int>();
|
||||
Lib.current.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
|
||||
Lib.current.stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
|
||||
@@ -81,27 +82,4 @@ class HumanControl extends Control {
|
||||
private function shot():Void {
|
||||
action(TankAction.SHOT);
|
||||
}
|
||||
|
||||
private static function resolve(controlIndex:Int):KeyBinding {
|
||||
switch (controlIndex) {
|
||||
case 0:
|
||||
return [
|
||||
Keyboard.A => TankAction.MOVE(Direction.LEFT),
|
||||
Keyboard.S => TankAction.MOVE(Direction.BOTTOM),
|
||||
Keyboard.W => TankAction.MOVE(Direction.TOP),
|
||||
Keyboard.D => TankAction.MOVE(Direction.RIGHT),
|
||||
Keyboard.SPACE => TankAction.SHOT
|
||||
];
|
||||
case 1:
|
||||
return [
|
||||
Keyboard.LEFT => TankAction.MOVE(Direction.LEFT),
|
||||
Keyboard.DOWN => TankAction.MOVE(Direction.BOTTOM),
|
||||
Keyboard.UP => TankAction.MOVE(Direction.TOP),
|
||||
Keyboard.RIGHT => TankAction.MOVE(Direction.RIGHT),
|
||||
Keyboard.NUMPAD_0 => TankAction.SHOT
|
||||
];
|
||||
case x:
|
||||
throw 'Invalid control index ${x}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,19 +37,24 @@ class GameFrame extends VGroupView {
|
||||
start(Provider.get(GameSave));
|
||||
}
|
||||
|
||||
private function connectGame(game: Game) {
|
||||
game.engine.connect(render);
|
||||
game.engine.connect(Provider.get(SoundManager));
|
||||
}
|
||||
|
||||
private function start(save:GameSave):Void {
|
||||
switch (save.server) {
|
||||
case GameServer.LOCAL:
|
||||
game = new Game(save.state.type);
|
||||
connectGame(game);
|
||||
game.start(save).then(onGameStateChange).endThen(onGameComplete);
|
||||
timer = new Timer(10);
|
||||
timer.run = updateEngine;
|
||||
case GameServer.NETWORK:
|
||||
game = new NetworkGame(save.state.type);
|
||||
connectGame(game);
|
||||
network.game = cast game;
|
||||
}
|
||||
game.engine.connect(render);
|
||||
game.engine.connect(Provider.get(SoundManager));
|
||||
content.addEventListener(Event.ENTER_FRAME, redraw);
|
||||
render.draw(game.engine);
|
||||
state.text = stateString(game);
|
||||
|
||||
24
src/client/haxe/ru/m/tankz/frame/SettingsFrame.hx
Normal file
24
src/client/haxe/ru/m/tankz/frame/SettingsFrame.hx
Normal file
@@ -0,0 +1,24 @@
|
||||
package ru.m.tankz.frame;
|
||||
|
||||
import haxework.gui.frame.IFrameSwitcher;
|
||||
import haxework.gui.ButtonView;
|
||||
import haxework.gui.VGroupView;
|
||||
|
||||
|
||||
@:template("ru/m/tankz/frame/SettingsFrame.yaml", "ru/m/tankz/Style.yaml")
|
||||
class SettingsFrame extends VGroupView {
|
||||
|
||||
public static var ID(default, never):String = "settings";
|
||||
|
||||
@:provide var frameSwitcher:IFrameSwitcher;
|
||||
|
||||
@:view var close:ButtonView;
|
||||
|
||||
private function init():Void {
|
||||
close.onPress = this;
|
||||
}
|
||||
|
||||
public function onPress(_):Void {
|
||||
frameSwitcher.change(StartFrame.ID);
|
||||
}
|
||||
}
|
||||
24
src/client/haxe/ru/m/tankz/frame/SettingsFrame.yaml
Normal file
24
src/client/haxe/ru/m/tankz/frame/SettingsFrame.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
pWidth: 100
|
||||
pHeight: 100
|
||||
views:
|
||||
- $type: haxework.gui.LabelView
|
||||
$style: label
|
||||
pWidth: 100
|
||||
height: 20
|
||||
text: Settings
|
||||
- $type: haxework.gui.HGroupView
|
||||
pWidth: 100
|
||||
pHeight: 100
|
||||
views:
|
||||
- $type: ru.m.tankz.frame.settings.SettingsEditor
|
||||
pWidth: 50
|
||||
pHeight: 100
|
||||
controlIndex: 0
|
||||
- $type: ru.m.tankz.frame.settings.SettingsEditor
|
||||
pWidth: 50
|
||||
pHeight: 100
|
||||
controlIndex: 1
|
||||
- id: close
|
||||
$type: haxework.gui.ButtonView
|
||||
$style: close
|
||||
@@ -23,6 +23,7 @@ class StartFrame extends VGroupView {
|
||||
@:view var dota_2p_coop(default, null):ButtonView;
|
||||
@:view var dota_2p_vs(default, null):ButtonView;
|
||||
@:view var network(default, null):ButtonView;
|
||||
@:view var settings(default, null):ButtonView;
|
||||
|
||||
@:provide var frameSwitcher:IFrameSwitcher;
|
||||
@:provide var storage:SaveStorage;
|
||||
@@ -35,6 +36,7 @@ class StartFrame extends VGroupView {
|
||||
dota_2p_coop.onPress = this;
|
||||
dota_2p_vs.onPress = this;
|
||||
network.onPress = this;
|
||||
settings.onPress = this;
|
||||
}
|
||||
|
||||
public function onShow():Void {
|
||||
@@ -63,6 +65,8 @@ class StartFrame extends VGroupView {
|
||||
startGame(DotaGame.TYPE, DotaGame.PLAYER2_VS);
|
||||
case 'network':
|
||||
frameSwitcher.change(NetworkFrame.ID);
|
||||
case 'settings':
|
||||
frameSwitcher.change(SettingsFrame.ID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,3 +70,15 @@ views:
|
||||
$type: haxework.gui.ButtonView
|
||||
text: Network
|
||||
$style: button
|
||||
# settings
|
||||
- id: settings
|
||||
$type: haxework.gui.ButtonView
|
||||
inLayout: false
|
||||
hAlign: LEFT
|
||||
vAlign: BOTTOM
|
||||
leftMargin: 10
|
||||
bottomMargin: 10
|
||||
contentSize: true
|
||||
skin:
|
||||
$type: haxework.gui.skin.ButtonBitmapSkin
|
||||
image: "@asset:image:resources/image/ui/settings.png"
|
||||
|
||||
88
src/client/haxe/ru/m/tankz/frame/settings/ActionView.hx
Executable file
88
src/client/haxe/ru/m/tankz/frame/settings/ActionView.hx
Executable file
@@ -0,0 +1,88 @@
|
||||
package ru.m.tankz.frame.settings;
|
||||
|
||||
import haxework.gui.ButtonView;
|
||||
import haxework.gui.HGroupView;
|
||||
import haxework.gui.LabelView;
|
||||
import haxework.gui.list.ListView.IListItemView;
|
||||
import haxework.gui.skin.ColorSkin;
|
||||
import openfl.Assets;
|
||||
import openfl.events.KeyboardEvent;
|
||||
import promhx.Deferred;
|
||||
import promhx.Promise;
|
||||
import ru.m.tankz.control.ActionConfig;
|
||||
import ru.m.tankz.control.Control;
|
||||
|
||||
|
||||
class KeyboardMap {
|
||||
|
||||
private var data:Map<Int, String>;
|
||||
|
||||
public function new() {
|
||||
this.data = new Map();
|
||||
var data = Assets.getText("resources/keyboard.txt");
|
||||
for (line in data.split("\n")) {
|
||||
var arr = line.split("\t");
|
||||
if (arr.length == 2) {
|
||||
this.data.set(Std.parseInt(arr[1]), arr[0]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static var instance: KeyboardMap;
|
||||
|
||||
public static function getName(key: Int): String {
|
||||
if (instance == null) instance = new KeyboardMap();
|
||||
return key == -1 ? "<NONE>" : instance.data.exists(key) ? instance.data.get(key) : Std.string(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@:template("ru/m/tankz/frame/settings/ActionView.yaml", "ru/m/tankz/Style.yaml")
|
||||
class ActionView extends HGroupView implements IListItemView<ActionItem> {
|
||||
|
||||
public var item_index(default, default):Int;
|
||||
public var data(default, set):ActionItem;
|
||||
|
||||
@:view var action(default, null):LabelView;
|
||||
@:view var key(default, null):LabelView;
|
||||
|
||||
private var editDeferred: Deferred<Int>;
|
||||
|
||||
private function init():Void {
|
||||
}
|
||||
|
||||
private static function actionLabel(action: TankAction): String {
|
||||
return ActionConfig.action2string(action);
|
||||
}
|
||||
|
||||
private static function keyLabel(key: Int): String {
|
||||
return KeyboardMap.getName(key);
|
||||
}
|
||||
|
||||
private function set_data(value:ActionItem):ActionItem {
|
||||
data = value;
|
||||
action.text = actionLabel(data.action);
|
||||
key.text = keyLabel(data.key);
|
||||
return data;
|
||||
}
|
||||
|
||||
public function edit():Promise<Int> {
|
||||
cast(this.skin, ColorSkin).color = 0x00ff00;
|
||||
invalidate();
|
||||
editDeferred = new Deferred();
|
||||
content.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
|
||||
return editDeferred.promise();
|
||||
}
|
||||
|
||||
private function onKeyDown(event: KeyboardEvent):Void {
|
||||
content.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
|
||||
cast(this.skin, ColorSkin).color = 0x000000;
|
||||
invalidate();
|
||||
|
||||
data.key = event.keyCode;
|
||||
key.text = keyLabel(data.key);
|
||||
editDeferred.resolve(data.key);
|
||||
editDeferred = null;
|
||||
}
|
||||
}
|
||||
21
src/client/haxe/ru/m/tankz/frame/settings/ActionView.yaml
Normal file
21
src/client/haxe/ru/m/tankz/frame/settings/ActionView.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
width: 440
|
||||
height: 44
|
||||
margins: 5
|
||||
views:
|
||||
- id: action
|
||||
$type: haxework.gui.LabelView
|
||||
$style: label
|
||||
pWidth: 50
|
||||
pHeight: 100
|
||||
text: ""
|
||||
- id: key
|
||||
$type: haxework.gui.LabelView
|
||||
$style: label
|
||||
pWidth: 50
|
||||
pHeight: 100
|
||||
text: ""
|
||||
skin:
|
||||
$type: haxework.gui.skin.ColorSkin
|
||||
color: "#000000"
|
||||
alpha: 0.2
|
||||
75
src/client/haxe/ru/m/tankz/frame/settings/SettingsEditor.hx
Normal file
75
src/client/haxe/ru/m/tankz/frame/settings/SettingsEditor.hx
Normal file
@@ -0,0 +1,75 @@
|
||||
package ru.m.tankz.frame.settings;
|
||||
|
||||
import haxework.gui.LabelView;
|
||||
import ru.m.tankz.control.ActionConfig;
|
||||
import promhx.Promise;
|
||||
import ru.m.tankz.storage.SettingsStorage;
|
||||
import haxework.gui.ButtonView;
|
||||
import ru.m.tankz.control.ActionConfig.ActionItem;
|
||||
import haxework.gui.list.ListView;
|
||||
import haxework.gui.VGroupView;
|
||||
|
||||
|
||||
@:template("ru/m/tankz/frame/settings/SettingsEditor.yaml", "ru/m/tankz/Style.yaml")
|
||||
class SettingsEditor extends VGroupView {
|
||||
|
||||
public var controlIndex(default, set): Int;
|
||||
|
||||
@:view var label:LabelView;
|
||||
@:view var list:ListView<ActionItem>;
|
||||
@:view var change:ButtonView;
|
||||
@:view var clear:ButtonView;
|
||||
@:view var reset:ButtonView;
|
||||
|
||||
@:provide var storage: SettingsStorage;
|
||||
|
||||
private function init():Void {
|
||||
change.onPress = this;
|
||||
clear.onPress = this;
|
||||
reset.onPress = this;
|
||||
}
|
||||
|
||||
private function set_controlIndex(value: Int): Int {
|
||||
this.controlIndex = value;
|
||||
label.text = 'Player ${controlIndex+1}';
|
||||
list.data = storage.read(controlIndex).data;
|
||||
return this.controlIndex;
|
||||
}
|
||||
|
||||
public function onPress(view:ButtonView):Void {
|
||||
switch (view.id) {
|
||||
case "change": _change();
|
||||
case "clear": _clear();
|
||||
case "reset": _reset();
|
||||
case _:
|
||||
}
|
||||
}
|
||||
|
||||
private function _change():Void {
|
||||
var p: Promise<Int> = Promise.promise(0);
|
||||
for (view in list.items) {
|
||||
var v: ActionView = cast view;
|
||||
if (v.data == null) break;
|
||||
p = p.pipe(function(_):Promise<Int> return v.edit());
|
||||
}
|
||||
p.then(function(_) _save());
|
||||
}
|
||||
|
||||
private function _clear():Void {
|
||||
for (item in list.data) {
|
||||
item.key = -1;
|
||||
}
|
||||
list.invalidate();
|
||||
_save();
|
||||
}
|
||||
|
||||
private function _reset():Void {
|
||||
list.data = SettingsStorage.getDefault(controlIndex).data;
|
||||
list.invalidate();
|
||||
_save();
|
||||
}
|
||||
|
||||
private function _save():Void {
|
||||
storage.write(controlIndex, new ActionConfig(list.data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
$type: haxework.gui.VGroupView
|
||||
layoutMargin: 10
|
||||
views:
|
||||
- id: label
|
||||
$type: haxework.gui.LabelView
|
||||
$style: label
|
||||
- id: change
|
||||
$type: haxework.gui.ButtonView
|
||||
$style: button
|
||||
text: Change
|
||||
- id: clear
|
||||
$type: haxework.gui.ButtonView
|
||||
$style: button
|
||||
text: Clear
|
||||
- id: reset
|
||||
$type: haxework.gui.ButtonView
|
||||
$style: button
|
||||
text: Reset
|
||||
- id: list
|
||||
$type: haxework.gui.list.VListView<ru.m.tankz.control.ActionItem>
|
||||
factory: "@class:ru.m.tankz.frame.settings.ActionView"
|
||||
pWidth: 100
|
||||
pHeight: 100
|
||||
scroll:
|
||||
$type: haxework.gui.list.VScrollView
|
||||
width: 1
|
||||
pHeight: 100
|
||||
55
src/client/haxe/ru/m/tankz/storage/SettingsStorage.hx
Normal file
55
src/client/haxe/ru/m/tankz/storage/SettingsStorage.hx
Normal file
@@ -0,0 +1,55 @@
|
||||
package ru.m.tankz.storage;
|
||||
|
||||
import flash.net.SharedObject;
|
||||
import flash.ui.Keyboard;
|
||||
import ru.m.geom.Direction;
|
||||
import ru.m.tankz.control.ActionConfig;
|
||||
import ru.m.tankz.control.Control.TankAction;
|
||||
|
||||
|
||||
class SettingsStorage {
|
||||
|
||||
private static var TAG(default, never):String = 'SettingsStorage';
|
||||
|
||||
private var so:SharedObject;
|
||||
|
||||
public function new() {
|
||||
so = SharedObject.getLocal('settings');
|
||||
}
|
||||
|
||||
public function read(index: Int):Null<ActionConfig> {
|
||||
var data:String = Reflect.getProperty(so.data, Std.string(index));
|
||||
L.d(TAG, 'read: ${data}');
|
||||
if (data != null) {
|
||||
return ActionConfig.loads(data);
|
||||
}
|
||||
return getDefault(index);
|
||||
}
|
||||
|
||||
public function write(index: Int, data: ActionConfig):Void {
|
||||
L.d(TAG, 'write: ${data}');
|
||||
so.setProperty(Std.string(index), data.dumps());
|
||||
so.flush();
|
||||
}
|
||||
|
||||
public static function getDefault(index: Int): ActionConfig {
|
||||
return defaults.get(index).clone();
|
||||
}
|
||||
|
||||
private static var defaults: Map<Int, ActionConfig> = [
|
||||
0 => new ActionConfig([
|
||||
{action:TankAction.MOVE(Direction.TOP), key:Keyboard.W},
|
||||
{action:TankAction.MOVE(Direction.LEFT), key:Keyboard.A},
|
||||
{action:TankAction.MOVE(Direction.BOTTOM), key:Keyboard.S},
|
||||
{action:TankAction.MOVE(Direction.RIGHT), key:Keyboard.D},
|
||||
{action:TankAction.SHOT, key:Keyboard.SPACE},
|
||||
]),
|
||||
1 => new ActionConfig([
|
||||
{action:TankAction.MOVE(Direction.TOP), key:Keyboard.UP},
|
||||
{action:TankAction.MOVE(Direction.LEFT), key:Keyboard.LEFT},
|
||||
{action:TankAction.MOVE(Direction.BOTTOM), key:Keyboard.RIGHT},
|
||||
{action:TankAction.MOVE(Direction.RIGHT), key:Keyboard.DOWN},
|
||||
{action:TankAction.SHOT, key:Keyboard.NUMPAD_0},
|
||||
]),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user