mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 04:53:49 +00:00
UI refinements (#2859)
## Description: UI Refinements requested by @evanpelle check https://ui.openfront.dev ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { formatKeyForDisplay, translateText } from "../client/Utils";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./components/baseComponents/setting/SettingKeybind";
|
||||
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
|
||||
import "./components/baseComponents/setting/SettingNumber";
|
||||
import "./components/baseComponents/setting/SettingSlider";
|
||||
import "./components/baseComponents/setting/SettingToggle";
|
||||
@@ -13,18 +15,203 @@ interface FlagInputModalElement extends HTMLElement {
|
||||
returnTo?: string;
|
||||
}
|
||||
|
||||
const DefaultKeybinds: Record<string, string> = {
|
||||
toggleView: "Space",
|
||||
buildCity: "Digit1",
|
||||
buildFactory: "Digit2",
|
||||
buildPort: "Digit3",
|
||||
buildDefensePost: "Digit4",
|
||||
buildMissileSilo: "Digit5",
|
||||
buildSamLauncher: "Digit6",
|
||||
buildWarship: "Digit7",
|
||||
buildAtomBomb: "Digit8",
|
||||
buildHydrogenBomb: "Digit9",
|
||||
buildMIRV: "Digit0",
|
||||
attackRatioDown: "KeyT",
|
||||
attackRatioUp: "KeyY",
|
||||
boatAttack: "KeyB",
|
||||
groundAttack: "KeyG",
|
||||
zoomOut: "KeyQ",
|
||||
zoomIn: "KeyE",
|
||||
centerCamera: "KeyC",
|
||||
moveUp: "KeyW",
|
||||
moveLeft: "KeyA",
|
||||
moveDown: "KeyS",
|
||||
moveRight: "KeyD",
|
||||
};
|
||||
|
||||
@customElement("user-setting")
|
||||
export class UserSettingModal extends BaseModal {
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
@state() private activeTab: "basic" | "keybinds" = "basic";
|
||||
|
||||
@state() private keySequence: string[] = [];
|
||||
@state() private showEasterEggSettings = false;
|
||||
|
||||
@state() private keybinds: Record<
|
||||
string,
|
||||
{ value: string | string[]; key: string }
|
||||
> = {};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadKeybindsFromStorage();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleEasterEggKey);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private loadKeybindsFromStorage() {
|
||||
const savedKeybinds = localStorage.getItem("settings.keybinds");
|
||||
if (!savedKeybinds) return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(savedKeybinds);
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
!Array.isArray(parsed)
|
||||
) {
|
||||
const isValid = Object.values(parsed).every((entry) => {
|
||||
if (
|
||||
typeof entry !== "object" ||
|
||||
entry === null ||
|
||||
Array.isArray(entry)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!("key" in entry) || typeof (entry as any).key !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (!("value" in entry)) {
|
||||
return false;
|
||||
}
|
||||
const value = (entry as any).value;
|
||||
if (typeof value === "string") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.every((v) => typeof v === "string");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
this.keybinds = parsed;
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid keybinds structure: entries must be objects with 'key' (string) and 'value' (string or string[]) properties. Ignoring saved data.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid keybinds data: expected non-array object. Ignoring saved data.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Invalid keybinds JSON:", e);
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{
|
||||
action: string;
|
||||
value: string;
|
||||
key: string;
|
||||
prevValue?: string;
|
||||
}>,
|
||||
) {
|
||||
const { action, value, key, prevValue } = e.detail;
|
||||
|
||||
const activeKeybinds: Record<string, string> = { ...DefaultKeybinds };
|
||||
for (const [k, v] of Object.entries(this.keybinds)) {
|
||||
const normalizedValue = Array.isArray(v.value)
|
||||
? v.value[0] || ""
|
||||
: v.value;
|
||||
if (normalizedValue === "Null") {
|
||||
delete activeKeybinds[k];
|
||||
} else {
|
||||
activeKeybinds[k] = normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
const values = Object.entries(activeKeybinds)
|
||||
.filter(([k]) => k !== action)
|
||||
.map(([, v]) => v);
|
||||
|
||||
if (values.includes(value) && value !== "Null") {
|
||||
const displayKey = formatKeyForDisplay(key || value);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-red-500 inline-block align-middle mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">
|
||||
${(() => {
|
||||
const message = translateText(
|
||||
"user_setting.keybind_conflict_error",
|
||||
{ key: displayKey },
|
||||
);
|
||||
const parts = message.split(displayKey);
|
||||
return html`${parts[0]}<span
|
||||
class="font-mono font-bold bg-white/10 px-1.5 py-0.5 rounded text-red-200 mx-1 border border-white/10"
|
||||
>${displayKey}</span
|
||||
>${parts[1] || ""}`;
|
||||
})()}
|
||||
</span>
|
||||
`,
|
||||
color: "red",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const element = this.renderRoot.querySelector(
|
||||
`setting-keybind[action="${action}"]`,
|
||||
) as SettingKeybind;
|
||||
if (element) {
|
||||
element.value = prevValue ?? DefaultKeybinds[action] ?? "";
|
||||
element.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.keybinds = { ...this.keybinds, [action]: { value: value, key: key } };
|
||||
localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds));
|
||||
}
|
||||
|
||||
private getKeyValue(action: string): string | undefined {
|
||||
const entry = this.keybinds[action];
|
||||
if (!entry) return undefined;
|
||||
const normalizedValue = Array.isArray(entry.value)
|
||||
? entry.value[0] || ""
|
||||
: entry.value;
|
||||
if (normalizedValue === "Null") return "";
|
||||
return normalizedValue || undefined;
|
||||
}
|
||||
|
||||
private getKeyChar(action: string): string {
|
||||
const entry = this.keybinds[action];
|
||||
if (!entry) return "";
|
||||
return entry.key || "";
|
||||
}
|
||||
|
||||
private handleEasterEggKey = (e: KeyboardEvent) => {
|
||||
if (!this.isModalOpen || this.showEasterEggSettings) return;
|
||||
|
||||
@@ -188,20 +375,25 @@ export class UserSettingModal extends BaseModal {
|
||||
document.querySelector<FlagInputModalElement>("#flag-input-modal");
|
||||
if (flagInputModal?.open) {
|
||||
this.close();
|
||||
flagInputModal.returnTo = "#" + (this.id || "page-options");
|
||||
flagInputModal.returnTo = "#" + (this.id || "page-settings");
|
||||
flagInputModal.open();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const activeContent =
|
||||
this.activeTab === "basic"
|
||||
? this.renderBasicSettings()
|
||||
: this.renderKeybindSettings();
|
||||
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center mb-6 pb-2 border-b border-white/10 gap-2 shrink-0 p-6"
|
||||
class="relative flex flex-col mb-6 border-b border-white/10 pb-4 shrink-0 p-6"
|
||||
>
|
||||
<div class="flex items-center gap-4 flex-1 flex-wrap">
|
||||
<button
|
||||
@@ -230,12 +422,33 @@ export class UserSettingModal extends BaseModal {
|
||||
${translateText("user_setting.title")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-2 justify-center mt-4">
|
||||
<button
|
||||
class="px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest ${this
|
||||
.activeTab === "basic"
|
||||
? "bg-blue-500/20 text-blue-400 border border-blue-500/30 shadow-[0_0_15px_rgba(59,130,246,0.2)]"
|
||||
: "text-white/40 hover:text-white hover:bg-white/5 border border-transparent"}"
|
||||
@click=${() => (this.activeTab = "basic")}
|
||||
>
|
||||
${translateText("user_setting.tab_basic")}
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest ${this
|
||||
.activeTab === "keybinds"
|
||||
? "bg-blue-500/20 text-blue-400 border border-blue-500/30 shadow-[0_0_15px_rgba(59,130,246,0.2)]"
|
||||
: "text-white/40 hover:text-white hover:bg-white/5 border border-transparent"}"
|
||||
@click=${() => (this.activeTab = "keybinds")}
|
||||
>
|
||||
${translateText("user_setting.tab_keybinds")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent px-6 pb-6 mr-1"
|
||||
>
|
||||
<div class="flex flex-col gap-2">${this.renderBasicSettings()}</div>
|
||||
<div class="flex flex-col gap-2">${activeContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -260,6 +473,266 @@ export class UserSettingModal extends BaseModal {
|
||||
window.removeEventListener("keydown", this.handleEasterEggKey);
|
||||
}
|
||||
|
||||
private renderKeybindSettings() {
|
||||
return html`
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-4 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.view_options")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="toggleView"
|
||||
label=${translateText("user_setting.toggle_view")}
|
||||
description=${translateText("user_setting.toggle_view_desc")}
|
||||
defaultKey="Space"
|
||||
.value=${this.getKeyValue("toggleView")}
|
||||
.display=${this.getKeyChar("toggleView")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.build_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="buildCity"
|
||||
label=${translateText("user_setting.build_city")}
|
||||
description=${translateText("user_setting.build_city_desc")}
|
||||
defaultKey="Digit1"
|
||||
.value=${this.getKeyValue("buildCity")}
|
||||
.display=${this.getKeyChar("buildCity")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildFactory"
|
||||
label=${translateText("user_setting.build_factory")}
|
||||
description=${translateText("user_setting.build_factory_desc")}
|
||||
defaultKey="Digit2"
|
||||
.value=${this.getKeyValue("buildFactory")}
|
||||
.display=${this.getKeyChar("buildFactory")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildPort"
|
||||
label=${translateText("user_setting.build_port")}
|
||||
description=${translateText("user_setting.build_port_desc")}
|
||||
defaultKey="Digit3"
|
||||
.value=${this.getKeyValue("buildPort")}
|
||||
.display=${this.getKeyChar("buildPort")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildDefensePost"
|
||||
label=${translateText("user_setting.build_defense_post")}
|
||||
description=${translateText("user_setting.build_defense_post_desc")}
|
||||
defaultKey="Digit4"
|
||||
.value=${this.getKeyValue("buildDefensePost")}
|
||||
.display=${this.getKeyChar("buildDefensePost")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildMissileSilo"
|
||||
label=${translateText("user_setting.build_missile_silo")}
|
||||
description=${translateText("user_setting.build_missile_silo_desc")}
|
||||
defaultKey="Digit5"
|
||||
.value=${this.getKeyValue("buildMissileSilo")}
|
||||
.display=${this.getKeyChar("buildMissileSilo")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildSamLauncher"
|
||||
label=${translateText("user_setting.build_sam_launcher")}
|
||||
description=${translateText("user_setting.build_sam_launcher_desc")}
|
||||
defaultKey="Digit6"
|
||||
.value=${this.getKeyValue("buildSamLauncher")}
|
||||
.display=${this.getKeyChar("buildSamLauncher")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildWarship"
|
||||
label=${translateText("user_setting.build_warship")}
|
||||
description=${translateText("user_setting.build_warship_desc")}
|
||||
defaultKey="Digit7"
|
||||
.value=${this.getKeyValue("buildWarship")}
|
||||
.display=${this.getKeyChar("buildWarship")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildAtomBomb"
|
||||
label=${translateText("user_setting.build_atom_bomb")}
|
||||
description=${translateText("user_setting.build_atom_bomb_desc")}
|
||||
defaultKey="Digit8"
|
||||
.value=${this.getKeyValue("buildAtomBomb")}
|
||||
.display=${this.getKeyChar("buildAtomBomb")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildHydrogenBomb"
|
||||
label=${translateText("user_setting.build_hydrogen_bomb")}
|
||||
description=${translateText("user_setting.build_hydrogen_bomb_desc")}
|
||||
defaultKey="Digit9"
|
||||
.value=${this.getKeyValue("buildHydrogenBomb")}
|
||||
.display=${this.getKeyChar("buildHydrogenBomb")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildMIRV"
|
||||
label=${translateText("user_setting.build_mirv")}
|
||||
description=${translateText("user_setting.build_mirv_desc")}
|
||||
defaultKey="Digit0"
|
||||
.value=${this.getKeyValue("buildMIRV")}
|
||||
.display=${this.getKeyChar("buildMIRV")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.attack_ratio_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="attackRatioDown"
|
||||
label=${translateText("user_setting.attack_ratio_down")}
|
||||
description=${translateText("user_setting.attack_ratio_down_desc")}
|
||||
defaultKey="KeyT"
|
||||
.value=${this.getKeyValue("attackRatioDown")}
|
||||
.display=${this.getKeyChar("attackRatioDown")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="attackRatioUp"
|
||||
label=${translateText("user_setting.attack_ratio_up")}
|
||||
description=${translateText("user_setting.attack_ratio_up_desc")}
|
||||
defaultKey="KeyY"
|
||||
.value=${this.getKeyValue("attackRatioUp")}
|
||||
.display=${this.getKeyChar("attackRatioUp")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.attack_keybinds")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="boatAttack"
|
||||
label=${translateText("user_setting.boat_attack")}
|
||||
description=${translateText("user_setting.boat_attack_desc")}
|
||||
defaultKey="KeyB"
|
||||
.value=${this.getKeyValue("boatAttack")}
|
||||
.display=${this.getKeyChar("boatAttack")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="groundAttack"
|
||||
label=${translateText("user_setting.ground_attack")}
|
||||
description=${translateText("user_setting.ground_attack_desc")}
|
||||
defaultKey="KeyG"
|
||||
.value=${this.getKeyValue("groundAttack")}
|
||||
.display=${this.getKeyChar("groundAttack")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.zoom_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomOut"
|
||||
label=${translateText("user_setting.zoom_out")}
|
||||
description=${translateText("user_setting.zoom_out_desc")}
|
||||
defaultKey="KeyQ"
|
||||
.value=${this.getKeyValue("zoomOut")}
|
||||
.display=${this.getKeyChar("zoomOut")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomIn"
|
||||
label=${translateText("user_setting.zoom_in")}
|
||||
description=${translateText("user_setting.zoom_in_desc")}
|
||||
defaultKey="KeyE"
|
||||
.value=${this.getKeyValue("zoomIn")}
|
||||
.display=${this.getKeyChar("zoomIn")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.camera_movement")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="centerCamera"
|
||||
label=${translateText("user_setting.center_camera")}
|
||||
description=${translateText("user_setting.center_camera_desc")}
|
||||
defaultKey="KeyC"
|
||||
.value=${this.getKeyValue("centerCamera")}
|
||||
.display=${this.getKeyChar("centerCamera")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveUp"
|
||||
label=${translateText("user_setting.move_up")}
|
||||
description=${translateText("user_setting.move_up_desc")}
|
||||
defaultKey="KeyW"
|
||||
.value=${this.getKeyValue("moveUp")}
|
||||
.display=${this.getKeyChar("moveUp")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveLeft"
|
||||
label=${translateText("user_setting.move_left")}
|
||||
description=${translateText("user_setting.move_left_desc")}
|
||||
defaultKey="KeyA"
|
||||
.value=${this.getKeyValue("moveLeft")}
|
||||
.display=${this.getKeyChar("moveLeft")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveDown"
|
||||
label=${translateText("user_setting.move_down")}
|
||||
description=${translateText("user_setting.move_down_desc")}
|
||||
defaultKey="KeyS"
|
||||
.value=${this.getKeyValue("moveDown")}
|
||||
.display=${this.getKeyChar("moveDown")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveRight"
|
||||
label=${translateText("user_setting.move_right")}
|
||||
description=${translateText("user_setting.move_right_desc")}
|
||||
defaultKey="KeyD"
|
||||
.value=${this.getKeyValue("moveRight")}
|
||||
.display=${this.getKeyChar("moveRight")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBasicSettings() {
|
||||
return html`
|
||||
<!-- 🚩 Flag Selector -->
|
||||
@@ -450,6 +923,7 @@ export class UserSettingModal extends BaseModal {
|
||||
|
||||
protected onOpen(): void {
|
||||
window.addEventListener("keydown", this.handleEasterEggKey);
|
||||
this.loadKeybindsFromStorage();
|
||||
}
|
||||
|
||||
public open() {
|
||||
|
||||
Reference in New Issue
Block a user