init
This commit is contained in:
93
frontend/Inter Font License.txt
Normal file
93
frontend/Inter Font License.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
18
frontend/README.md
Normal file
18
frontend/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
@@ -0,0 +1,23 @@
|
||||
//@ts-check
|
||||
// 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";
|
||||
|
||||
// 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,
|
||||
}));
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = discovery$0.Peer.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
|
||||
configure();
|
||||
19
frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
19
frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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 type { Events } from "@wailsio/runtime";
|
||||
|
||||
// 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";
|
||||
|
||||
declare module "@wailsio/runtime" {
|
||||
namespace Events {
|
||||
interface CustomEvents {
|
||||
"peers:update": discovery$0.Peer[];
|
||||
"transfer:refreshList": void;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
frontend/bindings/mesh-drop/internal/discovery/index.ts
Normal file
13
frontend/bindings/mesh-drop/internal/discovery/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export {
|
||||
OS,
|
||||
Peer,
|
||||
RouteState
|
||||
} from "./models.js";
|
||||
128
frontend/bindings/mesh-drop/internal/discovery/models.ts
Normal file
128
frontend/bindings/mesh-drop/internal/discovery/models.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
export enum OS {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
OSLinux = "linux",
|
||||
OSWindows = "windows",
|
||||
OSMac = "darwin",
|
||||
};
|
||||
|
||||
/**
|
||||
* Peer 代表一个可达的网络端点 (Network Endpoint)。
|
||||
* 注意:一个物理设备 (Device) 可能通过多个网络接口广播,因此会对应多个 Peer 结构体。
|
||||
*/
|
||||
export class Peer {
|
||||
/**
|
||||
* ID 是物理设备的全局唯一标识 (UUID/MachineID)。
|
||||
* 具有相同 ID 的 Peer 属于同一台物理设备。
|
||||
*/
|
||||
"id": string;
|
||||
|
||||
/**
|
||||
* Name 是设备的主机名或用户设置的显示名称 (如 "Nite's Arch")。
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* Routes 记录了设备的 IP 地址和状态。
|
||||
* Key: ip, Value: *RouteState
|
||||
*/
|
||||
"routes": { [_: string]: RouteState | null };
|
||||
|
||||
/**
|
||||
* Port 是文件传输服务的监听端口。
|
||||
*/
|
||||
"port": number;
|
||||
|
||||
/**
|
||||
* IsOnline 标记该端点当前是否活跃 (UI 渲染用)。
|
||||
*/
|
||||
"is_online": boolean;
|
||||
"os": OS;
|
||||
|
||||
/** Creates a new Peer instance. */
|
||||
constructor($$source: Partial<Peer> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("routes" in $$source)) {
|
||||
this["routes"] = {};
|
||||
}
|
||||
if (!("port" in $$source)) {
|
||||
this["port"] = 0;
|
||||
}
|
||||
if (!("is_online" in $$source)) {
|
||||
this["is_online"] = false;
|
||||
}
|
||||
if (!("os" in $$source)) {
|
||||
this["os"] = OS.$zero;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Peer instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Peer {
|
||||
const $$createField2_0 = $$createType2;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("routes" in $$parsedSource) {
|
||||
$$parsedSource["routes"] = $$createField2_0($$parsedSource["routes"]);
|
||||
}
|
||||
return new Peer($$parsedSource as Partial<Peer>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RouteState 记录单条路径的状态
|
||||
*/
|
||||
export class RouteState {
|
||||
"ip": string;
|
||||
|
||||
/**
|
||||
* 该特定 IP 最后一次响应的时间
|
||||
*/
|
||||
"last_seen": time$0.Time;
|
||||
|
||||
/** Creates a new RouteState instance. */
|
||||
constructor($$source: Partial<RouteState> = {}) {
|
||||
if (!("ip" in $$source)) {
|
||||
this["ip"] = "";
|
||||
}
|
||||
if (!("last_seen" in $$source)) {
|
||||
this["last_seen"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RouteState instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): RouteState {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new RouteState($$parsedSource as Partial<RouteState>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = RouteState.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $$createType1);
|
||||
39
frontend/bindings/mesh-drop/internal/discovery/service.ts
Normal file
39
frontend/bindings/mesh-drop/internal/discovery/service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function GetID(): $CancellablePromise<string> {
|
||||
return $Call.ByID(1539451205);
|
||||
}
|
||||
|
||||
export function GetName(): $CancellablePromise<string> {
|
||||
return $Call.ByID(1578367131);
|
||||
}
|
||||
|
||||
export function GetPeerByIP(ip: string): $CancellablePromise<$models.Peer | null> {
|
||||
return $Call.ByID(1626825408, ip).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetPeers(): $CancellablePromise<$models.Peer[]> {
|
||||
return $Call.ByID(3041084029).then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function Start(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1014177536);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.Peer.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType0);
|
||||
16
frontend/bindings/mesh-drop/internal/transfer/index.ts
Normal file
16
frontend/bindings/mesh-drop/internal/transfer/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export {
|
||||
ContentType,
|
||||
Progress,
|
||||
Sender,
|
||||
Transfer,
|
||||
TransferStatus,
|
||||
TransferType
|
||||
} from "./models.js";
|
||||
244
frontend/bindings/mesh-drop/internal/transfer/models.ts
Normal file
244
frontend/bindings/mesh-drop/internal/transfer/models.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
// 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 enum ContentType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
ContentTypeFile = "file",
|
||||
ContentTypeText = "text",
|
||||
ContentTypeFolder = "folder",
|
||||
};
|
||||
|
||||
/**
|
||||
* Progress 用户前端传输进度
|
||||
*/
|
||||
export class Progress {
|
||||
/**
|
||||
* 当前进度
|
||||
*/
|
||||
"current": number;
|
||||
|
||||
/**
|
||||
* 总进度
|
||||
*/
|
||||
"total": number;
|
||||
|
||||
/**
|
||||
* 速度
|
||||
*/
|
||||
"speed": number;
|
||||
|
||||
/** Creates a new Progress instance. */
|
||||
constructor($$source: Partial<Progress> = {}) {
|
||||
if (!("current" in $$source)) {
|
||||
this["current"] = 0;
|
||||
}
|
||||
if (!("total" in $$source)) {
|
||||
this["total"] = 0;
|
||||
}
|
||||
if (!("speed" in $$source)) {
|
||||
this["speed"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Progress instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Progress {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Progress($$parsedSource as Partial<Progress>);
|
||||
}
|
||||
}
|
||||
|
||||
export class Sender {
|
||||
/**
|
||||
* 发送者 ID
|
||||
*/
|
||||
"id": string;
|
||||
|
||||
/**
|
||||
* 发送者名称
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/** Creates a new Sender instance. */
|
||||
constructor($$source: Partial<Sender> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Sender instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Sender {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Sender($$parsedSource as Partial<Sender>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer
|
||||
*/
|
||||
export class Transfer {
|
||||
/**
|
||||
* 传输会话 ID
|
||||
*/
|
||||
"id": string;
|
||||
|
||||
/**
|
||||
* 发送者
|
||||
*/
|
||||
"sender": Sender;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
"file_name": string;
|
||||
|
||||
/**
|
||||
* 文件大小 (字节)
|
||||
*/
|
||||
"file_size": number;
|
||||
|
||||
/**
|
||||
* 保存路径
|
||||
*/
|
||||
"savePath": string;
|
||||
|
||||
/**
|
||||
* 传输状态
|
||||
*/
|
||||
"status": TransferStatus;
|
||||
|
||||
/**
|
||||
* 传输进度
|
||||
*/
|
||||
"progress": Progress;
|
||||
|
||||
/**
|
||||
* 进度类型
|
||||
*/
|
||||
"type": TransferType;
|
||||
|
||||
/**
|
||||
* 内容类型
|
||||
*/
|
||||
"content_type": ContentType;
|
||||
|
||||
/**
|
||||
* 文本内容
|
||||
*/
|
||||
"text": string;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
"error_msg": string;
|
||||
|
||||
/**
|
||||
* 用于上传的凭证
|
||||
*/
|
||||
"token": string;
|
||||
|
||||
/** Creates a new Transfer instance. */
|
||||
constructor($$source: Partial<Transfer> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("sender" in $$source)) {
|
||||
this["sender"] = (new Sender());
|
||||
}
|
||||
if (!("file_name" in $$source)) {
|
||||
this["file_name"] = "";
|
||||
}
|
||||
if (!("file_size" in $$source)) {
|
||||
this["file_size"] = 0;
|
||||
}
|
||||
if (!("savePath" in $$source)) {
|
||||
this["savePath"] = "";
|
||||
}
|
||||
if (!("status" in $$source)) {
|
||||
this["status"] = TransferStatus.$zero;
|
||||
}
|
||||
if (!("progress" in $$source)) {
|
||||
this["progress"] = (new Progress());
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = TransferType.$zero;
|
||||
}
|
||||
if (!("content_type" in $$source)) {
|
||||
this["content_type"] = ContentType.$zero;
|
||||
}
|
||||
if (!("text" in $$source)) {
|
||||
this["text"] = "";
|
||||
}
|
||||
if (!("error_msg" in $$source)) {
|
||||
this["error_msg"] = "";
|
||||
}
|
||||
if (!("token" in $$source)) {
|
||||
this["token"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Transfer instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Transfer {
|
||||
const $$createField1_0 = $$createType0;
|
||||
const $$createField6_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("sender" in $$parsedSource) {
|
||||
$$parsedSource["sender"] = $$createField1_0($$parsedSource["sender"]);
|
||||
}
|
||||
if ("progress" in $$parsedSource) {
|
||||
$$parsedSource["progress"] = $$createField6_0($$parsedSource["progress"]);
|
||||
}
|
||||
return new Transfer($$parsedSource as Partial<Transfer>);
|
||||
}
|
||||
}
|
||||
|
||||
export enum TransferStatus {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
TransferStatusPending = "pending",
|
||||
TransferStatusAccepted = "accepted",
|
||||
TransferStatusRejected = "rejected",
|
||||
TransferStatusCompleted = "completed",
|
||||
TransferStatusError = "error",
|
||||
TransferStatusCanceled = "canceled",
|
||||
TransferStatusActive = "active",
|
||||
};
|
||||
|
||||
export enum TransferType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
TransferTypeSend = "send",
|
||||
TransferTypeReceive = "receive",
|
||||
};
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = Sender.createFrom;
|
||||
const $$createType1 = Progress.createFrom;
|
||||
48
frontend/bindings/mesh-drop/internal/transfer/service.ts
Normal file
48
frontend/bindings/mesh-drop/internal/transfer/service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as discovery$0 from "../discovery/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function GetPort(): $CancellablePromise<number> {
|
||||
return $Call.ByID(4195335736);
|
||||
}
|
||||
|
||||
export function GetTransferList(): $CancellablePromise<$models.Transfer[]> {
|
||||
return $Call.ByID(584162076).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolvePendingRequest 外部调用,解决待处理的传输请求
|
||||
* 返回 true 表示成功处理,false 表示未找到该 ID 的请求
|
||||
*/
|
||||
export function ResolvePendingRequest(id: string, accept: boolean, savePath: string): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(207902967, id, accept, savePath);
|
||||
}
|
||||
|
||||
export function SendFile(target: discovery$0.Peer | null, targetIP: string, filePath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2954589433, target, targetIP, filePath);
|
||||
}
|
||||
|
||||
export function SendText(target: discovery$0.Peer | null, targetIP: string, text: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1497421440, target, targetIP, text);
|
||||
}
|
||||
|
||||
export function Start(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3611800535);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.Transfer.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
6
frontend/bindings/time/index.ts
Normal file
6
frontend/bindings/time/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 type {
|
||||
Time
|
||||
} from "./models.js";
|
||||
51
frontend/bindings/time/models.ts
Normal file
51
frontend/bindings/time/models.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* A Time represents an instant in time with nanosecond precision.
|
||||
*
|
||||
* Programs using times should typically store and pass them as values,
|
||||
* not pointers. That is, time variables and struct fields should be of
|
||||
* type [time.Time], not *time.Time.
|
||||
*
|
||||
* A Time value can be used by multiple goroutines simultaneously except
|
||||
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
|
||||
* [Time.UnmarshalText] are not concurrency-safe.
|
||||
*
|
||||
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
|
||||
* The [Time.Sub] method subtracts two instants, producing a [Duration].
|
||||
* The [Time.Add] method adds a Time and a Duration, producing a Time.
|
||||
*
|
||||
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
|
||||
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
|
||||
* a simple way of detecting a time that has not been initialized explicitly.
|
||||
*
|
||||
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
|
||||
* Time with a specific Location. Changing the Location of a Time value with
|
||||
* these methods does not change the actual instant it represents, only the time
|
||||
* zone in which to interpret it.
|
||||
*
|
||||
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
|
||||
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
|
||||
* but not the location name. They therefore lose information about Daylight Saving Time.
|
||||
*
|
||||
* In addition to the required “wall clock” reading, a Time may contain an optional
|
||||
* reading of the current process's monotonic clock, to provide additional precision
|
||||
* for comparison or subtraction.
|
||||
* See the “Monotonic Clocks” section in the package documentation for details.
|
||||
*
|
||||
* Note that the Go == operator compares not just the time instant but also the
|
||||
* Location and the monotonic clock reading. Therefore, Time values should not
|
||||
* be used as map or database keys without first guaranteeing that the
|
||||
* identical Location has been set for all values, which can be achieved
|
||||
* through use of the UTC or Local method, and that the monotonic clock reading
|
||||
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
|
||||
* to t == u, since t.Equal uses the most accurate comparison available and
|
||||
* correctly handles the case when only one of its arguments has a monotonic
|
||||
* clock reading.
|
||||
*/
|
||||
export type Time = any;
|
||||
@@ -0,0 +1,9 @@
|
||||
//@ts-check
|
||||
// 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";
|
||||
|
||||
Object.freeze($Create.Events);
|
||||
2
frontend/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
2
frontend/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
15
frontend/index.html
Normal file
15
frontend/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MeshDrop</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1683
frontend/package-lock.json
generated
Normal file
1683
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:dev": "vue-tsc && vite build --minify false --mode development",
|
||||
"build": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.3",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.79",
|
||||
"naive-ui": "^2.43.2",
|
||||
"vfonts": "^0.0.3",
|
||||
"vue": "^3.2.45"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^5.0.0",
|
||||
"vue-tsc": "^1.0.11"
|
||||
}
|
||||
}
|
||||
39
frontend/src/App.vue
Normal file
39
frontend/src/App.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NConfigProvider,
|
||||
NGlobalStyle,
|
||||
NMessageProvider,
|
||||
NDialogProvider,
|
||||
darkTheme,
|
||||
} from "naive-ui";
|
||||
import MainLayout from "./components/MainLayout.vue";
|
||||
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
primaryColor: "#38bdf8",
|
||||
primaryColorHover: "#0ea5e9",
|
||||
},
|
||||
Card: {
|
||||
borderColor: "#334155",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme="darkTheme" :theme-overrides="themeOverrides">
|
||||
<n-global-style />
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<MainLayout />
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
body,
|
||||
#app,
|
||||
.n-config-provider {
|
||||
font-family: "Noto Sans", "Roboto", "Segoe UI", sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
333
frontend/src/components/MainLayout.vue
Normal file
333
frontend/src/components/MainLayout.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, computed, h } from "vue";
|
||||
import PeerCard from "./PeerCard.vue";
|
||||
import TransferItem from "./TransferItem.vue";
|
||||
import {
|
||||
NLayout,
|
||||
NLayoutHeader,
|
||||
NLayoutContent,
|
||||
NLayoutSider,
|
||||
NSpace,
|
||||
NText,
|
||||
NEmpty,
|
||||
NGrid,
|
||||
NGi,
|
||||
NMenu,
|
||||
NBadge,
|
||||
NButton,
|
||||
NIcon,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
useDialog,
|
||||
NInput,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faSatelliteDish,
|
||||
faInbox,
|
||||
faBars,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
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 { Events } from "@wailsio/runtime";
|
||||
import {
|
||||
GetTransferList,
|
||||
SendFile,
|
||||
SendText,
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
const peers = ref<Peer[]>([]);
|
||||
const transferList = ref<Transfer[]>([]);
|
||||
const activeKey = ref("discover");
|
||||
const showMobileMenu = ref(false);
|
||||
const isMobile = ref(false);
|
||||
|
||||
// 监听窗口大小变化更新 isMobile
|
||||
onMounted(() => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
});
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
if (!isMobile.value) showMobileMenu.value = false;
|
||||
};
|
||||
|
||||
// --- 菜单选项 ---
|
||||
const renderIcon = (icon: any) => {
|
||||
return () => h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon }) });
|
||||
};
|
||||
|
||||
const menuOptions = computed<MenuOption[]>(() => [
|
||||
{
|
||||
label: "Discover",
|
||||
key: "discover",
|
||||
icon: renderIcon(faSatelliteDish),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
style:
|
||||
"display: flex; align-items: center; justify-content: space-between; width: 100%",
|
||||
},
|
||||
[
|
||||
"Transfers",
|
||||
pendingCount.value > 0 ?
|
||||
h(NBadge, { value: pendingCount.value, max: 99, type: "error" })
|
||||
: null,
|
||||
],
|
||||
),
|
||||
key: "transfers",
|
||||
icon: renderIcon(faInbox),
|
||||
},
|
||||
]);
|
||||
|
||||
// --- 后端集成 ---
|
||||
onMounted(async () => {
|
||||
peers.value = await GetPeers();
|
||||
});
|
||||
|
||||
// --- 事件监听 ---
|
||||
|
||||
Events.On("peers:update", (event) => {
|
||||
peers.value = event.data;
|
||||
});
|
||||
|
||||
Events.On("transfer:refreshList", async () => {
|
||||
transferList.value = await GetTransferList();
|
||||
});
|
||||
|
||||
// --- 计算属性 ---
|
||||
const pendingCount = computed(() => {
|
||||
return transferList.value.filter(
|
||||
(t) => t.type === "receive" && t.status === "pending",
|
||||
).length;
|
||||
});
|
||||
|
||||
// --- 操作 ---
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
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;
|
||||
await SendFile(peer, ip, filePath);
|
||||
activeKey.value = "transfers";
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
alert("Failed to send file: " + e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendFolder = async (ip: string) => {
|
||||
// TODO
|
||||
};
|
||||
|
||||
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;
|
||||
await SendText(peer, ip, textContent.value);
|
||||
activeKey.value = "transfers";
|
||||
} 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;
|
||||
await SendText(peer, ip, text);
|
||||
activeKey.value = "transfers";
|
||||
};
|
||||
|
||||
const removeTransfer = (id: string) => {
|
||||
transferList.value = transferList.value.filter((t) => t.id !== id);
|
||||
};
|
||||
|
||||
const handleMenuUpdate = (key: string) => {
|
||||
activeKey.value = key;
|
||||
showMobileMenu.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 小尺寸头部 -->
|
||||
<n-layout-header v-if="isMobile" bordered class="mobile-header">
|
||||
<n-space
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style="height: 100%; padding: 0 16px">
|
||||
<n-text class="logo">Mesh Drop</n-text>
|
||||
<n-button
|
||||
text
|
||||
style="font-size: 24px"
|
||||
@click="showMobileMenu = !showMobileMenu">
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="showMobileMenu ? faXmark : faBars" />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-layout-header>
|
||||
|
||||
<!-- 小尺寸抽屉菜单 -->
|
||||
<n-drawer
|
||||
v-model:show="showMobileMenu"
|
||||
placement="top"
|
||||
height="200"
|
||||
v-if="isMobile">
|
||||
<n-drawer-content>
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:options="menuOptions"
|
||||
@update:value="handleMenuUpdate" />
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
|
||||
<n-layout
|
||||
has-sider
|
||||
position="absolute"
|
||||
:style="{ top: isMobile ? '64px' : '0' }">
|
||||
<!-- 桌面端侧边栏 -->
|
||||
<n-layout-sider
|
||||
v-if="!isMobile"
|
||||
bordered
|
||||
width="240"
|
||||
content-style="padding: 24px;">
|
||||
<div class="desktop-logo">
|
||||
<n-text class="logo">Mesh Drop</n-text>
|
||||
</div>
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:options="menuOptions"
|
||||
@update:value="handleMenuUpdate" />
|
||||
</n-layout-sider>
|
||||
|
||||
<n-layout-content class="content">
|
||||
<div class="content-container">
|
||||
<!-- 发现页视图 -->
|
||||
<div v-if="activeKey === 'discover'">
|
||||
<n-space vertical size="large" v-if="peers.length > 0">
|
||||
<n-grid x-gap="16" y-gap="16" cols="1 500:2 700:3">
|
||||
<n-gi v-for="peer in peers" :key="peer.id">
|
||||
<PeerCard
|
||||
:peer="peer"
|
||||
@sendFile="handleSendFile"
|
||||
@sendFolder="handleSendFolder"
|
||||
@sendText="handleSendText"
|
||||
@sendClipboard="handleSendClipboard" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-space>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<n-empty description="Scanning for peers...">
|
||||
<template #icon>
|
||||
<n-icon class="radar-icon">
|
||||
<FontAwesomeIcon :icon="faSatelliteDish" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传输列表视图 -->
|
||||
<div v-else-if="activeKey === 'transfers'">
|
||||
<div v-if="transferList.length > 0">
|
||||
<TransferItem
|
||||
v-for="transfer in transferList"
|
||||
:key="transfer.id"
|
||||
:transfer="transfer" />
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<n-empty style="user-select: none" description="No transfers yet">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faInbox" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-header {
|
||||
height: 64px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.desktop-logo {
|
||||
margin-bottom: 24px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.radar-icon {
|
||||
animation: spin 3s linear infinite;
|
||||
color: #38bdf8;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
194
frontend/src/components/PeerCard.vue
Normal file
194
frontend/src/components/PeerCard.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, h } from "vue";
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
NIcon,
|
||||
NTag,
|
||||
NSpace,
|
||||
NDropdown,
|
||||
NSelect,
|
||||
type DropdownOption,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faLinux,
|
||||
faWindows,
|
||||
faApple,
|
||||
} from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faDesktop,
|
||||
faGlobe,
|
||||
faPaperPlane,
|
||||
faChevronDown,
|
||||
faFile,
|
||||
faFolder,
|
||||
faFont,
|
||||
faClipboard,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery";
|
||||
|
||||
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;
|
||||
}>();
|
||||
|
||||
const ips = computed(() => {
|
||||
if (!props.peer.routes) return [];
|
||||
return Object.keys(props.peer.routes);
|
||||
});
|
||||
|
||||
const selectedIp = ref<string>("");
|
||||
|
||||
watch(
|
||||
ips,
|
||||
(newIps) => {
|
||||
if (newIps.length > 0) {
|
||||
if (!selectedIp.value || !newIps.includes(selectedIp.value)) {
|
||||
selectedIp.value = newIps[0];
|
||||
}
|
||||
} else {
|
||||
selectedIp.value = "";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const ipOptions = computed(() => {
|
||||
return ips.value.map((ip) => ({
|
||||
label: ip,
|
||||
value: ip,
|
||||
}));
|
||||
});
|
||||
|
||||
const osIcon = computed(() => {
|
||||
switch (props.peer.os) {
|
||||
case "linux":
|
||||
return faLinux;
|
||||
case "windows":
|
||||
return faWindows;
|
||||
case "darwin":
|
||||
return faApple;
|
||||
default:
|
||||
return faDesktop;
|
||||
}
|
||||
});
|
||||
|
||||
const sendOptions: DropdownOption[] = [
|
||||
{
|
||||
label: "Send File",
|
||||
key: "file",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFile }) }),
|
||||
},
|
||||
{
|
||||
label: "Send Folder",
|
||||
key: "folder",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFolder }) }),
|
||||
},
|
||||
{
|
||||
label: "Send Text",
|
||||
key: "text",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFont }) }),
|
||||
},
|
||||
{
|
||||
label: "Send Clipboard",
|
||||
key: "clipboard",
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(FontAwesomeIcon, { icon: faClipboard }),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const handleAction = (key: string) => {
|
||||
if (!selectedIp.value) return;
|
||||
|
||||
switch (key) {
|
||||
case "file":
|
||||
emit("sendFile", selectedIp.value);
|
||||
break;
|
||||
case "folder":
|
||||
emit("sendFolder", selectedIp.value);
|
||||
break;
|
||||
case "text":
|
||||
emit("sendText", selectedIp.value);
|
||||
break;
|
||||
case "clipboard":
|
||||
emit("sendClipboard", selectedIp.value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card hoverable class="peer-card">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<n-icon size="24">
|
||||
<FontAwesomeIcon :icon="osIcon" />
|
||||
</n-icon>
|
||||
<span style="user-select: none">{{ peer.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-space vertical>
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faGlobe" />
|
||||
</n-icon>
|
||||
<!-- Single IP Display -->
|
||||
<n-tag
|
||||
v-if="ips.length === 1"
|
||||
:bordered="false"
|
||||
type="info"
|
||||
size="small">
|
||||
{{ ips[0] }}
|
||||
</n-tag>
|
||||
<!-- Multiple IP Selector -->
|
||||
<n-select
|
||||
v-else-if="ips.length > 1"
|
||||
v-model:value="selectedIp"
|
||||
:options="ipOptions"
|
||||
size="small"
|
||||
style="width: 140px" />
|
||||
<!-- No Route -->
|
||||
<n-tag v-else :bordered="false" type="warning" size="small">
|
||||
No Route
|
||||
</n-tag>
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
<template #action>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="sendOptions"
|
||||
@select="handleAction"
|
||||
:disabled="ips.length === 0">
|
||||
<n-button type="primary" block dashed style="width: 100%">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" />
|
||||
</n-icon>
|
||||
</template>
|
||||
Send...
|
||||
<n-icon style="margin-left: 4px">
|
||||
<FontAwesomeIcon :icon="faChevronDown" />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
283
frontend/src/components/TransferItem.vue
Normal file
283
frontend/src/components/TransferItem.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
NIcon,
|
||||
NProgress,
|
||||
NSpace,
|
||||
NText,
|
||||
NTag,
|
||||
useMessage,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faArrowUp,
|
||||
faArrowDown,
|
||||
faCircleExclamation,
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import { ResolvePendingRequest } from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
const props = defineProps<{
|
||||
transfer: Transfer;
|
||||
}>();
|
||||
|
||||
const formatSize = (bytes?: number) => {
|
||||
if (bytes === undefined) return "";
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
const formatSpeed = (speed?: number) => {
|
||||
if (!speed) return "";
|
||||
return formatSize(speed) + "/s";
|
||||
};
|
||||
|
||||
const percentage = computed(() =>
|
||||
Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
(props.transfer.progress.current / props.transfer.progress.total) * 100,
|
||||
),
|
||||
),
|
||||
);
|
||||
const progressStatus = computed(() => {
|
||||
if (props.transfer.status === "error") return "error";
|
||||
if (props.transfer.status === "completed") return "success";
|
||||
return "default";
|
||||
});
|
||||
|
||||
const acceptTransfer = () => {
|
||||
ResolvePendingRequest(props.transfer.id, true, "");
|
||||
};
|
||||
|
||||
const rejectTransfer = () => {
|
||||
ResolvePendingRequest(props.transfer.id, false, "");
|
||||
};
|
||||
|
||||
const acceptToFolder = async () => {
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
Title: "Select Folder to save the file",
|
||||
CanChooseDirectories: true,
|
||||
CanChooseFiles: false,
|
||||
AllowsMultipleSelection: false,
|
||||
};
|
||||
const path = await Dialogs.OpenFile(opts);
|
||||
if (path !== "") {
|
||||
ResolvePendingRequest(props.transfer.id, true, path as string);
|
||||
}
|
||||
};
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const handleCopy = async () => {
|
||||
Clipboard.SetText(props.transfer.text)
|
||||
.then(() => {
|
||||
message.success("Copied to clipboard");
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("Failed to copy to clipboard");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card size="small" class="transfer-item">
|
||||
<div class="transfer-row">
|
||||
<!-- 图标 -->
|
||||
<div class="icon-wrapper">
|
||||
<n-icon size="24" v-if="props.transfer.type === 'send'" color="#38bdf8">
|
||||
<FontAwesomeIcon :icon="faArrowUp" />
|
||||
</n-icon>
|
||||
<n-icon
|
||||
size="24"
|
||||
v-else-if="props.transfer.type === 'receive'"
|
||||
color="#22c55e">
|
||||
<FontAwesomeIcon :icon="faArrowDown" />
|
||||
</n-icon>
|
||||
<n-icon size="24" v-else color="#f59e0b">
|
||||
<FontAwesomeIcon :icon="faCircleExclamation" />
|
||||
</n-icon>
|
||||
</div>
|
||||
|
||||
<!-- 信息 -->
|
||||
<div class="info-wrapper">
|
||||
<div class="header-line">
|
||||
<n-text
|
||||
v-if="props.transfer.content_type === 'file'"
|
||||
strong
|
||||
class="filename"
|
||||
:title="props.transfer.file_name"
|
||||
>{{ props.transfer.file_name }}</n-text
|
||||
>
|
||||
<n-text
|
||||
v-else-if="props.transfer.content_type === 'text'"
|
||||
strong
|
||||
class="filename"
|
||||
title="Text"
|
||||
>Text</n-text
|
||||
>
|
||||
<n-tag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
v-if="props.transfer.sender.name">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faUser" />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ props.transfer.sender.name }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="meta-line">
|
||||
<n-text depth="3" class="size">{{
|
||||
formatSize(props.transfer.file_size)
|
||||
}}</n-text>
|
||||
|
||||
<!-- 状态文本(进行中/已完成) -->
|
||||
<span>
|
||||
<n-text depth="3" v-if="props.transfer.status === 'active'">
|
||||
- {{ formatSpeed(props.transfer.progress.speed) }}</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'completed'"
|
||||
type="success">
|
||||
- Completed</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'error'"
|
||||
type="error">
|
||||
- {{ props.transfer.error_msg || "Error" }}</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'"
|
||||
type="line"
|
||||
:percentage="percentage"
|
||||
:status="progressStatus"
|
||||
:height="4"
|
||||
:show-indicator="false"
|
||||
processing
|
||||
style="margin-top: 4px" />
|
||||
</div>
|
||||
|
||||
<!-- 接受/拒绝操作按钮 -->
|
||||
<div
|
||||
class="actions-wrapper"
|
||||
v-if="
|
||||
props.transfer.type === 'receive' &&
|
||||
props.transfer.status === 'pending'
|
||||
">
|
||||
<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>
|
||||
|
||||
<!-- 复制按钮 -->
|
||||
<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="handleCopy"
|
||||
>Copy</n-button
|
||||
>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 发送方取消按钮 -->
|
||||
<div
|
||||
class="actions-wrapper"
|
||||
v-if="
|
||||
props.transfer.type === 'send' &&
|
||||
props.transfer.status !== 'completed'
|
||||
">
|
||||
<n-space>
|
||||
<n-button size="small" type="error" ghost @click="">
|
||||
Cancel
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.transfer-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.transfer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.filename {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
4
frontend/src/main.ts
Normal file
4
frontend/src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
19
frontend/tsconfig.json
Normal file
19
frontend/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "bindings"],
|
||||
}
|
||||
8
frontend/vite.config.ts
Normal file
8
frontend/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import wails from "@wailsio/runtime/plugins/vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), wails("./bindings")],
|
||||
});
|
||||
Reference in New Issue
Block a user