diff --git a/README.md b/README.md index 0d2f2bd..46b3612 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,4 @@ TODO - [x] 多样化图标 - [ ] 加密传输 - [x] 取消传输 -- [ ] 多文件发送 \ No newline at end of file +- [x] 多文件发送 \ No newline at end of file diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts index 36d659e..a27e943 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -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(); diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts index 8025005..7b3e840 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -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; } diff --git a/frontend/bindings/mesh-drop/index.ts b/frontend/bindings/mesh-drop/index.ts new file mode 100644 index 0000000..787d33d --- /dev/null +++ b/frontend/bindings/mesh-drop/index.ts @@ -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"; diff --git a/frontend/bindings/mesh-drop/internal/transfer/models.ts b/frontend/bindings/mesh-drop/internal/transfer/models.ts index 83417e2..be4c325 100644 --- a/frontend/bindings/mesh-drop/internal/transfer/models.ts +++ b/frontend/bindings/mesh-drop/internal/transfer/models.ts @@ -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); } diff --git a/frontend/bindings/mesh-drop/internal/transfer/service.ts b/frontend/bindings/mesh-drop/internal/transfer/service.ts index f3bda93..c5c4116 100644 --- a/frontend/bindings/mesh-drop/internal/transfer/service.ts +++ b/frontend/bindings/mesh-drop/internal/transfer/service.ts @@ -17,23 +17,38 @@ export function CancelTransfer(transferID: string): $CancellablePromise { return $Call.ByID(900002248, transferID); } +/** + * CleanTransferList 清理完成的 transfer + */ +export function CleanTransferList(): $CancellablePromise { + return $Call.ByID(3775121017); +} + +export function DeleteTransfer(transferID: string): $CancellablePromise { + return $Call.ByID(4158310409, transferID); +} + export function GetPort(): $CancellablePromise { 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 { + 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 { + return $Call.ByID(3308811582, target, targetIP, filePaths); +} + export function SendFolder(target: discovery$0.Peer | null, targetIP: string, folderPath: string): $CancellablePromise { return $Call.ByID(3258308403, target, targetIP, folderPath); } @@ -58,6 +77,11 @@ export function Start(): $CancellablePromise { return $Call.ByID(3611800535); } +export function StoreTransferToList(transfer: $models.Transfer | null): $CancellablePromise { + 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); diff --git a/frontend/bindings/mesh-drop/models.ts b/frontend/bindings/mesh-drop/models.ts new file mode 100644 index 0000000..0be8b8c --- /dev/null +++ b/frontend/bindings/mesh-drop/models.ts @@ -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 = {}) { + 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); + } +} + +// Private type creation functions +const $$createType0 = $Create.Array($Create.Any); diff --git a/frontend/src/components/MainLayout.vue b/frontend/src/components/MainLayout.vue index 044709d..69a0c96 100644 --- a/frontend/src/components/MainLayout.vue +++ b/frontend/src/components/MainLayout.vue @@ -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([]); const transferList = ref([]); @@ -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) => { + @transferStarted="activeKey = 'transfers'" /> diff --git a/frontend/src/components/PeerCard.vue b/frontend/src/components/PeerCard.vue index 02dc88c..fd381ec 100644 --- a/frontend/src/components/PeerCard.vue +++ b/frontend/src/components/PeerCard.vue @@ -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(); +}; + + +
+ + + +
+ +
+
+ + + + + + +
+ + + Add more files + +
+ + +
+ + + + + + - + diff --git a/frontend/src/components/TransferItem.vue b/frontend/src/components/TransferItem.vue index db83bf6..6f118fd 100644 --- a/frontend/src/components/TransferItem.vue +++ b/frontend/src/components/TransferItem.vue @@ -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; +});