add: send files
This commit is contained in:
@@ -6,18 +6,23 @@
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as main$0 from "../../../../../mesh-drop/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as discovery$0 from "../../../../../mesh-drop/internal/discovery/models.js";
|
||||
|
||||
function configure() {
|
||||
Object.freeze(Object.assign($Create.Events, {
|
||||
"peers:update": $$createType1,
|
||||
"files-dropped": $$createType0,
|
||||
"peers:update": $$createType2,
|
||||
}));
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = discovery$0.Peer.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType0 = main$0.FilesDroppedEvent.createFrom;
|
||||
const $$createType1 = discovery$0.Peer.createFrom;
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
configure();
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
// @ts-ignore: Unused imports
|
||||
import type { Events } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import type * as main$0 from "../../../../../mesh-drop/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import type * as discovery$0 from "../../../../../mesh-drop/internal/discovery/models.js";
|
||||
@@ -12,6 +15,7 @@ import type * as discovery$0 from "../../../../../mesh-drop/internal/discovery/m
|
||||
declare module "@wailsio/runtime" {
|
||||
namespace Events {
|
||||
interface CustomEvents {
|
||||
"files-dropped": main$0.FilesDroppedEvent;
|
||||
"peers:update": discovery$0.Peer[];
|
||||
"transfer:refreshList": void;
|
||||
}
|
||||
|
||||
6
frontend/bindings/mesh-drop/index.ts
Normal file
6
frontend/bindings/mesh-drop/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
FilesDroppedEvent
|
||||
} from "./models.js";
|
||||
@@ -100,6 +100,11 @@ export class Transfer {
|
||||
*/
|
||||
"id": string;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
"create_time": number;
|
||||
|
||||
/**
|
||||
* 发送者
|
||||
*/
|
||||
@@ -160,6 +165,9 @@ export class Transfer {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("create_time" in $$source)) {
|
||||
this["create_time"] = 0;
|
||||
}
|
||||
if (!("sender" in $$source)) {
|
||||
this["sender"] = (new Sender());
|
||||
}
|
||||
@@ -201,14 +209,14 @@ export class Transfer {
|
||||
* Creates a new Transfer instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Transfer {
|
||||
const $$createField1_0 = $$createType0;
|
||||
const $$createField6_0 = $$createType1;
|
||||
const $$createField2_0 = $$createType0;
|
||||
const $$createField7_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("sender" in $$parsedSource) {
|
||||
$$parsedSource["sender"] = $$createField1_0($$parsedSource["sender"]);
|
||||
$$parsedSource["sender"] = $$createField2_0($$parsedSource["sender"]);
|
||||
}
|
||||
if ("progress" in $$parsedSource) {
|
||||
$$parsedSource["progress"] = $$createField6_0($$parsedSource["progress"]);
|
||||
$$parsedSource["progress"] = $$createField7_0($$parsedSource["progress"]);
|
||||
}
|
||||
return new Transfer($$parsedSource as Partial<Transfer>);
|
||||
}
|
||||
|
||||
@@ -17,23 +17,38 @@ export function CancelTransfer(transferID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(900002248, transferID);
|
||||
}
|
||||
|
||||
/**
|
||||
* CleanTransferList 清理完成的 transfer
|
||||
*/
|
||||
export function CleanTransferList(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3775121017);
|
||||
}
|
||||
|
||||
export function DeleteTransfer(transferID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(4158310409, transferID);
|
||||
}
|
||||
|
||||
export function GetPort(): $CancellablePromise<number> {
|
||||
return $Call.ByID(4195335736);
|
||||
}
|
||||
|
||||
export function GetTransfer(transferID: string): $CancellablePromise<[$models.Transfer, boolean]> {
|
||||
export function GetTransfer(transferID: string): $CancellablePromise<[$models.Transfer | null, boolean]> {
|
||||
return $Call.ByID(1198637268, transferID).then(($result: any) => {
|
||||
$result[0] = $$createType0($result[0]);
|
||||
$result[0] = $$createType1($result[0]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
export function GetTransferList(): $CancellablePromise<$models.Transfer[]> {
|
||||
export function GetTransferList(): $CancellablePromise<($models.Transfer | null)[]> {
|
||||
return $Call.ByID(584162076).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
return $$createType2($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function NotifyTransferListUpdate(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1220032142);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolvePendingRequest 外部调用,解决待处理的传输请求
|
||||
* 返回 true 表示成功处理,false 表示未找到该 ID 的请求
|
||||
@@ -46,6 +61,10 @@ export function SendFile(target: discovery$0.Peer | null, targetIP: string, file
|
||||
return $Call.ByID(2954589433, target, targetIP, filePath);
|
||||
}
|
||||
|
||||
export function SendFiles(target: discovery$0.Peer | null, targetIP: string, filePaths: string[]): $CancellablePromise<void> {
|
||||
return $Call.ByID(3308811582, target, targetIP, filePaths);
|
||||
}
|
||||
|
||||
export function SendFolder(target: discovery$0.Peer | null, targetIP: string, folderPath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3258308403, target, targetIP, folderPath);
|
||||
}
|
||||
@@ -58,6 +77,11 @@ export function Start(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3611800535);
|
||||
}
|
||||
|
||||
export function StoreTransferToList(transfer: $models.Transfer | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(3225941780, transfer);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.Transfer.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
38
frontend/bindings/mesh-drop/models.ts
Normal file
38
frontend/bindings/mesh-drop/models.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
export class FilesDroppedEvent {
|
||||
"files": string[];
|
||||
"target": string;
|
||||
|
||||
/** Creates a new FilesDroppedEvent instance. */
|
||||
constructor($$source: Partial<FilesDroppedEvent> = {}) {
|
||||
if (!("files" in $$source)) {
|
||||
this["files"] = [];
|
||||
}
|
||||
if (!("target" in $$source)) {
|
||||
this["target"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new FilesDroppedEvent instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): FilesDroppedEvent {
|
||||
const $$createField0_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("files" in $$parsedSource) {
|
||||
$$parsedSource["files"] = $$createField0_0($$parsedSource["files"]);
|
||||
}
|
||||
return new FilesDroppedEvent($$parsedSource as Partial<FilesDroppedEvent>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
@@ -16,10 +16,6 @@ import {
|
||||
NBadge,
|
||||
NButton,
|
||||
NIcon,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
useDialog,
|
||||
NInput,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
@@ -31,18 +27,9 @@ import {
|
||||
import { type MenuOption } from "naive-ui";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import {
|
||||
GetPeers,
|
||||
GetPeerByIP,
|
||||
} from "../../bindings/mesh-drop/internal/discovery/service";
|
||||
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import {
|
||||
GetTransferList,
|
||||
SendFile,
|
||||
SendText,
|
||||
SendFolder,
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
|
||||
const peers = ref<Peer[]>([]);
|
||||
const transferList = ref<Transfer[]>([]);
|
||||
@@ -54,7 +41,10 @@ const isMobile = ref(false);
|
||||
onMounted(async () => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
transferList.value = await GetTransferList();
|
||||
const list = await GetTransferList();
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
});
|
||||
|
||||
const checkMobile = () => {
|
||||
@@ -110,7 +100,10 @@ Events.On("peers:update", (event) => {
|
||||
});
|
||||
|
||||
Events.On("transfer:refreshList", async () => {
|
||||
transferList.value = await GetTransferList();
|
||||
const list = await GetTransferList();
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
});
|
||||
|
||||
// --- 计算属性 ---
|
||||
@@ -122,83 +115,6 @@ const pendingCount = computed(() => {
|
||||
|
||||
// --- 操作 ---
|
||||
|
||||
const handleSendFile = async (ip: string) => {
|
||||
try {
|
||||
const filePath = await Dialogs.OpenFile({
|
||||
Title: "Select file to send",
|
||||
});
|
||||
if (!filePath) return;
|
||||
const peer = await GetPeerByIP(ip);
|
||||
if (!peer) return;
|
||||
activeKey.value = "transfers";
|
||||
await SendFile(peer, ip, filePath);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
alert("Failed to send file: " + e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendFolder = async (ip: string) => {
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
Title: "Select folder to send",
|
||||
CanChooseDirectories: true,
|
||||
CanChooseFiles: false,
|
||||
AllowsMultipleSelection: false,
|
||||
};
|
||||
const folderPath = await Dialogs.OpenFile(opts);
|
||||
if (!folderPath) return;
|
||||
const peer = await GetPeerByIP(ip);
|
||||
if (!peer) return;
|
||||
activeKey.value = "transfers";
|
||||
await SendFolder(peer, ip, folderPath as string);
|
||||
};
|
||||
|
||||
const dialog = useDialog();
|
||||
const handleSendText = (ip: string) => {
|
||||
const textContent = ref("");
|
||||
const d = dialog.create({
|
||||
title: "Send Text",
|
||||
content: () =>
|
||||
h(NInput, {
|
||||
value: textContent.value,
|
||||
"onUpdate:value": (v) => (textContent.value = v),
|
||||
type: "textarea",
|
||||
placeholder: "Type something to send...",
|
||||
autosize: { minRows: 3, maxRows: 8 },
|
||||
}),
|
||||
positiveText: "Send",
|
||||
negativeText: "Cancel",
|
||||
onPositiveClick: async () => {
|
||||
if (!textContent.value) return;
|
||||
try {
|
||||
const peer = await GetPeerByIP(ip);
|
||||
if (!peer) return;
|
||||
activeKey.value = "transfers";
|
||||
await SendText(peer, ip, textContent.value);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
alert("Failed to send text: " + e);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendClipboard = async (ip: string) => {
|
||||
const text = await Clipboard.Text();
|
||||
if (!text) {
|
||||
alert("Clipboard is empty");
|
||||
return;
|
||||
}
|
||||
const peer = await GetPeerByIP(ip);
|
||||
if (!peer) return;
|
||||
activeKey.value = "transfers";
|
||||
await SendText(peer, ip, text);
|
||||
};
|
||||
|
||||
const removeTransfer = (id: string) => {
|
||||
transferList.value = transferList.value.filter((t) => t.id !== id);
|
||||
};
|
||||
|
||||
const handleMenuUpdate = (key: string) => {
|
||||
activeKey.value = key;
|
||||
showMobileMenu.value = false;
|
||||
@@ -266,10 +182,7 @@ const handleMenuUpdate = (key: string) => {
|
||||
<n-gi v-for="peer in peers" :key="peer.id">
|
||||
<PeerCard
|
||||
:peer="peer"
|
||||
@sendFile="handleSendFile"
|
||||
@sendFolder="handleSendFolder"
|
||||
@sendText="handleSendText"
|
||||
@sendClipboard="handleSendClipboard" />
|
||||
@transferStarted="activeKey = 'transfers'" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-space>
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
NDropdown,
|
||||
NSelect,
|
||||
type DropdownOption,
|
||||
NModal,
|
||||
NList,
|
||||
NListItem,
|
||||
NThing,
|
||||
NEmpty,
|
||||
NInput,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
@@ -25,18 +31,24 @@ import {
|
||||
faFolder,
|
||||
faFont,
|
||||
faClipboard,
|
||||
faTrash,
|
||||
faPlus,
|
||||
faCloudArrowUp,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
import { Dialogs, Events, Clipboard } from "@wailsio/runtime";
|
||||
import {
|
||||
SendFiles,
|
||||
SendFolder,
|
||||
SendText,
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
|
||||
const props = defineProps<{
|
||||
peer: Peer;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "sendFile", ip: string): void;
|
||||
(e: "sendFolder", ip: string): void;
|
||||
(e: "sendText", ip: string): void;
|
||||
(e: "sendClipboard", ip: string): void;
|
||||
(e: "transferStarted"): void;
|
||||
}>();
|
||||
|
||||
const ips = computed(() => {
|
||||
@@ -82,8 +94,8 @@ const osIcon = computed(() => {
|
||||
|
||||
const sendOptions: DropdownOption[] = [
|
||||
{
|
||||
label: "Send File",
|
||||
key: "file",
|
||||
label: "Send Files",
|
||||
key: "files",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFile }) }),
|
||||
},
|
||||
@@ -113,20 +125,133 @@ const handleAction = (key: string) => {
|
||||
if (!selectedIp.value) return;
|
||||
|
||||
switch (key) {
|
||||
case "file":
|
||||
emit("sendFile", selectedIp.value);
|
||||
case "files":
|
||||
showFileModal.value = true;
|
||||
break;
|
||||
case "folder":
|
||||
emit("sendFolder", selectedIp.value);
|
||||
handleSendFolder();
|
||||
break;
|
||||
case "text":
|
||||
emit("sendText", selectedIp.value);
|
||||
showTextModal.value = true;
|
||||
break;
|
||||
case "clipboard":
|
||||
emit("sendClipboard", selectedIp.value);
|
||||
handleSendClipboard();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 发送逻辑 ---
|
||||
|
||||
const handleSendFolder = async () => {
|
||||
if (!selectedIp.value) return;
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
Title: "Select folder to send",
|
||||
CanChooseDirectories: true,
|
||||
CanChooseFiles: false,
|
||||
AllowsMultipleSelection: false,
|
||||
};
|
||||
const folderPath = await Dialogs.OpenFile(opts);
|
||||
if (!folderPath) return;
|
||||
|
||||
SendFolder(props.peer, selectedIp.value, folderPath as string).catch((e) => {
|
||||
console.error(e);
|
||||
alert("Failed to send folder: " + e);
|
||||
});
|
||||
emit("transferStarted");
|
||||
};
|
||||
|
||||
const handleSendClipboard = async () => {
|
||||
if (!selectedIp.value) return;
|
||||
const text = await Clipboard.Text();
|
||||
if (!text) {
|
||||
alert("Clipboard is empty");
|
||||
return;
|
||||
}
|
||||
SendText(props.peer, selectedIp.value, text).catch((e) => {
|
||||
console.error(e);
|
||||
alert("Failed to send clipboard: " + e);
|
||||
});
|
||||
emit("transferStarted");
|
||||
};
|
||||
|
||||
// --- 文本发送 ---
|
||||
const showTextModal = ref(false);
|
||||
const textContent = ref("");
|
||||
|
||||
const executeSendText = async () => {
|
||||
if (!selectedIp.value || !textContent.value) return;
|
||||
SendText(props.peer, selectedIp.value, textContent.value).catch((e) => {
|
||||
console.error(e);
|
||||
alert("Failed to send text: " + e);
|
||||
});
|
||||
emit("transferStarted");
|
||||
showTextModal.value = false;
|
||||
textContent.value = "";
|
||||
};
|
||||
|
||||
// --- 文件选择 ---
|
||||
const showFileModal = ref(false);
|
||||
watch(showFileModal, (newVal) => {
|
||||
if (newVal) {
|
||||
Events.On("files-dropped", (event) => {
|
||||
fileList.value = event.data.files.map((f) => ({
|
||||
name: f.split(/[\/]/).pop() || f,
|
||||
path: f,
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
Events.Off("files-dropped");
|
||||
}
|
||||
});
|
||||
const fileList = ref<{ name: string; path: string }[]>([]);
|
||||
|
||||
const openFileDialog = async () => {
|
||||
const files = await Dialogs.OpenFile({
|
||||
Title: "Select files to send",
|
||||
AllowsMultipleSelection: true,
|
||||
});
|
||||
if (files) {
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach((f) => {
|
||||
// 去重
|
||||
if (!fileList.value.find((existing) => existing.path === f)) {
|
||||
fileList.value.push({
|
||||
name: f.split(/[\\/]/).pop() || f,
|
||||
path: f,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const f = files as string;
|
||||
if (!fileList.value.find((existing) => existing.path === f)) {
|
||||
fileList.value.push({
|
||||
name: f.split(/[\\/]/).pop() || f,
|
||||
path: f,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
fileList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleCancelFiles = () => {
|
||||
showFileModal.value = false;
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
const handleSendFiles = () => {
|
||||
if (fileList.value.length === 0 || !selectedIp.value) return;
|
||||
const paths = fileList.value.map((f) => f.path);
|
||||
SendFiles(props.peer, selectedIp.value, paths).catch((e) => {
|
||||
console.error(e);
|
||||
alert("Failed to send files: " + e);
|
||||
});
|
||||
emit("transferStarted");
|
||||
handleCancelFiles();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -189,6 +314,107 @@ const handleAction = (key: string) => {
|
||||
</div>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showFileModal"
|
||||
preset="card"
|
||||
title="Send Files"
|
||||
style="width: 600px; max-width: 90%"
|
||||
:bordered="false">
|
||||
<div
|
||||
v-if="fileList.length === 0"
|
||||
class="drop-zone"
|
||||
@click="openFileDialog"
|
||||
data-file-drop-target>
|
||||
<n-empty description="Click to select files">
|
||||
<template #icon>
|
||||
<n-icon :size="48">
|
||||
<FontAwesomeIcon :icon="faCloudArrowUp" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div
|
||||
style="max-height: 400px; overflow-y: auto; margin-bottom: 16px"
|
||||
data-file-drop-target>
|
||||
<n-list bordered>
|
||||
<n-list-item v-for="(file, index) in fileList" :key="file.path">
|
||||
<template #suffix>
|
||||
<n-button text type="error" @click="handleRemoveFile(index)">
|
||||
<template #icon>
|
||||
<n-icon><FontAwesomeIcon :icon="faTrash" /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-thing :title="file.name" :description="file.path"></n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
<n-button dashed block @click="openFileDialog">
|
||||
<template #icon>
|
||||
<n-icon><FontAwesomeIcon :icon="faPlus" /></n-icon>
|
||||
</template>
|
||||
Add more files
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="handleCancelFiles">Cancel</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleSendFiles"
|
||||
:disabled="fileList.length === 0">
|
||||
Send {{ fileList.length > 0 ? `(${fileList.length})` : "" }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 文本发送 Modal -->
|
||||
<n-modal
|
||||
v-model:show="showTextModal"
|
||||
preset="card"
|
||||
title="Send Text"
|
||||
style="width: 500px; max-width: 90%"
|
||||
:bordered="false">
|
||||
<n-input
|
||||
v-model:value="textContent"
|
||||
type="textarea"
|
||||
placeholder="Type something to send..."
|
||||
:autosize="{ minRows: 4, maxRows: 10 }" />
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showTextModal = false">Cancel</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="executeSendText"
|
||||
:disabled="!textContent">
|
||||
Send
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.drop-zone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.drop-zone:hover {
|
||||
border-color: #38bdf8;
|
||||
background-color: rgba(56, 189, 248, 0.05);
|
||||
}
|
||||
|
||||
.drop-zone.file-drop-target-active {
|
||||
border-color: #38bdf8;
|
||||
background-color: rgba(56, 189, 248, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
NTag,
|
||||
useMessage,
|
||||
NInput,
|
||||
NDropdown,
|
||||
NButtonGroup,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
@@ -20,12 +22,21 @@ import {
|
||||
faFile,
|
||||
faFileLines,
|
||||
faFolder,
|
||||
faClock,
|
||||
faChevronDown,
|
||||
faEye,
|
||||
faCopy,
|
||||
faTrash,
|
||||
faXmark,
|
||||
faStop,
|
||||
faCheck,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import {
|
||||
ResolvePendingRequest,
|
||||
CancelTransfer,
|
||||
DeleteTransfer,
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
@@ -49,6 +60,10 @@ const formatSpeed = (speed?: number) => {
|
||||
return formatSize(speed) + "/s";
|
||||
};
|
||||
|
||||
const formatTime = (time: number): string => {
|
||||
return new Date(time).toLocaleString();
|
||||
};
|
||||
|
||||
const percentage = computed(() =>
|
||||
Math.min(
|
||||
100,
|
||||
@@ -84,6 +99,23 @@ const acceptToFolder = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const dropdownOptions = [
|
||||
{
|
||||
label: "Accept To Folder",
|
||||
key: "folder",
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = (key: string | number) => {
|
||||
if (key === "folder") {
|
||||
acceptToFolder();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
DeleteTransfer(props.transfer.id);
|
||||
};
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const handleCopy = async () => {
|
||||
@@ -130,6 +162,27 @@ const canCancel = computed(() => {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const canCopy = computed(() => {
|
||||
if (
|
||||
props.transfer.type === "receive" &&
|
||||
props.transfer.status === "completed" &&
|
||||
props.transfer.content_type === "text"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const canAccept = computed(() => {
|
||||
if (
|
||||
props.transfer.type === "receive" &&
|
||||
props.transfer.status === "pending"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -183,7 +236,9 @@ const canCancel = computed(() => {
|
||||
<n-tag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
v-if="props.transfer.sender.name">
|
||||
v-if="
|
||||
props.transfer.sender.name && props.transfer.type === 'receive'
|
||||
">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faUser" />
|
||||
@@ -191,6 +246,17 @@ const canCancel = computed(() => {
|
||||
</template>
|
||||
{{ props.transfer.sender.name }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
v-if="props.transfer.create_time">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faClock" />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ formatTime(props.transfer.create_time) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="meta-line">
|
||||
@@ -218,7 +284,7 @@ const canCancel = computed(() => {
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'canceled'"
|
||||
type="error">
|
||||
type="info">
|
||||
- Canceled</n-text
|
||||
>
|
||||
<n-text
|
||||
@@ -227,19 +293,15 @@ const canCancel = computed(() => {
|
||||
type="error">
|
||||
- Rejected</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'pending'"
|
||||
type="warning">
|
||||
- Waiting for accept</n-text
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 文字内容 -->
|
||||
<n-text
|
||||
v-if="
|
||||
props.transfer.type === 'send' &&
|
||||
props.transfer.status === 'pending'
|
||||
"
|
||||
depth="3"
|
||||
>Waiting for accept</n-text
|
||||
>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<n-progress
|
||||
v-if="props.transfer.status === 'active'"
|
||||
@@ -252,58 +314,83 @@ const canCancel = computed(() => {
|
||||
style="margin-top: 4px" />
|
||||
</div>
|
||||
|
||||
<!-- 接受/拒绝操作按钮 -->
|
||||
<div
|
||||
class="actions-wrapper"
|
||||
v-if="
|
||||
props.transfer.type === 'receive' &&
|
||||
props.transfer.status === 'pending'
|
||||
">
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions-wrapper">
|
||||
<n-space>
|
||||
<n-button size="small" type="success" @click="acceptTransfer">
|
||||
Accept
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="props.transfer.content_type !== 'text'"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="acceptToFolder">
|
||||
Accept To Folder
|
||||
</n-button>
|
||||
<n-button size="small" type="error" ghost @click="rejectTransfer">
|
||||
Reject
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-button-group size="small">
|
||||
<n-button v-if="canAccept" type="success" @click="acceptTransfer">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faCheck" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropdownOptions"
|
||||
@select="handleSelect"
|
||||
v-if="canAccept && props.transfer.content_type !== 'text'">
|
||||
<n-button type="success">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faChevronDown" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<n-button
|
||||
v-if="canAccept"
|
||||
size="small"
|
||||
type="error"
|
||||
@click="rejectTransfer">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faXmark" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
|
||||
<!-- 文本传输按钮 -->
|
||||
<div
|
||||
class="actions-wrapper"
|
||||
v-if="
|
||||
props.transfer.type === 'receive' &&
|
||||
props.transfer.status === 'completed' &&
|
||||
props.transfer.content_type === 'text'
|
||||
">
|
||||
<n-space>
|
||||
<n-button size="small" type="success" @click="handleOpen"
|
||||
>Open</n-button
|
||||
>
|
||||
<n-button size="small" type="success" @click="handleCopy"
|
||||
>Copy</n-button
|
||||
>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 取消按钮 -->
|
||||
<div class="actions-wrapper" v-if="canCancel">
|
||||
<n-space>
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
ghost
|
||||
@click="CancelTransfer(props.transfer.id)"
|
||||
>Cancel</n-button
|
||||
>
|
||||
<n-button type="success" @click="handleOpen" v-if="canCopy"
|
||||
><template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faEye" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button type="success" @click="handleCopy" v-if="canCopy"
|
||||
><template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faCopy" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
type="success"
|
||||
@click="handleDelete"
|
||||
v-if="
|
||||
props.transfer.status === 'completed' ||
|
||||
props.transfer.status === 'error' ||
|
||||
props.transfer.status === 'canceled' ||
|
||||
props.transfer.status === 'rejected'
|
||||
">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faTrash" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="canCancel"
|
||||
size="small"
|
||||
type="error"
|
||||
@click="CancelTransfer(props.transfer.id)"
|
||||
><template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faStop" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,6 +406,7 @@ const canCancel = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
@@ -350,4 +438,17 @@ const canCancel = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.actions-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.transfer-row {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user