添加文件复制功能,修复所有对话框在等待时可以使用ESC中断的BUG
This commit is contained in:
parent
d7376ca2fb
commit
027e31f192
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "media_player_client",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.1",
|
||||
"description": "A Quasar Framework app",
|
||||
"productName": "MediaPlayerClient",
|
||||
"author": "fangxiang <fangxiang@cloudview.work>",
|
||||
|
|
|
@ -852,6 +852,26 @@ export default class ClientConnection {
|
|||
}
|
||||
}
|
||||
|
||||
public async fileOperator(
|
||||
from_path: string,
|
||||
to_path: string,
|
||||
operator_type: string,
|
||||
force_operator: boolean = true
|
||||
) {
|
||||
try {
|
||||
return await this.doRpc<Protocol.FileOperatorResponseEntity>(
|
||||
new Protocol.FileOperatorRequestEntity(
|
||||
from_path,
|
||||
to_path,
|
||||
operator_type,
|
||||
force_operator
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getEdgeBlendingInfo() {
|
||||
return await this.doRpc<Protocol.GetEdgeBlendingInfoResponseEntity>(
|
||||
new Protocol.GetEdgeBlendingInfoRequestEntity(0)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<template>
|
||||
<q-dialog
|
||||
v-model="show_dialog"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
|||
>
|
||||
<q-card
|
||||
class="overflow-hidden"
|
||||
style="overflow-y: scroll; max-width: 60vw; max-height: 80vh"
|
||||
style="overflow-y: scroll; max-width: 70vw; max-height: 80vh"
|
||||
>
|
||||
<q-card-section class="q-ma-none q-pa-sm">
|
||||
<div class="row">
|
||||
|
@ -27,6 +27,7 @@
|
|||
<div>
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
:disable="loading"
|
||||
flat
|
||||
round
|
||||
icon="close"
|
||||
|
@ -122,6 +123,20 @@
|
|||
{{ $t("refresh") }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
icon="content_paste"
|
||||
@click="pasteFile"
|
||||
:disable="
|
||||
!(clipboard && clipboard.type && clipboard.type != 'NONE') ||
|
||||
path == clipboard.path
|
||||
"
|
||||
:label="$t('paste')"
|
||||
>
|
||||
<q-tooltip>
|
||||
{{ $t("paste") }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
icon="create_new_folder"
|
||||
|
@ -159,7 +174,7 @@
|
|||
<q-separator />
|
||||
|
||||
<q-card-section
|
||||
style="max-height: 50vh; width: 60vw; max-height: 80vh"
|
||||
style="max-height: 50vh; width: 70vw; max-height: 80vh"
|
||||
class="scroll q-pa-none q-ma-none"
|
||||
>
|
||||
<q-table
|
||||
|
@ -182,6 +197,12 @@
|
|||
<template v-slot:body-cell="props">
|
||||
<q-td :props="props">
|
||||
<div v-if="props.col.name == 'name'">
|
||||
<q-icon
|
||||
v-if="false && clipboard.name == props.value"
|
||||
:name="clipboard.type == 'COPY' ? 'file_copy' : 'content_cut'"
|
||||
style="color: #ffbe4a; font-size: 2.5em"
|
||||
>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
:name="props.row.is_directory ? 'folder' : 'description'"
|
||||
style="color: #ffbe4a; font-size: 2.5em"
|
||||
|
@ -206,6 +227,13 @@
|
|||
</q-tooltip>
|
||||
</a>
|
||||
|
||||
<a href="#" @click="(evt) => copyFile(props.row)">
|
||||
{{ $t("_copy2") }}
|
||||
<q-tooltip>
|
||||
{{ $t("click") }}{{ $t("_copy2") }}
|
||||
</q-tooltip>
|
||||
</a>
|
||||
|
||||
<a href="#" @click="(evt) => deleteFile(props.row)">
|
||||
{{ $t("delete") }}
|
||||
<q-tooltip>
|
||||
|
@ -250,6 +278,7 @@
|
|||
<q-menu :target="target_dom" v-model="show_context_menu">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-if="false"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="(evt) => copyStringToClipboard(current_file.name)"
|
||||
|
@ -257,6 +286,38 @@
|
|||
<q-item-section avatar><q-icon name="file_copy" /></q-item-section>
|
||||
<q-item-section>{{ $t("copy name") }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="(evt) => copyFile(current_file)"
|
||||
>
|
||||
<q-item-section avatar><q-icon name="file_copy" /></q-item-section>
|
||||
<q-item-section>{{ $t("_copy2") }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="false"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="(evt) => cutFile(current_file)"
|
||||
>
|
||||
<q-item-section avatar><q-icon name="content_cut" /></q-item-section>
|
||||
<q-item-section>{{ $t("cut") }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="false"
|
||||
:disable="
|
||||
!(clipboard && clipboard.type && clipboard.type != 'NONE') ||
|
||||
path == clipboard.path
|
||||
"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="(evt) => pasteFile()"
|
||||
>
|
||||
<q-item-section avatar
|
||||
><q-icon name="content_paste"
|
||||
/></q-item-section>
|
||||
<q-item-section>{{ $t("paste") }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
|
@ -324,7 +385,7 @@ a:hover {
|
|||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref, watch, computed } from "vue";
|
||||
import { defineComponent, ref, Ref, reactive, watch, computed } from "vue";
|
||||
import { useStore } from "src/store";
|
||||
import { useQuasar, date, copyToClipboard } from "quasar";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
@ -333,6 +394,7 @@ import GlobalData from "src/common/GlobalData";
|
|||
import FileEntity from "src/entities/FileEntity";
|
||||
import { HttpProtocol } from "src/entities/HttpProtocol";
|
||||
import NormalResult from "src/entities/NormalResult";
|
||||
import { Protocol } from "src/entities/WSProtocol";
|
||||
|
||||
class _BreadcrumbItem {
|
||||
name: string = "";
|
||||
|
@ -348,6 +410,27 @@ class _BreadcrumbItem {
|
|||
}
|
||||
}
|
||||
|
||||
class _ClipboardType {
|
||||
static kTypeNone = "NONE";
|
||||
static kTypeCopy = "COPY";
|
||||
static kTypeCut = "CUT";
|
||||
|
||||
name: string = "";
|
||||
path: string = "";
|
||||
type: string = _ClipboardType.kTypeNone;
|
||||
is_directory = false;
|
||||
|
||||
get full_path() {
|
||||
return this.path + "/" + this.name;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.name = "";
|
||||
this.path = "";
|
||||
this.type = _ClipboardType.kTypeNone;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "ComponentFileManageDialog",
|
||||
|
||||
|
@ -375,6 +458,8 @@ export default defineComponent({
|
|||
let resolve_value: any = null;
|
||||
const filters: Ref<string[]> = ref([]);
|
||||
|
||||
const clipboard = reactive(new _ClipboardType()); // 剪切板全路径
|
||||
|
||||
const disk_options = ref([
|
||||
{
|
||||
label: $t.t("local disk"),
|
||||
|
@ -412,7 +497,7 @@ export default defineComponent({
|
|||
format: (val: any) => `${val}`,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
/*{
|
||||
name: "can_read",
|
||||
align: "center",
|
||||
label: $t.t("can read"),
|
||||
|
@ -435,7 +520,7 @@ export default defineComponent({
|
|||
field: (val: FileEntity) => val && val.can_exec,
|
||||
format: (val: any) => `${$t.t(val ? "can true" : "can false")}`,
|
||||
sortable: true,
|
||||
},
|
||||
},*/
|
||||
{
|
||||
align: "center",
|
||||
name: "operator",
|
||||
|
@ -629,6 +714,7 @@ export default defineComponent({
|
|||
uploader,
|
||||
disk_options,
|
||||
current_file,
|
||||
clipboard,
|
||||
refresh_file_list,
|
||||
refresh_file_list_async,
|
||||
status,
|
||||
|
@ -642,7 +728,6 @@ export default defineComponent({
|
|||
show_dialog.value = true;
|
||||
},
|
||||
showDialogAsync(in_status: string, filter?: string) {
|
||||
console.log(filter);
|
||||
return new Promise((_resolve, _reject) => {
|
||||
status.value = in_status;
|
||||
parseFilter(filter);
|
||||
|
@ -674,6 +759,7 @@ export default defineComponent({
|
|||
path.value = default_path.value;
|
||||
status.value = "normal";
|
||||
upload_url.value = "";
|
||||
clipboard.clear();
|
||||
if (resolve) {
|
||||
resolve(resolve_value);
|
||||
}
|
||||
|
@ -884,6 +970,103 @@ export default defineComponent({
|
|||
loading.value = false;
|
||||
loading.value = false;
|
||||
},
|
||||
copyFile(file: FileEntity) {
|
||||
if (file) {
|
||||
clipboard.name = file.name;
|
||||
clipboard.path = path.value;
|
||||
clipboard.type = _ClipboardType.kTypeCopy;
|
||||
clipboard.is_directory = file.is_directory;
|
||||
$q.notify({
|
||||
color: "positive",
|
||||
icon: "done",
|
||||
html: true,
|
||||
message:
|
||||
$t.t("copy to colipboard success") +
|
||||
"!" +
|
||||
"<br />" +
|
||||
$t.t(
|
||||
"please use the paste command to paste to another directory"
|
||||
) +
|
||||
"!",
|
||||
position: "top",
|
||||
timeout: 1500,
|
||||
});
|
||||
}
|
||||
},
|
||||
cutFile(file: FileEntity) {
|
||||
if (file) {
|
||||
clipboard.name = file.name;
|
||||
clipboard.path = path.value;
|
||||
clipboard.type = _ClipboardType.kTypeCut;
|
||||
clipboard.is_directory = file.is_directory;
|
||||
}
|
||||
},
|
||||
pasteFile() {
|
||||
if (clipboard && clipboard.type) {
|
||||
let operator_type = "NONE";
|
||||
if (clipboard.type == _ClipboardType.kTypeCopy) {
|
||||
operator_type =
|
||||
Protocol.FileOperatorRequestEntity.kOperatorTypeCopy;
|
||||
} else if (clipboard.type == _ClipboardType.kTypeCut) {
|
||||
operator_type = Protocol.FileOperatorRequestEntity.kOperatorTypeCut;
|
||||
}
|
||||
$q.dialog({
|
||||
title: $t.t("Confirm"),
|
||||
html: true,
|
||||
message:
|
||||
(clipboard.type == _ClipboardType.kTypeCopy
|
||||
? $t.t("_copy2")
|
||||
: $t.t("cut")) +
|
||||
(clipboard.is_directory ? $t.t("directory") : $t.t("file")) +
|
||||
$t.t("from") +
|
||||
' "' +
|
||||
clipboard.full_path +
|
||||
'" ' +
|
||||
$t.t("to") +
|
||||
' "' +
|
||||
(path.value + "/" + clipboard.name) +
|
||||
'"' +
|
||||
"<br/>" +
|
||||
"<span style='color:red;'>" +
|
||||
$t.t(
|
||||
"if the file size is too large, wait for the result patiently. don't refresh the page or perform other operations during the process. otherwise, unexpected consequences may occur"
|
||||
) +
|
||||
"!" +
|
||||
"</span>",
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
let success = false;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const reponse = await GlobalData.getInstance()
|
||||
.getCurrentClient()
|
||||
?.fileOperator(
|
||||
clipboard.full_path,
|
||||
path.value + "/" + clipboard.name,
|
||||
operator_type
|
||||
);
|
||||
success = reponse?.success ?? false;
|
||||
} catch {}
|
||||
loading.value = false;
|
||||
if (success) {
|
||||
refresh_file_list_async();
|
||||
}
|
||||
$q.notify({
|
||||
color: success ? "positive" : "negative",
|
||||
icon: success ? "done" : "warning",
|
||||
message:
|
||||
$t.t("copy") +
|
||||
(clipboard.is_directory ? $t.t("directory") : $t.t("file")) +
|
||||
(success ? $t.t("success") : $t.t("fail")) +
|
||||
"!",
|
||||
position: "top",
|
||||
timeout: 1500,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@
|
|||
</div>
|
||||
</q-item-section>
|
||||
<q-popup-proxy context-menu>
|
||||
<q-popup-proxy context-menu />
|
||||
<q-list>
|
||||
<q-item
|
||||
v-if="
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
</div>
|
||||
</q-item-section>
|
||||
<q-popup-proxy context-menu>
|
||||
<q-popup-proxy context-menu />
|
||||
<q-list>
|
||||
<q-item
|
||||
v-if="
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
</div>
|
||||
</q-item-section>
|
||||
<q-popup-proxy context-menu>
|
||||
<q-popup-proxy context-menu />
|
||||
<q-list>
|
||||
<q-item
|
||||
v-if="
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
} else if (
|
||||
evt.keyCode == 85 &&
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
</div>
|
||||
</q-item-section>
|
||||
<q-popup-proxy context-menu>
|
||||
<q-popup-proxy context-menu />
|
||||
<q-list>
|
||||
<q-item
|
||||
v-if="
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@before-hide="resetData"
|
||||
@keydown="
|
||||
(evt) => {
|
||||
if (evt.keyCode == 27) {
|
||||
if (!loading && evt.keyCode == 27) {
|
||||
show_dialog = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -350,6 +350,9 @@ export namespace Protocol {
|
|||
public static get kRpcSetDevicePowerMode() {
|
||||
return Commands.PROTOCOL_PREFIX + "RpcSetDevicePowerMode";
|
||||
}
|
||||
public static get kRpcFileOperator() {
|
||||
return Commands.PROTOCOL_PREFIX + "RpcFileOperator";
|
||||
}
|
||||
|
||||
static _all_commands = new Set([
|
||||
Commands.kUnKnowCommand,
|
||||
|
@ -437,6 +440,7 @@ export namespace Protocol {
|
|||
Commands.kRpcSetEdgeBlendingInfo,
|
||||
Commands.kSetEdgeBlendingPoint,
|
||||
Commands.kRpcSetDevicePowerMode,
|
||||
Commands.kRpcFileOperator,
|
||||
]);
|
||||
|
||||
public static get AllCommands() {
|
||||
|
@ -2123,4 +2127,44 @@ export namespace Protocol {
|
|||
this.point = point;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileOperatorRequestEntity extends PacketEntity {
|
||||
static kOperatorTypeNone = "NONE";
|
||||
static kOperatorTypeCopy = "COPY";
|
||||
static kOperatorTypeCut = "CUT";
|
||||
|
||||
from_path = "";
|
||||
to_path = "";
|
||||
operator_type = "";
|
||||
force_operator = false;
|
||||
ext_data = "";
|
||||
|
||||
constructor(
|
||||
from_path: string,
|
||||
to_path: string,
|
||||
operator_type: string,
|
||||
force_operator: boolean = true,
|
||||
rpcid?: number
|
||||
) {
|
||||
super();
|
||||
this.timeout = 1000 * 60 * 5;
|
||||
this.rpc_id = rpcid ?? 0;
|
||||
this.command = Commands.kRpcFileOperator;
|
||||
|
||||
this.from_path = from_path ?? "";
|
||||
this.to_path = to_path ?? "";
|
||||
this.operator_type =
|
||||
operator_type ?? FileOperatorRequestEntity.kOperatorTypeNone;
|
||||
this.force_operator = force_operator;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileOperatorResponseEntity extends PacketEntity {
|
||||
success = true;
|
||||
message = "";
|
||||
constructor() {
|
||||
super();
|
||||
this.command = Commands.kRpcFileOperator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -420,4 +420,16 @@ export default {
|
|||
"remember password": "记住密码",
|
||||
"auto login": "自动登录",
|
||||
"back to login page": "返回登录页面",
|
||||
_copy2: "复制",
|
||||
cut: "剪切",
|
||||
paste: "粘贴",
|
||||
"if the file size is too large, wait for the result patiently. don't refresh the page or perform other operations during the process. otherwise, unexpected consequences may occur":
|
||||
"如果文件很大,请耐心等待结果,中途不要刷新页面或做其他操作,以免造成不可预知的严重后果",
|
||||
directory: "目录",
|
||||
file: "文件",
|
||||
from: "从",
|
||||
to: "到",
|
||||
"copy to colipboard success": "成功拷贝到剪切板",
|
||||
"please use the paste command to paste to another directory":
|
||||
"请使用粘贴命令粘贴到其它目录",
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue