add: settings page
This commit is contained in:
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -9,7 +9,8 @@
|
|||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/main.go"
|
"program": "${workspaceFolder}/main.go",
|
||||||
|
"preLaunchTask": "build frontend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
14
.vscode/tasks.json
vendored
Normal file
14
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "build:dev",
|
||||||
|
"path": "frontend",
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "build frontend",
|
||||||
|
"detail": "vite build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,4 +6,9 @@ TODO
|
|||||||
- [x] 取消传输
|
- [x] 取消传输
|
||||||
- [x] 多文件发送
|
- [x] 多文件发送
|
||||||
- [ ] 加密传输
|
- [ ] 加密传输
|
||||||
- [ ] 设置页面:默认保存路径
|
- [x] 设置页面
|
||||||
|
- [x] 默认保存路径
|
||||||
|
- [x] 设置主机名
|
||||||
|
- [x] 保存历史
|
||||||
|
- [x] 自动接收
|
||||||
|
- [ ] 传输加密
|
||||||
|
|||||||
@@ -5,6 +5,22 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
export function GetAutoAccept(): $CancellablePromise<boolean> {
|
||||||
|
return $Call.ByID(2605668438);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetHostName(): $CancellablePromise<string> {
|
||||||
|
return $Call.ByID(972342140);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetID(): $CancellablePromise<string> {
|
||||||
|
return $Call.ByID(4240411568);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetSaveHistory(): $CancellablePromise<boolean> {
|
||||||
|
return $Call.ByID(2178923392);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetSavePath(): $CancellablePromise<string> {
|
export function GetSavePath(): $CancellablePromise<string> {
|
||||||
return $Call.ByID(4081533263);
|
return $Call.ByID(4081533263);
|
||||||
}
|
}
|
||||||
@@ -16,6 +32,18 @@ export function Save(): $CancellablePromise<void> {
|
|||||||
return $Call.ByID(3089450934);
|
return $Call.ByID(3089450934);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SetAutoAccept(autoAccept: boolean): $CancellablePromise<void> {
|
||||||
|
return $Call.ByID(3371961138, autoAccept);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetHostName(hostName: string): $CancellablePromise<void> {
|
||||||
|
return $Call.ByID(1580131496, hostName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetSaveHistory(saveHistory: boolean): $CancellablePromise<void> {
|
||||||
|
return $Call.ByID(3779587628, saveHistory);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetSavePath 修改配置
|
* SetSavePath 修改配置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,11 +46,6 @@ export class Peer {
|
|||||||
* Port 是文件传输服务的监听端口。
|
* Port 是文件传输服务的监听端口。
|
||||||
*/
|
*/
|
||||||
"port": number;
|
"port": number;
|
||||||
|
|
||||||
/**
|
|
||||||
* IsOnline 标记该端点当前是否活跃 (UI 渲染用)。
|
|
||||||
*/
|
|
||||||
"is_online": boolean;
|
|
||||||
"os": OS;
|
"os": OS;
|
||||||
|
|
||||||
/** Creates a new Peer instance. */
|
/** Creates a new Peer instance. */
|
||||||
@@ -67,9 +62,6 @@ export class Peer {
|
|||||||
if (!("port" in $$source)) {
|
if (!("port" in $$source)) {
|
||||||
this["port"] = 0;
|
this["port"] = 0;
|
||||||
}
|
}
|
||||||
if (!("is_online" in $$source)) {
|
|
||||||
this["is_online"] = false;
|
|
||||||
}
|
|
||||||
if (!("os" in $$source)) {
|
if (!("os" in $$source)) {
|
||||||
this["os"] = OS.$zero;
|
this["os"] = OS.$zero;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ export function GetID(): $CancellablePromise<string> {
|
|||||||
return $Call.ByID(1539451205);
|
return $Call.ByID(1539451205);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetName(): $CancellablePromise<string> {
|
|
||||||
return $Call.ByID(1578367131);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GetPeerByIP(ip: string): $CancellablePromise<$models.Peer | null> {
|
export function GetPeerByIP(ip: string): $CancellablePromise<$models.Peer | null> {
|
||||||
return $Call.ByID(1626825408, ip).then(($result: any) => {
|
return $Call.ByID(1626825408, ip).then(($result: any) => {
|
||||||
return $$createType1($result);
|
return $$createType1($result);
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export function GetTransferList(): $CancellablePromise<($models.Transfer | null)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LoadHistory(): $CancellablePromise<void> {
|
||||||
|
return $Call.ByID(2987999795);
|
||||||
|
}
|
||||||
|
|
||||||
export function NotifyTransferListUpdate(): $CancellablePromise<void> {
|
export function NotifyTransferListUpdate(): $CancellablePromise<void> {
|
||||||
return $Call.ByID(1220032142);
|
return $Call.ByID(1220032142);
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,10 @@ export function ResolvePendingRequest(id: string, accept: boolean, savePath: str
|
|||||||
return $Call.ByID(207902967, id, accept, savePath);
|
return $Call.ByID(207902967, id, accept, savePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SaveHistory(): $CancellablePromise<void> {
|
||||||
|
return $Call.ByID(713135400);
|
||||||
|
}
|
||||||
|
|
||||||
export function SendFile(target: discovery$0.Peer | null, targetIP: string, filePath: string): $CancellablePromise<void> {
|
export function SendFile(target: discovery$0.Peer | null, targetIP: string, filePath: string): $CancellablePromise<void> {
|
||||||
return $Call.ByID(2954589433, target, targetIP, filePath);
|
return $Call.ByID(2954589433, target, targetIP, filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,4 @@ import MainLayout from "./components/MainLayout.vue";
|
|||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style></style>
|
||||||
body,
|
|
||||||
#app {
|
|
||||||
font-family: "Noto Sans", "Roboto", "Segoe UI", sans-serif !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -7,11 +7,22 @@ import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
|||||||
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
|
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
|
||||||
import { Events } from "@wailsio/runtime";
|
import { Events } from "@wailsio/runtime";
|
||||||
import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/service";
|
import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/service";
|
||||||
|
import {
|
||||||
|
GetSavePath,
|
||||||
|
SetSavePath,
|
||||||
|
GetHostName,
|
||||||
|
SetHostName,
|
||||||
|
GetAutoAccept,
|
||||||
|
SetAutoAccept,
|
||||||
|
GetSaveHistory,
|
||||||
|
SetSaveHistory,
|
||||||
|
} from "../../bindings/mesh-drop/internal/config/config";
|
||||||
|
import { Dialogs } from "@wailsio/runtime";
|
||||||
|
|
||||||
const peers = ref<Peer[]>([]);
|
const peers = ref<Peer[]>([]);
|
||||||
const transferList = ref<Transfer[]>([]);
|
const transferList = ref<Transfer[]>([]);
|
||||||
const activeKey = ref("discover");
|
const activeKey = ref("discover");
|
||||||
const drawer = ref(true); // Control drawer visibility
|
const drawer = ref(true);
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
|
|
||||||
// 监听窗口大小变化更新 isMobile
|
// 监听窗口大小变化更新 isMobile
|
||||||
@@ -26,6 +37,12 @@ onMounted(async () => {
|
|||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
drawer.value = false;
|
drawer.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
savePath.value = await GetSavePath();
|
||||||
|
hostName.value = await GetHostName();
|
||||||
|
autoAccept.value = await GetAutoAccept();
|
||||||
|
saveHistory.value = await GetSaveHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
@@ -73,8 +90,34 @@ const menuItems = computed(() => [
|
|||||||
icon: "mdi-inbox",
|
icon: "mdi-inbox",
|
||||||
badge: pendingCount.value > 0 ? pendingCount.value : null,
|
badge: pendingCount.value > 0 ? pendingCount.value : null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
value: "settings",
|
||||||
|
icon: "mdi-cog",
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// --- 设置 ---
|
||||||
|
const savePath = ref("");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hostName = ref("");
|
||||||
|
const autoAccept = ref(false);
|
||||||
|
const saveHistory = ref(false);
|
||||||
|
|
||||||
// --- 操作 ---
|
// --- 操作 ---
|
||||||
|
|
||||||
const handleMenuClick = (key: string) => {
|
const handleMenuClick = (key: string) => {
|
||||||
@@ -87,17 +130,17 @@ const handleMenuClick = (key: string) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-layout>
|
<v-layout>
|
||||||
<!-- App Bar for Mobile -->
|
<!-- 小屏幕抽屉 -->
|
||||||
<v-app-bar v-if="isMobile" border flat>
|
<v-app-bar v-if="isMobile" border flat>
|
||||||
<v-toolbar-title class="text-primary font-weight-bold"
|
<v-toolbar-title class="text-primary font-weight-bold"
|
||||||
>Mesh Drop</v-toolbar-title
|
>Mesh Drop</v-toolbar-title
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template #append>
|
||||||
<v-btn icon="mdi-menu" @click="drawer = !drawer"></v-btn>
|
<v-btn icon="mdi-menu" @click="drawer = !drawer"></v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<!-- Navigation Drawer -->
|
<!-- 导航抽屉 -->
|
||||||
<v-navigation-drawer v-model="drawer" :permanent="!isMobile">
|
<v-navigation-drawer v-model="drawer" :permanent="!isMobile">
|
||||||
<div class="pa-4" v-if="!isMobile">
|
<div class="pa-4" v-if="!isMobile">
|
||||||
<div class="text-h6 text-primary font-weight-bold">Mesh Drop</div>
|
<div class="text-h6 text-primary font-weight-bold">Mesh Drop</div>
|
||||||
@@ -113,7 +156,7 @@ const handleMenuClick = (key: string) => {
|
|||||||
rounded="xl"
|
rounded="xl"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template #prepend>
|
||||||
<v-icon :icon="item.icon"></v-icon>
|
<v-icon :icon="item.icon"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -131,10 +174,10 @@ const handleMenuClick = (key: string) => {
|
|||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- 主内容 -->
|
||||||
<v-main>
|
<v-main>
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
<!-- Discover View -->
|
<!-- 发现视图 -->
|
||||||
<div v-show="activeKey === 'discover'">
|
<div v-show="activeKey === 'discover'">
|
||||||
<div v-if="peers.length > 0" class="peer-grid">
|
<div v-if="peers.length > 0" class="peer-grid">
|
||||||
<div v-for="peer in peers" :key="peer.id">
|
<div v-for="peer in peers" :key="peer.id">
|
||||||
@@ -160,7 +203,7 @@ const handleMenuClick = (key: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transfers View -->
|
<!-- 传输视图 -->
|
||||||
<div v-show="activeKey === 'transfers'">
|
<div v-show="activeKey === 'transfers'">
|
||||||
<div v-if="transferList.length > 0">
|
<div v-if="transferList.length > 0">
|
||||||
<TransferItem
|
<TransferItem
|
||||||
@@ -177,6 +220,69 @@ const handleMenuClick = (key: string) => {
|
|||||||
<div class="text-grey">No transfers yet</div>
|
<div class="text-grey">No transfers yet</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置视图 -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
|||||||
@@ -208,14 +208,14 @@ const handleSendFiles = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-card hover link class="peer-card pa-2">
|
<v-card hover link class="peer-card pa-2">
|
||||||
<template v-slot:title>
|
<template #title>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
|
<v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
|
||||||
<span class="text-subtitle-1 font-weight-bold">{{ peer.name }}</span>
|
<span class="text-subtitle-1 font-weight-bold">{{ peer.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:text>
|
<template #text>
|
||||||
<div class="d-flex align-center flex-wrap ga-2 mt-2">
|
<div class="d-flex align-center flex-wrap ga-2 mt-2">
|
||||||
<v-icon icon="mdi-web" size="20" class="text-medium-emphasis"></v-icon>
|
<v-icon icon="mdi-web" size="20" class="text-medium-emphasis"></v-icon>
|
||||||
|
|
||||||
@@ -241,9 +241,9 @@ const handleSendFiles = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:actions>
|
<template #actions>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
block
|
block
|
||||||
@@ -252,10 +252,10 @@ const handleSendFiles = () => {
|
|||||||
:disabled="ips.length === 0"
|
:disabled="ips.length === 0"
|
||||||
append-icon="mdi-chevron-down"
|
append-icon="mdi-chevron-down"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template #prepend>
|
||||||
<v-icon icon="mdi-send"></v-icon>
|
<v-icon icon="mdi-send"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
Send...
|
Send
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
@@ -265,7 +265,7 @@ const handleSendFiles = () => {
|
|||||||
:value="item.value"
|
:value="item.value"
|
||||||
@click="handleAction(item.value)"
|
@click="handleAction(item.value)"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template #prepend>
|
||||||
<v-icon :icon="item.icon"></v-icon>
|
<v-icon :icon="item.icon"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||||
@@ -312,7 +312,7 @@ const handleSendFiles = () => {
|
|||||||
:subtitle="file.path"
|
:subtitle="file.path"
|
||||||
lines="two"
|
lines="two"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template #append>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon="mdi-delete"
|
icon="mdi-delete"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -261,29 +261,16 @@ const canAccept = computed(() => {
|
|||||||
<v-btn
|
<v-btn
|
||||||
v-if="canAccept"
|
v-if="canAccept"
|
||||||
color="success"
|
color="success"
|
||||||
icon="mdi-check"
|
icon="mdi-content-save"
|
||||||
@click="acceptTransfer"
|
@click="acceptTransfer"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
|
|
||||||
<v-menu v-if="canAccept && props.transfer.content_type !== 'text'">
|
<v-btn
|
||||||
<template v-slot:activator="{ props }">
|
v-if="canAccept"
|
||||||
<v-btn
|
color="success"
|
||||||
color="success"
|
icon="mdi-folder-arrow-right"
|
||||||
icon="mdi-chevron-down"
|
@click="acceptToFolder"
|
||||||
v-bind="props"
|
></v-btn>
|
||||||
></v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
v-for="(item, index) in dropdownItems"
|
|
||||||
:key="index"
|
|
||||||
:value="item.value"
|
|
||||||
@click="handleSelect(item.value)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="canAccept"
|
v-if="canAccept"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,9 +20,15 @@ type WindowState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
mu sync.RWMutex
|
v *viper.Viper
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
WindowState WindowState `mapstructure:"window_state"`
|
WindowState WindowState `mapstructure:"window_state"`
|
||||||
|
ID string `mapstructure:"id"`
|
||||||
SavePath string `mapstructure:"save_path"`
|
SavePath string `mapstructure:"save_path"`
|
||||||
|
HostName string `mapstructure:"host_name"`
|
||||||
|
AutoAccept bool `mapstructure:"auto_accept"`
|
||||||
|
SaveHistory bool `mapstructure:"save_history"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认窗口配置
|
// 默认窗口配置
|
||||||
@@ -32,7 +39,7 @@ var defaultWindowState = WindowState{
|
|||||||
Y: -1,
|
Y: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigDir() string {
|
func GetConfigDir() string {
|
||||||
configPath, err := os.UserConfigDir()
|
configPath, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
configPath = "/tmp"
|
configPath = "/tmp"
|
||||||
@@ -40,7 +47,7 @@ func getConfigDir() string {
|
|||||||
return filepath.Join(configPath, "mesh-drop")
|
return filepath.Join(configPath, "mesh-drop")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserHomeDir() string {
|
func GetUserHomeDir() string {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "/tmp"
|
return "/tmp"
|
||||||
@@ -50,7 +57,8 @@ func getUserHomeDir() string {
|
|||||||
|
|
||||||
// New 读取配置
|
// New 读取配置
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
configDir := getConfigDir()
|
v := viper.New()
|
||||||
|
configDir := GetConfigDir()
|
||||||
err := os.MkdirAll(configDir, 0755)
|
err := os.MkdirAll(configDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create config directory", "error", err)
|
slog.Error("Failed to create config directory", "error", err)
|
||||||
@@ -58,15 +66,21 @@ func Load() *Config {
|
|||||||
configFile := filepath.Join(configDir, "config.json")
|
configFile := filepath.Join(configDir, "config.json")
|
||||||
|
|
||||||
// 设置默认值
|
// 设置默认值
|
||||||
defaultSavePath := filepath.Join(getUserHomeDir(), "Downloads")
|
defaultSavePath := filepath.Join(GetUserHomeDir(), "Downloads")
|
||||||
viper.SetDefault("window_state", defaultWindowState)
|
v.SetDefault("window_state", defaultWindowState)
|
||||||
viper.SetDefault("save_path", defaultSavePath)
|
v.SetDefault("save_path", defaultSavePath)
|
||||||
|
defaultHostName, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
defaultHostName = "localhost"
|
||||||
|
}
|
||||||
|
v.SetDefault("host_name", defaultHostName)
|
||||||
|
v.SetDefault("id", uuid.New().String())
|
||||||
|
|
||||||
viper.SetConfigFile(configFile)
|
v.SetConfigFile(configFile)
|
||||||
viper.SetConfigType("json")
|
v.SetConfigType("json")
|
||||||
|
|
||||||
// 尝试读取配置
|
// 尝试读取配置
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := v.ReadInConfig(); err != nil {
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
slog.Info("Config file not found, using defaults")
|
slog.Info("Config file not found, using defaults")
|
||||||
} else {
|
} else {
|
||||||
@@ -75,10 +89,12 @@ func Load() *Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
if err := viper.Unmarshal(&config); err != nil {
|
if err := v.Unmarshal(&config); err != nil {
|
||||||
slog.Error("Failed to unmarshal config", "error", err)
|
slog.Error("Failed to unmarshal config", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.v = v
|
||||||
|
|
||||||
return &config
|
return &config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +103,12 @@ func (c *Config) Save() error {
|
|||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
configDir := getConfigDir()
|
configDir := GetConfigDir()
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := viper.WriteConfig(); err != nil {
|
if err := c.v.WriteConfig(); err != nil {
|
||||||
slog.Error("Failed to write config", "error", err)
|
slog.Error("Failed to write config", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -106,7 +122,8 @@ func (c *Config) SetSavePath(savePath string) {
|
|||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
c.SavePath = savePath
|
c.SavePath = savePath
|
||||||
viper.Set("save_path", savePath)
|
c.v.Set("save_path", savePath)
|
||||||
|
_ = os.MkdirAll(savePath, 0755)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetSavePath() string {
|
func (c *Config) GetSavePath() string {
|
||||||
@@ -114,3 +131,48 @@ func (c *Config) GetSavePath() string {
|
|||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
return c.SavePath
|
return c.SavePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetHostName(hostName string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.HostName = hostName
|
||||||
|
c.v.Set("host_name", hostName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetHostName() string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.HostName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetID() string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetAutoAccept(autoAccept bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.AutoAccept = autoAccept
|
||||||
|
c.v.Set("auto_accept", autoAccept)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetAutoAccept() bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.AutoAccept
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetSaveHistory(saveHistory bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.SaveHistory = saveHistory
|
||||||
|
c.v.Set("save_history", saveHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetSaveHistory() bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.SaveHistory
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ type Peer struct {
|
|||||||
// Port 是文件传输服务的监听端口。
|
// Port 是文件传输服务的监听端口。
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
|
|
||||||
// IsOnline 标记该端点当前是否活跃 (UI 渲染用)。
|
|
||||||
IsOnline bool `json:"is_online"`
|
|
||||||
|
|
||||||
OS OS `json:"os"`
|
OS OS `json:"os"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"mesh-drop/internal/config"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +23,7 @@ type Service struct {
|
|||||||
app *application.App
|
app *application.App
|
||||||
|
|
||||||
ID string
|
ID string
|
||||||
Name string
|
config *config.Config
|
||||||
FileServerPort int
|
FileServerPort int
|
||||||
|
|
||||||
// key 使用 peer.id 和 peer.ip 组合而成的 hash
|
// key 使用 peer.id 和 peer.ip 组合而成的 hash
|
||||||
@@ -37,33 +35,11 @@ func init() {
|
|||||||
application.RegisterEvent[[]Peer]("peers:update")
|
application.RegisterEvent[[]Peer]("peers:update")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOrInitDeviceID 获取或初始化设备 ID
|
func NewService(config *config.Config, app *application.App, port int) *Service {
|
||||||
func getOrInitDeviceID() string {
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return uuid.New().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
appDir := filepath.Join(configDir, "mesh-drop")
|
|
||||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
|
||||||
return uuid.New().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
idFile := filepath.Join(appDir, "device_id")
|
|
||||||
if data, err := os.ReadFile(idFile); err == nil {
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
_ = os.WriteFile(idFile, []byte(id), 0644)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(app *application.App, name string, port int) *Service {
|
|
||||||
return &Service{
|
return &Service{
|
||||||
app: app,
|
app: app,
|
||||||
ID: getOrInitDeviceID(),
|
ID: config.GetID(),
|
||||||
Name: name,
|
config: config,
|
||||||
FileServerPort: port,
|
FileServerPort: port,
|
||||||
peers: make(map[string]*Peer),
|
peers: make(map[string]*Peer),
|
||||||
}
|
}
|
||||||
@@ -79,7 +55,7 @@ func (s *Service) startBroadcasting() {
|
|||||||
}
|
}
|
||||||
packet := PresencePacket{
|
packet := PresencePacket{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
Name: s.Name,
|
Name: s.config.GetHostName(),
|
||||||
Port: s.FileServerPort,
|
Port: s.FileServerPort,
|
||||||
OS: OS(runtime.GOOS),
|
OS: OS(runtime.GOOS),
|
||||||
}
|
}
|
||||||
@@ -163,7 +139,7 @@ func (s *Service) startListening() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理心跳包
|
// handleHeartbeat 处理心跳包
|
||||||
func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
|
func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
|
||||||
s.peersMutex.Lock()
|
s.peersMutex.Lock()
|
||||||
|
|
||||||
@@ -186,13 +162,14 @@ func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
|
|||||||
slog.Info("New device found", "name", pkt.Name, "ip", ip, "component", "discovery")
|
slog.Info("New device found", "name", pkt.Name, "ip", ip, "component", "discovery")
|
||||||
} else {
|
} else {
|
||||||
// 更新节点
|
// 更新节点
|
||||||
|
peer.Name = pkt.Name
|
||||||
|
peer.OS = pkt.OS
|
||||||
peer.Routes[ip] = &RouteState{
|
peer.Routes[ip] = &RouteState{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
LastSeen: time.Now(),
|
LastSeen: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.IsOnline = true
|
|
||||||
s.peersMutex.Unlock()
|
s.peersMutex.Unlock()
|
||||||
|
|
||||||
// 触发前端更新 (防抖逻辑可以之后加,这里每次变动都推)
|
// 触发前端更新 (防抖逻辑可以之后加,这里每次变动都推)
|
||||||
@@ -260,10 +237,6 @@ func (s *Service) GetPeers() []Peer {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetName() string {
|
|
||||||
return s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetID() string {
|
func (s *Service) GetID() string {
|
||||||
return s.ID
|
return s.ID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
|
|||||||
taskID,
|
taskID,
|
||||||
Sender{
|
Sender{
|
||||||
ID: s.discoveryService.GetID(),
|
ID: s.discoveryService.GetID(),
|
||||||
Name: s.discoveryService.GetName(),
|
Name: s.config.GetHostName(),
|
||||||
},
|
},
|
||||||
WithFileName(filepath.Base(filePath)),
|
WithFileName(filepath.Base(filePath)),
|
||||||
WithFileSize(stat.Size()),
|
WithFileSize(stat.Size()),
|
||||||
@@ -105,7 +105,7 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath
|
|||||||
taskID,
|
taskID,
|
||||||
Sender{
|
Sender{
|
||||||
ID: s.discoveryService.GetID(),
|
ID: s.discoveryService.GetID(),
|
||||||
Name: s.discoveryService.GetName(),
|
Name: s.config.GetHostName(),
|
||||||
},
|
},
|
||||||
WithFileName(filepath.Base(folderPath)),
|
WithFileName(filepath.Base(folderPath)),
|
||||||
WithFileSize(size),
|
WithFileSize(size),
|
||||||
@@ -159,7 +159,7 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string)
|
|||||||
taskID,
|
taskID,
|
||||||
Sender{
|
Sender{
|
||||||
ID: s.discoveryService.GetID(),
|
ID: s.discoveryService.GetID(),
|
||||||
Name: s.discoveryService.GetName(),
|
Name: s.config.GetHostName(),
|
||||||
},
|
},
|
||||||
WithFileSize(int64(len(text))),
|
WithFileSize(int64(len(text))),
|
||||||
WithType(TransferTypeSend),
|
WithType(TransferTypeSend),
|
||||||
|
|||||||
45
internal/transfer/history.go
Normal file
45
internal/transfer/history.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"mesh-drop/internal/config"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) SaveHistory() {
|
||||||
|
configDir := config.GetConfigDir()
|
||||||
|
historyPath := filepath.Join(configDir, "history.json")
|
||||||
|
historyJson, err := json.Marshal(s.GetTransferList())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(historyPath, os.O_CREATE|os.O_RDWR, os.FileMode(0644))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
_, err = file.Write(historyJson)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to write history", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) LoadHistory() {
|
||||||
|
configDir := config.GetConfigDir()
|
||||||
|
historyPath := filepath.Join(configDir, "history.json")
|
||||||
|
file, err := os.Open(historyPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
var history []Transfer
|
||||||
|
err = json.NewDecoder(file).Decode(&history)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, item := range history {
|
||||||
|
s.StoreTransferToList(&item)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,13 @@ func (s *Service) handleAsk(c *gin.Context) {
|
|||||||
task.DecisionChan = make(chan Decision)
|
task.DecisionChan = make(chan Decision)
|
||||||
s.StoreTransferToList(&task)
|
s.StoreTransferToList(&task)
|
||||||
|
|
||||||
// 通知 Wails 前端
|
if s.config.GetAutoAccept() {
|
||||||
|
task.DecisionChan <- Decision{
|
||||||
|
ID: task.ID,
|
||||||
|
Accepted: true,
|
||||||
|
SavePath: s.config.GetSavePath(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 等待用户决策或发送端放弃
|
// 等待用户决策或发送端放弃
|
||||||
select {
|
select {
|
||||||
@@ -53,15 +59,14 @@ func (s *Service) handleAsk(c *gin.Context) {
|
|||||||
task.SavePath = decision.SavePath
|
task.SavePath = decision.SavePath
|
||||||
token := uuid.New().String()
|
token := uuid.New().String()
|
||||||
task.Token = token
|
task.Token = token
|
||||||
|
c.JSON(http.StatusOK, TransferAskResponse{
|
||||||
|
ID: task.ID,
|
||||||
|
Accepted: decision.Accepted,
|
||||||
|
Token: task.Token,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
task.Status = TransferStatusRejected
|
task.Status = TransferStatusRejected
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, TransferAskResponse{
|
|
||||||
ID: task.ID,
|
|
||||||
Accepted: decision.Accepted,
|
|
||||||
Token: task.Token,
|
|
||||||
})
|
|
||||||
|
|
||||||
case <-c.Request.Context().Done():
|
case <-c.Request.Context().Done():
|
||||||
// 发送端放弃
|
// 发送端放弃
|
||||||
task.Status = TransferStatusCanceled
|
task.Status = TransferStatusCanceled
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -38,15 +38,18 @@ func main() {
|
|||||||
|
|
||||||
// 文件传输端口
|
// 文件传输端口
|
||||||
port := 9989
|
port := 9989
|
||||||
name, _ := os.Hostname()
|
|
||||||
|
|
||||||
// 初始化发现服务
|
// 初始化发现服务
|
||||||
discoveryService := discovery.NewService(app, name, port)
|
discoveryService := discovery.NewService(conf, app, port)
|
||||||
discoveryService.Start()
|
discoveryService.Start()
|
||||||
|
|
||||||
// 初始化传输服务
|
// 初始化传输服务
|
||||||
transferService := transfer.NewService(conf, app, port, discoveryService)
|
transferService := transfer.NewService(conf, app, port, discoveryService)
|
||||||
transferService.Start()
|
transferService.Start()
|
||||||
|
// 加载传输历史
|
||||||
|
if conf.GetSaveHistory() {
|
||||||
|
transferService.LoadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("Backend Service Started", "discovery_port", discovery.DiscoveryPort, "transfer_port", port)
|
slog.Info("Backend Service Started", "discovery_port", discovery.DiscoveryPort, "transfer_port", port)
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ func main() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 窗口文件拖拽事件
|
||||||
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
|
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
|
||||||
files := event.Context().DroppedFiles()
|
files := event.Context().DroppedFiles()
|
||||||
details := event.Context().DropTargetDetails()
|
details := event.Context().DropTargetDetails()
|
||||||
@@ -75,7 +79,9 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 窗口关闭事件
|
||||||
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
||||||
|
// 保存配置
|
||||||
x, y := window.Position()
|
x, y := window.Position()
|
||||||
width, height := window.Size()
|
width, height := window.Size()
|
||||||
conf.WindowState = config.WindowState{
|
conf.WindowState = config.WindowState{
|
||||||
@@ -85,6 +91,10 @@ func main() {
|
|||||||
Height: height,
|
Height: height,
|
||||||
}
|
}
|
||||||
_ = conf.Save()
|
_ = conf.Save()
|
||||||
|
// 保存传输历史
|
||||||
|
if conf.GetSaveHistory() {
|
||||||
|
transferService.SaveHistory()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
application.RegisterEvent[FilesDroppedEvent]("files-dropped")
|
application.RegisterEvent[FilesDroppedEvent]("files-dropped")
|
||||||
|
|||||||
Reference in New Issue
Block a user