mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-11 15:47:06 +08:00
feat: UI 控制器
This commit is contained in:
parent
fa7d2b2c16
commit
230e08e8a6
@ -64,7 +64,8 @@ export default tseslint.config(
|
||||
],
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'no-console': 'warn'
|
||||
'no-console': 'warn',
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
},
|
||||
eslintPluginPrettierRecommended
|
||||
|
@ -14,9 +14,9 @@ export type Props<
|
||||
> = T extends keyof JSX.IntrinsicElements
|
||||
? JSX.IntrinsicElements[T]
|
||||
: T extends DefineSetupFnComponent<any>
|
||||
? InstanceType<T>['$props']
|
||||
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
|
||||
: T extends DefineComponent
|
||||
? InstanceType<T>['$props']
|
||||
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
|
||||
: unknown;
|
||||
|
||||
export function disableViewport() {
|
||||
|
56
src/core/system/ui/container.tsx
Normal file
56
src/core/system/ui/container.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { IUIMountable, UIBaseElementSlots, UIComponent } from './shared';
|
||||
import { SetupComponentOptions } from '@/module';
|
||||
import { ContainerProps } from '@/core/render';
|
||||
|
||||
export interface UIContainerProps {
|
||||
controller: IUIMountable<UIComponent>;
|
||||
}
|
||||
|
||||
const containerConfig = {
|
||||
props: ['controller']
|
||||
} satisfies SetupComponentOptions<UIContainerProps>;
|
||||
|
||||
export const UIContainer = defineComponent<UIContainerProps>(props => {
|
||||
const data = props.controller;
|
||||
const back = data.backIns;
|
||||
const show = computed(() => data.stack.filter(v => !v.hidden));
|
||||
return () => {
|
||||
return (
|
||||
<data.baseElement>
|
||||
{(() => {
|
||||
const b = back.value;
|
||||
if (!b || !data.showBack.value || b.hidden) return;
|
||||
return (
|
||||
<b.ui.component
|
||||
{...b.vBind}
|
||||
key={b.key}
|
||||
></b.ui.component>
|
||||
);
|
||||
})()}
|
||||
{show.value.map(v => (
|
||||
<v.ui.component {...v.vBind} key={v.key}></v.ui.component>
|
||||
))}
|
||||
</data.baseElement>
|
||||
);
|
||||
};
|
||||
}, containerConfig);
|
||||
|
||||
export const UIRenderBase = defineComponent<
|
||||
ContainerProps,
|
||||
{},
|
||||
string,
|
||||
UIBaseElementSlots
|
||||
>((_props, { slots }) => {
|
||||
return () => {
|
||||
return <container>{slots.defaults()}</container>;
|
||||
};
|
||||
});
|
||||
|
||||
export const UIDomBase = defineComponent<{}, {}, string, UIBaseElementSlots>(
|
||||
(_props, { slots }) => {
|
||||
return () => {
|
||||
return <div>{slots.defaults()}</div>;
|
||||
};
|
||||
}
|
||||
);
|
@ -1,55 +1,307 @@
|
||||
import { Component, VNodeProps } from 'vue';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
IGameUI,
|
||||
IKeepController,
|
||||
IUIInstance,
|
||||
IUIMountable,
|
||||
UIBaseElement,
|
||||
UIComponent
|
||||
} from './shared';
|
||||
import { Props } from '@/core/render';
|
||||
import { UIInstance } from './instance';
|
||||
import {
|
||||
computed,
|
||||
ComputedRef,
|
||||
h,
|
||||
reactive,
|
||||
ref,
|
||||
Ref,
|
||||
shallowRef,
|
||||
ShallowRef
|
||||
} from 'vue';
|
||||
import { UIContainer } from './container';
|
||||
|
||||
export interface IUIControllerConfig<Element, UI> {
|
||||
/**
|
||||
* 将一个ui挂载至目标元素时的操作
|
||||
* @param element 要挂载至的目标元素
|
||||
* @param ui 要挂载的ui对象
|
||||
*/
|
||||
insert(element: Element, ui: UI): void;
|
||||
|
||||
/**
|
||||
* 将一个ui从目标元素上移除时的操作
|
||||
* @param element 被移除ui的父元素
|
||||
* @param ui 要被移除的ui元素
|
||||
*/
|
||||
remove(element: Element, ui: UI): void;
|
||||
|
||||
/**
|
||||
* 创建一个新UI
|
||||
* @param component UI组件
|
||||
* @param props UI传递的props
|
||||
*/
|
||||
createUI(
|
||||
component: Component,
|
||||
props?: (VNodeProps & { [key: string]: any }) | null
|
||||
): UI;
|
||||
export const enum UIMode {
|
||||
/** 仅显示最后一个 UI,在关闭时,只会关闭指定的 UI */
|
||||
LastOnly,
|
||||
/** 显示所有非手动隐藏的 UI,在关闭时,只会关闭指定 UI */
|
||||
All,
|
||||
/** 仅显示最后一个 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
|
||||
LastOnlyStack,
|
||||
/** 显示所有非手动隐藏的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
|
||||
AllStack,
|
||||
/** 自定义 UI 显示模式 */
|
||||
Custom
|
||||
}
|
||||
|
||||
export const enum OpenOption {
|
||||
Push,
|
||||
Unshift
|
||||
}
|
||||
|
||||
export const enum CloseOption {
|
||||
Splice,
|
||||
Pop,
|
||||
Shift
|
||||
}
|
||||
|
||||
export class UIController<Element, UI> {
|
||||
constructor(config: IUIControllerConfig<Element, UI>) {}
|
||||
export interface IUICustomConfig<C extends UIComponent> {
|
||||
/**
|
||||
* 打开一个新的 UI
|
||||
* @param ins 要打开的 UI 实例
|
||||
* @param stack 当前的 UI 栈,还未将 UI 实例加入栈中
|
||||
*/
|
||||
open(ins: IUIInstance<C>, stack: IUIInstance<C>[]): void;
|
||||
|
||||
/**
|
||||
* 设置当ui改变时控制器的行为
|
||||
* @param open 打开时的行为
|
||||
* @param close 关闭时的行为
|
||||
* 关闭一个 UI
|
||||
* @param ins 要关闭的 UI 实例
|
||||
* @param stack 当前的 UI 栈,还未将 UI 实例移除
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
setChangeMode(open: OpenOption, close: CloseOption) {}
|
||||
close(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void;
|
||||
|
||||
/**
|
||||
* 将这个UI控制器挂载至容器上
|
||||
* @param container 要挂载至的容器
|
||||
* 隐藏一个 UI
|
||||
* @param ins 要隐藏的 UI 实例,还未进入隐藏状态
|
||||
* @param stack 当前的 UI 栈
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
mount(container: Element) {}
|
||||
hide(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void;
|
||||
|
||||
/**
|
||||
* 显示一个 UI
|
||||
* @param ins 要显示的 UI 实例,还未进入显示状态
|
||||
* @param stack 当前的 UI 栈
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
show(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void;
|
||||
|
||||
/**
|
||||
* 更新所有 UI 的显示,一般会在显示模式更改时调用
|
||||
* @param stack 当前的 UI 栈
|
||||
*/
|
||||
update(stack: IUIInstance<C>[]): void;
|
||||
}
|
||||
|
||||
interface UIControllerEvent {}
|
||||
|
||||
export class UIController<C extends UIComponent = UIComponent>
|
||||
extends EventEmitter<UIControllerEvent>
|
||||
implements IUIMountable<C>
|
||||
{
|
||||
static controllers: Map<string, UIController> = new Map();
|
||||
|
||||
/** 当前的 ui 栈 */
|
||||
readonly stack: IUIInstance<C>[] = reactive([]);
|
||||
/** UI 显示方式 */
|
||||
mode: UIMode = UIMode.LastOnlyStack;
|
||||
/** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */
|
||||
background?: IGameUI<C>;
|
||||
|
||||
/** 背景 UI 实例 */
|
||||
readonly backIns: ShallowRef<IUIInstance<C> | null> = shallowRef(null);
|
||||
/** 当前是否显示背景 UI */
|
||||
readonly showBack: ComputedRef<boolean> = computed(
|
||||
() => this.userShowBack.value && this.sysShowBack.value
|
||||
);
|
||||
|
||||
/** 自定义显示模式下的配置信息 */
|
||||
private config?: IUICustomConfig<C>;
|
||||
/** 是否维持背景 UI */
|
||||
private keepBack: boolean = false;
|
||||
/** 用户是否显示背景 UI */
|
||||
private readonly userShowBack: Ref<boolean> = ref(true);
|
||||
/** 系统是否显示背景 UI */
|
||||
private readonly sysShowBack: Ref<boolean> = ref(false);
|
||||
|
||||
/**
|
||||
* 创建一个 ui 控制器
|
||||
* @param id 这个控制器的唯一标识符
|
||||
*/
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly baseElement: UIBaseElement
|
||||
) {
|
||||
super();
|
||||
if (UIController.controllers.has(id)) {
|
||||
logger.warn(57, id);
|
||||
} else {
|
||||
UIController.controllers.set(id, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染这个 UI
|
||||
*/
|
||||
render() {
|
||||
return h(UIContainer, { controller: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置背景 UI
|
||||
* @param back 这个 UI 控制器的背景 UI
|
||||
*/
|
||||
setBackground(back: IGameUI<C>) {
|
||||
this.background = back;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏背景 UI
|
||||
*/
|
||||
hideBackground() {
|
||||
this.userShowBack.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示背景 UI
|
||||
*/
|
||||
showBackground() {
|
||||
this.userShowBack.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 维持背景 UI,一般用于防闪烁,例如使用道具时可能在关闭道具栏后打开新 UI,这时就需要防闪烁
|
||||
*/
|
||||
keep(): IKeepController {
|
||||
this.keepBack = true;
|
||||
|
||||
return {
|
||||
safelyUnload: () => {
|
||||
if (this.stack.length > 0) return;
|
||||
this.sysShowBack.value = false;
|
||||
this.keepBack = false;
|
||||
},
|
||||
unload: () => {
|
||||
this.sysShowBack.value = false;
|
||||
this.keepBack = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个 ui
|
||||
* @param ui 要打开的 ui
|
||||
* @param vBind 传递给这个 ui 的响应式数据
|
||||
*/
|
||||
open(ui: IGameUI<C>, vBind: Props<C>) {
|
||||
const ins = new UIInstance(ui, vBind);
|
||||
switch (this.mode) {
|
||||
case UIMode.LastOnly:
|
||||
case UIMode.LastOnlyStack:
|
||||
this.stack.forEach(v => v.hide());
|
||||
this.stack.push(ins);
|
||||
break;
|
||||
case UIMode.All:
|
||||
case UIMode.AllStack:
|
||||
this.stack.push(ins);
|
||||
break;
|
||||
case UIMode.Custom:
|
||||
this.config?.open(ins, this.stack);
|
||||
break;
|
||||
}
|
||||
this.sysShowBack.value = true;
|
||||
return ins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭一个 ui
|
||||
* @param ui 要关闭的 ui 实例
|
||||
*/
|
||||
close(ui: UIInstance<C>) {
|
||||
const index = this.stack.indexOf(ui);
|
||||
if (index === -1) return;
|
||||
switch (this.mode) {
|
||||
case UIMode.LastOnly: {
|
||||
this.stack.splice(index, 1);
|
||||
this.stack.forEach(v => v.hide());
|
||||
const last = this.stack.at(-1);
|
||||
if (!last) break;
|
||||
last.show();
|
||||
break;
|
||||
}
|
||||
case UIMode.LastOnlyStack: {
|
||||
this.stack.splice(index);
|
||||
this.stack.forEach(v => v.hide());
|
||||
const last = this.stack[index - 1];
|
||||
if (!last) break;
|
||||
last.show();
|
||||
break;
|
||||
}
|
||||
case UIMode.All: {
|
||||
this.stack.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case UIMode.AllStack: {
|
||||
this.stack.splice(index);
|
||||
break;
|
||||
}
|
||||
case UIMode.Custom: {
|
||||
this.config?.close(ui, this.stack, index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.keepBack && this.stack.length === 0) {
|
||||
this.sysShowBack.value = false;
|
||||
}
|
||||
this.keepBack = false;
|
||||
}
|
||||
|
||||
hide(ins: IUIInstance<C>): void {
|
||||
const index = this.stack.indexOf(ins);
|
||||
if (index === -1) return;
|
||||
if (this.mode === UIMode.Custom) {
|
||||
this.config?.hide(ins, this.stack, index);
|
||||
} else {
|
||||
ins.hide();
|
||||
}
|
||||
}
|
||||
|
||||
show(ins: IUIInstance<C>): void {
|
||||
const index = this.stack.indexOf(ins);
|
||||
if (index === -1) return;
|
||||
if (this.mode === UIMode.Custom) {
|
||||
this.config?.show(ins, this.stack, index);
|
||||
} else {
|
||||
ins.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为仅显示最后一个 UI
|
||||
* @param stack 是否设置为栈模式,即删除一个 UI 后,其之后打开的 UI 是否也一并删除
|
||||
*/
|
||||
lastOnly(stack: boolean = true) {
|
||||
if (stack) {
|
||||
this.mode = UIMode.LastOnlyStack;
|
||||
} else {
|
||||
this.mode = UIMode.LastOnly;
|
||||
}
|
||||
this.stack.forEach(v => v.hide());
|
||||
this.stack.at(-1)?.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为显示所有 UI
|
||||
* @param stack 是否设置为栈模式,即删除一个 UI 后,其之后打开的 UI 是否也一并删除
|
||||
*/
|
||||
showAll(stack: boolean = false) {
|
||||
if (stack) {
|
||||
this.mode = UIMode.AllStack;
|
||||
} else {
|
||||
this.mode = UIMode.All;
|
||||
}
|
||||
this.stack.forEach(v => v.show());
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用自定义的显示模式
|
||||
* @param config 自定义显示模式的配置
|
||||
*/
|
||||
showCustom(config: IUICustomConfig<C>) {
|
||||
this.mode = UIMode.Custom;
|
||||
this.config = config;
|
||||
config.update(this.stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个元素上的 ui 控制器
|
||||
* @param id 要获取的 ui 控制器的唯一标识符
|
||||
*/
|
||||
static getController<T extends UIComponent>(
|
||||
id: string
|
||||
): UIController<T> | null {
|
||||
const res = this.controllers.get(id) as UIController<T>;
|
||||
return res ?? null;
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,5 @@
|
||||
export * from './controller';
|
||||
export * from './ui';
|
||||
export * from './shared';
|
||||
export * from './instance';
|
||||
export * from './container';
|
||||
|
50
src/core/system/ui/instance.ts
Normal file
50
src/core/system/ui/instance.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Props } from '@/core/render';
|
||||
import { IGameUI, IUIInstance, UIComponent } from './shared';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { markRaw, mergeProps } from 'vue';
|
||||
|
||||
interface UIInstanceEvent {
|
||||
hide: [];
|
||||
show: [];
|
||||
close: [];
|
||||
}
|
||||
|
||||
export class UIInstance<C extends UIComponent>
|
||||
extends EventEmitter<UIInstanceEvent>
|
||||
implements IUIInstance<C>
|
||||
{
|
||||
private static counter: number = 0;
|
||||
|
||||
readonly key: number = UIInstance.counter++;
|
||||
readonly ui: IGameUI<C>;
|
||||
hidden: boolean = false;
|
||||
|
||||
constructor(
|
||||
ui: IGameUI<C>,
|
||||
public vBind: Props<C>
|
||||
) {
|
||||
super();
|
||||
this.ui = markRaw(ui);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个 UI 实例的响应式数据的值
|
||||
* @param data 要设置的值
|
||||
* @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false)
|
||||
*/
|
||||
setVBind(data: Props<C>, merge: boolean = true) {
|
||||
if (merge) {
|
||||
this.vBind = mergeProps(this.vBind, data) as Props<C>;
|
||||
} else {
|
||||
this.vBind = data;
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.hidden = true;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.hidden = false;
|
||||
}
|
||||
}
|
87
src/core/system/ui/shared.ts
Normal file
87
src/core/system/ui/shared.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Props } from '@/core/render';
|
||||
import {
|
||||
DefineComponent,
|
||||
DefineSetupFnComponent,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
SlotsType,
|
||||
VNode
|
||||
} from 'vue';
|
||||
|
||||
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
|
||||
|
||||
export interface IGameUI<C extends UIComponent> {
|
||||
/** 这个 UI 的名称 */
|
||||
readonly name: string;
|
||||
/** 这个 UI 的组件 */
|
||||
readonly component: C;
|
||||
}
|
||||
|
||||
export interface IKeepController {
|
||||
/**
|
||||
* 安全关闭背景 UI,如果当前没有 UI 已开启,那么直接关闭,否则维持
|
||||
*/
|
||||
safelyUnload(): void;
|
||||
|
||||
/**
|
||||
* 不论当前是否有 UI 已开启,都关闭背景
|
||||
*/
|
||||
unload(): void;
|
||||
}
|
||||
|
||||
export type UIBaseElementSlots = SlotsType<{
|
||||
defaults: () => VNode[];
|
||||
}>;
|
||||
|
||||
export type UIBaseElement =
|
||||
| DefineComponent<{}, {}, string, UIBaseElementSlots>
|
||||
| DefineSetupFnComponent<{}, {}, UIBaseElementSlots>;
|
||||
|
||||
export interface IUIMountable<C extends UIComponent> {
|
||||
/** 当前的 UI 栈 */
|
||||
readonly stack: IUIInstance<C>[];
|
||||
/** 当前的背景 UI */
|
||||
readonly backIns: ShallowRef<IUIInstance<C> | null>;
|
||||
/** 当前是否显示背景 UI */
|
||||
readonly showBack: Ref<boolean>;
|
||||
/** UI 控制器的根元素 */
|
||||
readonly baseElement: UIBaseElement;
|
||||
|
||||
/**
|
||||
* 隐藏一个 UI
|
||||
* @param ins 要隐藏的 UI 实例
|
||||
*/
|
||||
hide(ins: IUIInstance<C>): void;
|
||||
|
||||
/**
|
||||
* 显示一个 UI
|
||||
* @param ins 要显示的 UI 实例
|
||||
*/
|
||||
show(ins: IUIInstance<C>): void;
|
||||
|
||||
/**
|
||||
* 维持背景,直到下次所有 UI 都被关闭
|
||||
*/
|
||||
keep(): IKeepController;
|
||||
}
|
||||
|
||||
export interface IUIInstance<C extends UIComponent> {
|
||||
/** 这个 ui 实例的唯一 key,用于 vue */
|
||||
readonly key: number;
|
||||
/** 这个 ui 实例的 ui 信息 */
|
||||
readonly ui: IGameUI<C>;
|
||||
/** 传递给这个 ui 实例的响应式数据 */
|
||||
readonly vBind: Props<C>;
|
||||
/** 当前元素是否被隐藏 */
|
||||
readonly hidden: boolean;
|
||||
|
||||
/**
|
||||
* 隐藏这个 ui
|
||||
*/
|
||||
hide(): void;
|
||||
|
||||
/**
|
||||
* 显示这个 ui
|
||||
*/
|
||||
show(): void;
|
||||
}
|
19
src/core/system/ui/ui.ts
Normal file
19
src/core/system/ui/ui.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { IGameUI, UIComponent } from './shared';
|
||||
|
||||
export class GameUI<C extends UIComponent> implements IGameUI<C> {
|
||||
static list: Map<string, GameUI<UIComponent>> = new Map();
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly component: C
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 根据 ui 名称获取 ui 实例
|
||||
* @param id ui 的名称
|
||||
*/
|
||||
static get<T extends UIComponent>(id: string): GameUI<T> | null {
|
||||
const ui = this.list.get(id) as GameUI<T>;
|
||||
return ui ?? null;
|
||||
}
|
||||
}
|
@ -87,7 +87,8 @@
|
||||
"53": "Cannot $1 audio route '$2', since there is not added route named it.",
|
||||
"54": "Missing start tag for '$1', index: $2.",
|
||||
"55": "Unchildable tag '$1' should follow with param.",
|
||||
"56": "Method '$1' is deprecated. Consider using '$2' instead.",
|
||||
"56": "Method '$1' has been deprecated. Consider using '$2' instead.",
|
||||
"57": "Repeated UI controller on item '$1', new controller will not work.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
|
||||
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import { Textbox } from './components';
|
||||
import { ILayerGroupRenderExtends, ILayerRenderExtends } from '@/core/render';
|
||||
import { Props } from '@/core/render';
|
||||
import { WeatherController } from '../weather';
|
||||
import { UIController, UIRenderBase } from '@/core/system';
|
||||
|
||||
export function create() {
|
||||
const main = new MotaRenderer();
|
||||
@ -63,8 +64,11 @@ export function create() {
|
||||
weather.bind(map.value);
|
||||
});
|
||||
|
||||
const ui = new UIController('main-ui', UIRenderBase);
|
||||
|
||||
return () => (
|
||||
<container id="map-draw" {...mapDrawProps}>
|
||||
{ui.render()}
|
||||
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
|
||||
<layer layer="bg" zIndex={10}></layer>
|
||||
<layer layer="bg2" zIndex={20}></layer>
|
||||
|
Loading…
Reference in New Issue
Block a user