mod: ui
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
// @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 GetAutoAccept(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(2605668438);
|
||||
}
|
||||
@@ -29,6 +33,12 @@ export function GetVersion(): $CancellablePromise<string> {
|
||||
return $Call.ByID(3578438023);
|
||||
}
|
||||
|
||||
export function GetWindowState(): $CancellablePromise<$models.WindowState> {
|
||||
return $Call.ByID(341414414).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save 保存配置到磁盘
|
||||
*/
|
||||
@@ -54,3 +64,10 @@ export function SetSaveHistory(saveHistory: boolean): $CancellablePromise<void>
|
||||
export function SetSavePath(savePath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3805718491, savePath);
|
||||
}
|
||||
|
||||
export function SetWindowState(state: $models.WindowState): $CancellablePromise<void> {
|
||||
return $Call.ByID(4007191514, state);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.WindowState.createFrom;
|
||||
|
||||
@@ -5,3 +5,7 @@ import * as Config from "./config.js";
|
||||
export {
|
||||
Config
|
||||
};
|
||||
|
||||
export {
|
||||
WindowState
|
||||
} from "./models.js";
|
||||
|
||||
46
frontend/bindings/mesh-drop/internal/config/models.ts
Normal file
46
frontend/bindings/mesh-drop/internal/config/models.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* WindowState 定义窗口状态
|
||||
*/
|
||||
export class WindowState {
|
||||
"Width": number;
|
||||
"Height": number;
|
||||
"X": number;
|
||||
"Y": number;
|
||||
"Maximised": boolean;
|
||||
|
||||
/** Creates a new WindowState instance. */
|
||||
constructor($$source: Partial<WindowState> = {}) {
|
||||
if (!("Width" in $$source)) {
|
||||
this["Width"] = 0;
|
||||
}
|
||||
if (!("Height" in $$source)) {
|
||||
this["Height"] = 0;
|
||||
}
|
||||
if (!("X" in $$source)) {
|
||||
this["X"] = 0;
|
||||
}
|
||||
if (!("Y" in $$source)) {
|
||||
this["Y"] = 0;
|
||||
}
|
||||
if (!("Maximised" in $$source)) {
|
||||
this["Maximised"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WindowState instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowState {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new WindowState($$parsedSource as Partial<WindowState>);
|
||||
}
|
||||
}
|
||||
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@@ -11,8 +11,11 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
FileSendModal: typeof import('./src/components/modals/FileSendModal.vue')['default']
|
||||
MainLayout: typeof import('./src/components/MainLayout.vue')['default']
|
||||
PeerCard: typeof import('./src/components/PeerCard.vue')['default']
|
||||
SettingsView: typeof import('./src/components/SettingsView.vue')['default']
|
||||
TextSendModal: typeof import('./src/components/modals/TextSendModal.vue')['default']
|
||||
TransferItem: typeof import('./src/components/TransferItem.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mesh Drop</title>
|
||||
<link rel="stylesheet" href="src/styles/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
56
frontend/package-lock.json
generated
56
frontend/package-lock.json
generated
@@ -20,7 +20,9 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"sass": "^1.97.3",
|
||||
"sass-embedded": "^1.92.1",
|
||||
"sass-loader": "^16.0.6",
|
||||
"typescript": "~5.9.2",
|
||||
"unplugin-fonts": "^1.4.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
@@ -1543,8 +1545,8 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -2013,6 +2015,13 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
@@ -2219,8 +2228,8 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
@@ -2323,8 +2332,8 @@
|
||||
"version": "1.97.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
|
||||
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -2670,6 +2679,47 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.6.tgz",
|
||||
"integrity": "sha512-sglGzId5gmlfxNs4gK2U3h7HlVRfx278YK6Ono5lwzuvi1jxig80YiuHkaDBVsYIKFhx8wN7XSCI0M2IDS/3qA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"neo-async": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/core": "0.x || 1.x",
|
||||
"node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||
"sass": "^1.3.0",
|
||||
"sass-embedded": "*",
|
||||
"webpack": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rspack/core": {
|
||||
"optional": true
|
||||
},
|
||||
"node-sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"sass": "^1.97.3",
|
||||
"sass-embedded": "^1.92.1",
|
||||
"sass-loader": "^16.0.6",
|
||||
"typescript": "~5.9.2",
|
||||
"unplugin-fonts": "^1.4.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
|
||||
@@ -8,8 +8,4 @@ import MainLayout from "./components/MainLayout.vue";
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
font-family: "Inter Variable", sans-serif;
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,79 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
// --- Vue 核心 ---
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
|
||||
// --- 组件 ---
|
||||
import PeerCard from "./PeerCard.vue";
|
||||
import TransferItem from "./TransferItem.vue";
|
||||
import SettingsView from "./SettingsView.vue";
|
||||
|
||||
// --- 类型 & 模型 ---
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import {
|
||||
GetSavePath,
|
||||
SetSavePath,
|
||||
GetHostName,
|
||||
SetHostName,
|
||||
GetAutoAccept,
|
||||
SetAutoAccept,
|
||||
GetSaveHistory,
|
||||
SetSaveHistory,
|
||||
GetVersion,
|
||||
} from "../../bindings/mesh-drop/internal/config/config";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
|
||||
// --- Service & 后端绑定 ---
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
|
||||
import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
|
||||
// --- 状态 ---
|
||||
const peers = ref<Peer[]>([]);
|
||||
const transferList = ref<Transfer[]>([]);
|
||||
const activeKey = ref("discover");
|
||||
const drawer = ref(true);
|
||||
const isMobile = ref(false);
|
||||
|
||||
// 监听窗口大小变化更新 isMobile
|
||||
onMounted(async () => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
const list = await GetTransferList();
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
|
||||
if (isMobile.value) {
|
||||
drawer.value = false;
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
savePath.value = await GetSavePath();
|
||||
hostName.value = await GetHostName();
|
||||
autoAccept.value = await GetAutoAccept();
|
||||
saveHistory.value = await GetSaveHistory();
|
||||
version.value = await GetVersion();
|
||||
});
|
||||
|
||||
const checkMobile = () => {
|
||||
const mobile = window.innerWidth < 768;
|
||||
if (mobile !== isMobile.value) {
|
||||
isMobile.value = mobile;
|
||||
drawer.value = !mobile;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 后端集成 ---
|
||||
onMounted(async () => {
|
||||
peers.value = await GetPeers();
|
||||
peers.value = peers.value.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// --- 事件监听 ---
|
||||
Events.On("peers:update", (event) => {
|
||||
peers.value = event.data;
|
||||
peers.value = peers.value.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
Events.On("transfer:refreshList", async () => {
|
||||
const list = await GetTransferList();
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
});
|
||||
|
||||
// --- 计算属性 ---
|
||||
const pendingCount = computed(() => {
|
||||
return transferList.value.filter(
|
||||
@@ -100,30 +49,47 @@ const menuItems = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
// --- 设置 ---
|
||||
const savePath = ref("");
|
||||
// --- 生命周期 ---
|
||||
onMounted(async () => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
const list = await GetTransferList();
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
|
||||
const changeSavePath = async () => {
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
Title: "Select Save Path",
|
||||
CanChooseDirectories: true,
|
||||
CanChooseFiles: false,
|
||||
AllowsMultipleSelection: false,
|
||||
};
|
||||
const path = await Dialogs.OpenFile(opts);
|
||||
if (path && typeof path === "string") {
|
||||
await SetSavePath(path);
|
||||
savePath.value = path;
|
||||
if (isMobile.value) {
|
||||
drawer.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// --- 后端集成 & 事件监听 ---
|
||||
onMounted(async () => {
|
||||
peers.value = await GetPeers();
|
||||
peers.value = peers.value.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
Events.On("peers:update", (event) => {
|
||||
peers.value = event.data;
|
||||
peers.value = peers.value.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
Events.On("transfer:refreshList", async () => {
|
||||
const list = await GetTransferList();
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
});
|
||||
|
||||
// --- 方法 ---
|
||||
const checkMobile = () => {
|
||||
const mobile = window.innerWidth < 768;
|
||||
if (mobile !== isMobile.value) {
|
||||
isMobile.value = mobile;
|
||||
drawer.value = !mobile;
|
||||
}
|
||||
};
|
||||
|
||||
const hostName = ref("");
|
||||
const autoAccept = ref(false);
|
||||
const saveHistory = ref(false);
|
||||
const version = ref("");
|
||||
|
||||
// --- 操作 ---
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
activeKey.value = key;
|
||||
if (isMobile.value) {
|
||||
@@ -146,7 +112,7 @@ const handleMenuClick = (key: string) => {
|
||||
|
||||
<!-- 导航抽屉 -->
|
||||
<v-navigation-drawer v-model="drawer" :permanent="!isMobile">
|
||||
<div class="pa-4" v-if="!isMobile">
|
||||
<div class="pa-4 d-flex align-center justify-center" v-if="!isMobile">
|
||||
<div class="text-h6 text-primary font-weight-bold">Mesh Drop</div>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +130,7 @@ const handleMenuClick = (key: string) => {
|
||||
<v-icon :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
<v-list-item-title class="text-body-2">
|
||||
{{ item.title }}
|
||||
<v-badge
|
||||
v-if="item.badge"
|
||||
@@ -227,73 +193,7 @@ const handleMenuClick = (key: string) => {
|
||||
|
||||
<!-- 设置视图 -->
|
||||
<div v-show="activeKey === 'settings'">
|
||||
<v-list lines="one" bg-color="transparent">
|
||||
<v-list-item title="Save Path" :subtitle="savePath">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-folder-download"></v-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="changeSavePath"
|
||||
prepend-icon="mdi-pencil"
|
||||
>
|
||||
Change
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item title="HostName">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-laptop"></v-icon>
|
||||
</template>
|
||||
<template #append
|
||||
><v-text-field
|
||||
clearable
|
||||
variant="underlined"
|
||||
v-model="hostName"
|
||||
width="200"
|
||||
@update:modelValue="SetHostName"
|
||||
></v-text-field
|
||||
></template>
|
||||
</v-list-item>
|
||||
<v-list-item title="Save History">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-history"></v-icon>
|
||||
</template>
|
||||
<template #append
|
||||
><v-switch
|
||||
v-model="saveHistory"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
@update:modelValue="SetSaveHistory(saveHistory)"
|
||||
></v-switch
|
||||
></template>
|
||||
</v-list-item>
|
||||
<v-list-item title="Auto Accept">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save"></v-icon>
|
||||
</template>
|
||||
<template #append
|
||||
><v-switch
|
||||
v-model="autoAccept"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
@update:modelValue="SetAutoAccept(autoAccept)"
|
||||
></v-switch
|
||||
></template>
|
||||
</v-list-item>
|
||||
<v-list-item title="Version">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information"></v-icon>
|
||||
</template>
|
||||
<template #append
|
||||
><div class="text-grey">{{ version }}</div></template
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<SettingsView />
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
// --- Vue 核心 ---
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
import { Dialogs, Events, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
// --- 组件 ---
|
||||
import FileSendModal from "./modals/FileSendModal.vue";
|
||||
import TextSendModal from "./modals/TextSendModal.vue";
|
||||
|
||||
// --- Wails & 后端绑定 ---
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
import {
|
||||
SendFiles,
|
||||
SendFolder,
|
||||
SendText,
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
|
||||
// --- 属性 & 事件 ---
|
||||
const props = defineProps<{
|
||||
peer: Peer;
|
||||
}>();
|
||||
@@ -16,39 +23,10 @@ const emit = defineEmits<{
|
||||
(e: "transferStarted"): 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 osIcon = computed(() => {
|
||||
switch (props.peer.os) {
|
||||
case "linux":
|
||||
return "mdi-linux";
|
||||
case "windows":
|
||||
return "mdi-microsoft-windows";
|
||||
case "darwin":
|
||||
return "mdi-apple";
|
||||
default:
|
||||
return "mdi-desktop-classic";
|
||||
}
|
||||
});
|
||||
const showFileModal = ref(false);
|
||||
const showTextModal = ref(false);
|
||||
|
||||
const sendOptions = [
|
||||
{
|
||||
@@ -73,6 +51,41 @@ const sendOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
// --- 计算属性 ---
|
||||
const ips = computed(() => {
|
||||
if (!props.peer.routes) return [];
|
||||
return Object.keys(props.peer.routes);
|
||||
});
|
||||
|
||||
const osIcon = computed(() => {
|
||||
switch (props.peer.os) {
|
||||
case "linux":
|
||||
return "mdi-linux";
|
||||
case "windows":
|
||||
return "mdi-microsoft-windows";
|
||||
case "darwin":
|
||||
return "mdi-apple";
|
||||
default:
|
||||
return "mdi-desktop-classic";
|
||||
}
|
||||
});
|
||||
|
||||
// --- 监听 ---
|
||||
watch(
|
||||
ips,
|
||||
(newIps) => {
|
||||
if (newIps.length > 0) {
|
||||
if (!selectedIp.value || !newIps.includes(selectedIp.value)) {
|
||||
selectedIp.value = newIps[0];
|
||||
}
|
||||
} else {
|
||||
selectedIp.value = "";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// --- 方法 ---
|
||||
const handleAction = (key: string) => {
|
||||
if (!selectedIp.value) return;
|
||||
|
||||
@@ -92,8 +105,6 @@ const handleAction = (key: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- 发送逻辑 ---
|
||||
|
||||
const handleSendFolder = async () => {
|
||||
if (!selectedIp.value) return;
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
@@ -125,85 +136,6 @@ const handleSendClipboard = async () => {
|
||||
});
|
||||
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>
|
||||
@@ -289,122 +221,18 @@ const handleSendFiles = () => {
|
||||
</template>
|
||||
</v-card>
|
||||
|
||||
<!-- 文件发送 Modal -->
|
||||
<v-dialog v-model="showFileModal" width="600" persistent eager>
|
||||
<v-card title="Send Files">
|
||||
<v-card-text>
|
||||
<div
|
||||
v-if="fileList.length === 0"
|
||||
class="drop-zone pa-10 text-center rounded-lg border-dashed"
|
||||
@click="openFileDialog"
|
||||
data-file-drop-target
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-cloud-upload"
|
||||
size="48"
|
||||
color="primary"
|
||||
class="mb-2"
|
||||
></v-icon>
|
||||
<div class="text-body-1 text-medium-emphasis">
|
||||
Click to select files
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modals -->
|
||||
<FileSendModal
|
||||
v-model="showFileModal"
|
||||
:peer="peer"
|
||||
:selectedIp="selectedIp"
|
||||
@transferStarted="emit('transferStarted')"
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
<v-list
|
||||
class="mb-4 text-left"
|
||||
border
|
||||
rounded
|
||||
max-height="400"
|
||||
style="overflow-y: auto"
|
||||
data-file-drop-target
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(file, index) in fileList"
|
||||
:key="file.path"
|
||||
:title="file.name"
|
||||
:subtitle="file.path"
|
||||
lines="two"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="handleRemoveFile(index)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
variant="outlined"
|
||||
style="border-style: dashed"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openFileDialog"
|
||||
class="mt-2"
|
||||
>
|
||||
Add more files
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="handleCancelFiles">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleSendFiles"
|
||||
:disabled="fileList.length === 0"
|
||||
>
|
||||
Send {{ fileList.length > 0 ? `(${fileList.length})` : "" }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 文本发送 Modal -->
|
||||
<v-dialog v-model="showTextModal" width="500" persistent eager>
|
||||
<v-card title="Send Text">
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="textContent"
|
||||
label="Content"
|
||||
placeholder="Type something to send..."
|
||||
rows="4"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showTextModal = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="executeSendText"
|
||||
:disabled="!textContent"
|
||||
>
|
||||
Send
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<TextSendModal
|
||||
v-model="showTextModal"
|
||||
:peer="peer"
|
||||
:selectedIp="selectedIp"
|
||||
@transferStarted="emit('transferStarted')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drop-zone {
|
||||
border: 2px dashed #666; /* Use a darker color or theme var */
|
||||
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>
|
||||
|
||||
119
frontend/src/components/SettingsView.vue
Normal file
119
frontend/src/components/SettingsView.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup>
|
||||
// --- Vue 核心 ---
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
// --- Wails & 后端绑定 ---
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import {
|
||||
GetSavePath,
|
||||
SetSavePath,
|
||||
GetHostName,
|
||||
SetHostName,
|
||||
GetAutoAccept,
|
||||
SetAutoAccept,
|
||||
GetSaveHistory,
|
||||
SetSaveHistory,
|
||||
GetVersion,
|
||||
} from "../../bindings/mesh-drop/internal/config/config";
|
||||
|
||||
// --- 状态 ---
|
||||
const savePath = ref("");
|
||||
const hostName = ref("");
|
||||
const autoAccept = ref(false);
|
||||
const saveHistory = ref(false);
|
||||
const version = ref("");
|
||||
|
||||
// ---生命周期 ---
|
||||
onMounted(async () => {
|
||||
savePath.value = await GetSavePath();
|
||||
hostName.value = await GetHostName();
|
||||
autoAccept.value = await GetAutoAccept();
|
||||
saveHistory.value = await GetSaveHistory();
|
||||
version.value = await GetVersion();
|
||||
});
|
||||
|
||||
// --- 方法 ---
|
||||
const changeSavePath = async () => {
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
Title: "Select Save Path",
|
||||
CanChooseDirectories: true,
|
||||
CanChooseFiles: false,
|
||||
AllowsMultipleSelection: false,
|
||||
};
|
||||
const path = await Dialogs.OpenFile(opts);
|
||||
if (path && typeof path === "string") {
|
||||
await SetSavePath(path);
|
||||
savePath.value = path;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list lines="one" bg-color="transparent">
|
||||
<v-list-item title="Save Path" :subtitle="savePath">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-folder-download"></v-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="changeSavePath"
|
||||
prepend-icon="mdi-pencil"
|
||||
>
|
||||
Change
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item title="HostName">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-laptop"></v-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
clearable
|
||||
variant="underlined"
|
||||
v-model="hostName"
|
||||
width="200"
|
||||
@update:modelValue="SetHostName"
|
||||
></v-text-field>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item title="Save History">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-history"></v-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="saveHistory"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
@update:modelValue="SetSaveHistory(saveHistory)"
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item title="Auto Accept">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save"></v-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="autoAccept"
|
||||
color="primary"
|
||||
inset
|
||||
hide-details
|
||||
@update:modelValue="SetAutoAccept(autoAccept)"
|
||||
></v-switch>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item title="Version">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-information"></v-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<div class="text-grey">{{ version }}</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
@@ -1,35 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, h } from "vue";
|
||||
// --- Vue 核心 ---
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
// --- Wails & 后端绑定 ---
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
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";
|
||||
|
||||
// --- 属性 & 事件 ---
|
||||
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 formatTime = (time: number): string => {
|
||||
return new Date(time).toLocaleString();
|
||||
};
|
||||
// --- 状态 ---
|
||||
const showContentDialog = ref(false);
|
||||
|
||||
// --- 计算属性 ---
|
||||
const percentage = computed(() =>
|
||||
Math.min(
|
||||
100,
|
||||
@@ -38,63 +28,13 @@ const percentage = computed(() =>
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const progressColor = computed(() => {
|
||||
if (props.transfer.status === "error") return "error";
|
||||
if (props.transfer.status === "completed") return "success";
|
||||
return "primary";
|
||||
});
|
||||
|
||||
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 dropdownItems = [
|
||||
{
|
||||
title: "Accept To Folder",
|
||||
value: "folder",
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = (key: string | number) => {
|
||||
if (key === "folder") {
|
||||
acceptToFolder();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
DeleteTransfer(props.transfer.id);
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
Clipboard.SetText(props.transfer.text)
|
||||
// .then(() => {
|
||||
// message.success("Copied to clipboard");
|
||||
// })
|
||||
.catch(() => {
|
||||
// message.error("Failed to copy to clipboard");
|
||||
console.error("Failed to copy");
|
||||
});
|
||||
};
|
||||
|
||||
const showContentDialog = ref(false);
|
||||
|
||||
const canCancel = computed(() => {
|
||||
if (
|
||||
props.transfer.status === "completed" ||
|
||||
@@ -136,10 +76,60 @@ const canAccept = computed(() => {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// --- 方法 ---
|
||||
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 formatTime = (time: number): string => {
|
||||
return new Date(time).toLocaleString();
|
||||
};
|
||||
|
||||
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 handleDelete = () => {
|
||||
DeleteTransfer(props.transfer.id);
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
Clipboard.SetText(props.transfer.text).catch(() => {
|
||||
console.error("Failed to copy");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="transfer-item mb-2" variant="outlined">
|
||||
<v-card class="transfer-item mb-2">
|
||||
<v-card-text class="py-2 px-3">
|
||||
<div class="d-flex align-center flex-wrap ga-2">
|
||||
<!-- 图标 -->
|
||||
@@ -257,54 +247,38 @@ const canAccept = computed(() => {
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions-wrapper">
|
||||
<v-btn-group density="compact" variant="outlined" divided>
|
||||
<v-btn
|
||||
v-if="canAccept"
|
||||
color="success"
|
||||
icon="mdi-content-save"
|
||||
@click="acceptTransfer"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Accept</v-tooltip>
|
||||
<v-btn-group density="compact" variant="tonal" divided rounded="xl">
|
||||
<v-btn v-if="canAccept" color="success" @click="acceptTransfer">
|
||||
<v-icon icon="mdi-content-save"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Accept</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canAccept"
|
||||
color="success"
|
||||
icon="mdi-folder-arrow-right"
|
||||
@click="acceptToFolder"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
<v-btn v-if="canAccept" color="success" @click="acceptToFolder">
|
||||
<v-icon icon="mdi-folder-arrow-right"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
Save to Folder
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canAccept"
|
||||
color="error"
|
||||
icon="mdi-close"
|
||||
@click="rejectTransfer"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Reject</v-tooltip>
|
||||
<v-btn v-if="canAccept" color="error" @click="rejectTransfer">
|
||||
<v-icon icon="mdi-close"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Reject</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canCopy"
|
||||
color="success"
|
||||
icon="mdi-eye"
|
||||
@click="showContentDialog = true"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
<v-icon icon="mdi-eye"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
View Content
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canCopy"
|
||||
color="success"
|
||||
icon="mdi-content-copy"
|
||||
@click="handleCopy"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Copy</v-tooltip>
|
||||
<v-btn v-if="canCopy" color="success" @click="handleCopy">
|
||||
<v-icon icon="mdi-content-copy"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Copy</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@@ -315,19 +289,19 @@ const canAccept = computed(() => {
|
||||
props.transfer.status === 'rejected'
|
||||
"
|
||||
color="info"
|
||||
icon="mdi-delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Delete</v-tooltip>
|
||||
<v-icon icon="mdi-delete"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Delete</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canCancel"
|
||||
color="error"
|
||||
icon="mdi-stop"
|
||||
@click="CancelTransfer(props.transfer.id)"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">Cancel</v-tooltip>
|
||||
<v-icon icon="mdi-stop"></v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Cancel</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</div>
|
||||
|
||||
191
frontend/src/components/modals/FileSendModal.vue
Normal file
191
frontend/src/components/modals/FileSendModal.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
// --- Vue 核心 ---
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
// --- Wails & 后端绑定 ---
|
||||
import { Events, Dialogs } from "@wailsio/runtime";
|
||||
import { SendFiles } from "../../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Peer } from "../../../bindings/mesh-drop/internal/discovery/models";
|
||||
|
||||
// --- 属性 & 事件 ---
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
peer: Peer;
|
||||
selectedIp: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "transferStarted"): void;
|
||||
}>();
|
||||
|
||||
// --- 状态 ---
|
||||
const fileList = ref<{ name: string; path: string }[]>([]);
|
||||
|
||||
// --- 计算属性 ---
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
// --- 监听 ---
|
||||
watch(show, (newVal) => {
|
||||
if (newVal) {
|
||||
Events.On("files-dropped", (event) => {
|
||||
const files: string[] = event.data.files || [];
|
||||
files.forEach((f) => {
|
||||
if (!fileList.value.find((existing) => existing.path === f)) {
|
||||
fileList.value.push({
|
||||
name: f.split(/[\/]/).pop() || f,
|
||||
path: f,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Events.Off("files-dropped");
|
||||
fileList.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
// --- 方法 ---
|
||||
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 handleSendFiles = async () => {
|
||||
if (fileList.value.length === 0 || !props.selectedIp) return;
|
||||
const paths = fileList.value.map((f) => f.path);
|
||||
|
||||
try {
|
||||
await SendFiles(props.peer, props.selectedIp, paths);
|
||||
emit("transferStarted");
|
||||
show.value = false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to send files: " + e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="show" width="600" persistent eager>
|
||||
<v-card title="Send Files">
|
||||
<v-card-text>
|
||||
<div
|
||||
v-if="fileList.length === 0"
|
||||
class="drop-zone pa-10 text-center rounded-lg border-dashed"
|
||||
@click="openFileDialog"
|
||||
data-file-drop-target
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-cloud-upload"
|
||||
size="48"
|
||||
color="primary"
|
||||
class="mb-2"
|
||||
></v-icon>
|
||||
<div class="text-body-1 text-medium-emphasis">
|
||||
Click to select files
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<v-list
|
||||
class="mb-4 text-left"
|
||||
border
|
||||
rounded
|
||||
max-height="400"
|
||||
style="overflow-y: auto"
|
||||
data-file-drop-target
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(file, index) in fileList"
|
||||
:key="file.path"
|
||||
:title="file.name"
|
||||
:subtitle="file.path"
|
||||
lines="two"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="handleRemoveFile(index)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
variant="outlined"
|
||||
style="border-style: dashed"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openFileDialog"
|
||||
class="mt-2"
|
||||
>
|
||||
Add more files
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="show = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleSendFiles"
|
||||
:disabled="fileList.length === 0"
|
||||
>
|
||||
Send {{ fileList.length > 0 ? `(${fileList.length})` : "" }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drop-zone {
|
||||
border: 2px dashed #666; /* Use a darker color or theme var */
|
||||
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>
|
||||
71
frontend/src/components/modals/TextSendModal.vue
Normal file
71
frontend/src/components/modals/TextSendModal.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
// --- Vue 核心 ---
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
// --- Wails & 后端绑定 ---
|
||||
import { SendText } from "../../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Peer } from "../../../bindings/mesh-drop/internal/discovery/models";
|
||||
|
||||
// --- 属性 & 事件 ---
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
peer: Peer;
|
||||
selectedIp: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "transferStarted"): void;
|
||||
}>();
|
||||
|
||||
// --- 状态 ---
|
||||
const textContent = ref("");
|
||||
|
||||
// --- 计算属性 ---
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
// --- 方法 ---
|
||||
const executeSendText = async () => {
|
||||
if (!props.selectedIp || !textContent.value) return;
|
||||
|
||||
try {
|
||||
await SendText(props.peer, props.selectedIp, textContent.value);
|
||||
emit("transferStarted");
|
||||
show.value = false;
|
||||
textContent.value = "";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to send text: " + e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" persistent eager>
|
||||
<v-card title="Send Text">
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="textContent"
|
||||
label="Content"
|
||||
placeholder="Type something to send..."
|
||||
rows="4"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="show = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="executeSendText"
|
||||
:disabled="!textContent"
|
||||
>
|
||||
Send
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -14,9 +14,6 @@ import App from "./App.vue";
|
||||
import { createApp } from "vue";
|
||||
|
||||
// Styles
|
||||
// import "unfonts.css";
|
||||
|
||||
// Fonts
|
||||
import "@fontsource-variable/inter";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
10
frontend/src/styles/settings.scss
Normal file
10
frontend/src/styles/settings.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@use "vuetify/settings" with (
|
||||
$body-font-family: (
|
||||
"Inter Variable",
|
||||
sans-serif,
|
||||
),
|
||||
$heading-font-family: (
|
||||
"Inter Variable",
|
||||
sans-serif,
|
||||
)
|
||||
);
|
||||
18
frontend/src/styles/style.css
Normal file
18
frontend/src/styles/style.css
Normal file
@@ -0,0 +1,18 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
-webkit-user-select: none;
|
||||
/* Chrome/Safari/Wails 必须 */
|
||||
user-select: none;
|
||||
/* 标准属性 */
|
||||
cursor: default;
|
||||
/* 鼠标指针变为默认箭头,而不是文本输入的 I 形 */
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
[contenteditable] {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/plugins/index.ts","./src/plugins/vuetify.ts","./src/App.vue","./src/components/MainLayout.vue","./src/components/PeerCard.vue","./src/components/TransferItem.vue","./bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts","./bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts","./bindings/mesh-drop/index.ts","./bindings/mesh-drop/models.ts","./bindings/mesh-drop/internal/config/config.ts","./bindings/mesh-drop/internal/config/index.ts","./bindings/mesh-drop/internal/discovery/index.ts","./bindings/mesh-drop/internal/discovery/models.ts","./bindings/mesh-drop/internal/discovery/service.ts","./bindings/mesh-drop/internal/transfer/index.ts","./bindings/mesh-drop/internal/transfer/models.ts","./bindings/mesh-drop/internal/transfer/service.ts","./bindings/time/index.ts","./bindings/time/models.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/plugins/index.ts","./src/plugins/vuetify.ts","./src/App.vue","./src/components/MainLayout.vue","./src/components/PeerCard.vue","./src/components/SettingsView.vue","./src/components/TransferItem.vue","./src/components/modals/FileSendModal.vue","./src/components/modals/TextSendModal.vue","./bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts","./bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts","./bindings/mesh-drop/index.ts","./bindings/mesh-drop/models.ts","./bindings/mesh-drop/internal/config/config.ts","./bindings/mesh-drop/internal/config/index.ts","./bindings/mesh-drop/internal/config/models.ts","./bindings/mesh-drop/internal/discovery/index.ts","./bindings/mesh-drop/internal/discovery/models.ts","./bindings/mesh-drop/internal/discovery/service.ts","./bindings/mesh-drop/internal/transfer/index.ts","./bindings/mesh-drop/internal/transfer/models.ts","./bindings/mesh-drop/internal/transfer/service.ts","./bindings/time/index.ts","./bindings/time/models.ts"],"version":"5.9.3"}
|
||||
@@ -2,7 +2,7 @@
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import Vue from "@vitejs/plugin-vue";
|
||||
import Vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
|
||||
import Fonts from "unplugin-fonts/vite";
|
||||
|
||||
import wails from "@wailsio/runtime/plugins/vite";
|
||||
|
||||
// Utilities
|
||||
@@ -15,9 +15,14 @@ export default defineConfig({
|
||||
Vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
|
||||
wails("./bindings"),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify(),
|
||||
Vuetify({
|
||||
styles: {
|
||||
configFile: "src/styles/settings.scss",
|
||||
},
|
||||
}),
|
||||
Components(),
|
||||
],
|
||||
optimizeDeps: {
|
||||
|
||||
Reference in New Issue
Block a user