Compare commits

...
22 Commits
Author SHA1 Message Date
黑先森 bf1403bf07 修复winIp无法自启的bug 2024-11-11 16:50:18 +08:00
黑先森 7a181200f3 增加下载内核的提示 2024-11-11 16:28:23 +08:00
黑先森 9413449c8e 合并 内核外工具 到 增强工具窗口, 新增防火墙关闭 跃点修改 ping测试功能 2024-11-11 16:01:41 +08:00
黑先森 ef172e4d4c 增加base64编码分享和导入 界面配置 #1
优化是否联机成功的判断,优化本地存储日志数量
2024-11-05 18:30:08 +08:00
leizi97 1a5d2cc8fc 托盘图标增加主题切换功能,调整成员列表延迟至前方,增加nat类型查看,修复自定义网卡无法保存信息的问题 2024-11-04 22:38:53 +08:00
leizi97 cf351b8708 增加根据windows主题切换主题颜色的功能,优化高级配置界面显示,增加网卡名称配置 2024-11-04 21:23:20 +08:00
leizi97 ddf4c32ae6 主题颜色跟随window进行切换 2024-11-04 01:05:51 +08:00
leizi97 3adf64e251 增加标题版本号,系统托盘按钮顺序切换 2024-11-03 23:09:13 +08:00
leizi97 4e4ebf1246 UAC开机自启版本回退 2024-11-03 22:17:59 +08:00
黑先森 4296f4a338 修复与功能更新
1.解决UAC开机自启失败的问题
2.联机成功后右下角图标增加 ip地址的 tooltip提示
3.高级选项: 新增软件启动自动联机的功能开关,搭配修复后的开机自启功能,可实现类windows服务的无感联机
4.高级选项:增加日志配置
2024-11-01 18:36:56 +08:00
leizi97 60078f00f7 增加宽度,减少间隔,避免个别电脑出现布局错误 2024-10-29 23:31:11 +08:00
leizi97 d0567e96b5 新增配置文件启动,修复部分文本缺失 2024-10-29 22:48:56 +08:00
黑先森 8c911f47f9 优化日志输出和提示 成员列表显示bug修复 优化打包后的目录结构,清爽干净 2024-10-25 18:40:47 +08:00
leizi97 f3fc37957c 增加子网代理和部分高级配置功能 升级tauri到2.0.6 2024-10-23 21:58:36 +08:00
黑先森 a4d6fe6e95 封装二级弹窗方法,新增子网代理和高级配置按钮,新增子网代理页面 2024-10-23 18:37:11 +08:00
黑先森 9b883d686d 优化界面css,增加部分页面autostart权限
-- 更新 element-plus 2.7.1 -> 2.8.6 并修复升级后的问题
-- 升级autostart插件为2.0.1 win10测试开机自启成功
2024-10-22 19:09:45 +08:00
黑先森 2ac0afb847 插件版本更新,rust打包使用1.77.2,支持win7 2024-10-21 10:51:16 +08:00
黑先森 a04fae725d #improvement 联机成功后右下角角标增加绿点,优化二级窗口创建逻辑修复控制台报错,增加开机自启功能,禁用p2p默认改为不禁用 2024-10-17 19:30:05 +08:00
黑先森 3150878dbd #Fix 紧急修复日志无法打开的问题 2024-10-16 17:21:49 +08:00
黑先森 0467074eec #Fix 内置WinIPBroadcast默认开启,成员列表显示优化 1.0.5发布 2024-10-16 16:33:45 +08:00
黑先森 2e598ac026 优化成员列表表头为中文,修复日志接收失败的问题 2024-10-16 11:18:10 +08:00
luoleixin 4bb1216a07 增加日志窗口,增加成员弹窗,优化按钮和布局,增加仓库地址,增加license,优化退出代码,退出时停止子进程,屏蔽右键菜单 2024-10-15 20:49:23 +08:00
45 changed files with 3210 additions and 812 deletions
+5 -1
View File
@@ -9,4 +9,8 @@ dist
.fleet .fleet
.idea .idea
.vscode .vscode
release release
src-tauri/easytier-cli.exe
src-tauri/easytier-core.exe
src-tauri/Packet.dll
src-tauri/wintun.dll
+73
View File
@@ -0,0 +1,73 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2023 sunsijie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+58 -8
View File
@@ -1,10 +1,60 @@
# EasyiterGame # [EasyTier游戏联机启动器](https://github.com/EasyTier/EasytierGame)
基于easytier的简易游戏联机客户端
1. 开发环境如何运行 ## 简介
拉取仓库,使用命令行 pnpm install 安装包
安装完成后, 使用 pnpm tauri dev 进行rust编译 等待启动 EasyTierGame游戏联机启动器,由`nuxt3` `typescript` `rust` `tauri` 开发
具有简易的界面,附带最新版easytier内核,联机游玩的时候无论是心理上和使用上都能给予您最舒服的体验,同时支持自定义配置文件启动,满足各种需求
## 下载
Github
Releases [https://github.com/EasyTier/EasytierGame/releases](https://github.com/EasyTier/EasytierGame/releases)
- 只有绿色zip包,个人不喜欢安装包各种写注册表,解压即用就行,目录清爽干净
![game-step1](/assets/game-step1.png)
## 使用教程
- 第一次使用,输入一个“主机名”点击启动联机即可,后续可以自建服务器或者使用爱心群友提供的服务器
![game-step2](/assets/game-step2.png)
![game-step3](/assets/game-step3.png)
- 高级选项里有一些特殊配置,可以自行选择
![game-step4](/assets/game-step4.png)
- 如果还是无法满足您的需求,可以使用配置文件进行启动,具体如何配置,可以查看文档[配置文件](/guide/network/config-file.html)
![game-step5](/assets/game-step5.png)
- easytier内核升级后,可以点击更新插件按钮就可以进行更新,但是需要出国,如果无法更新,可以在群里获取
![game-step6](/assets/game-step6.png)
- 1.1.4更新了 配置分享功能 可以与朋友之间分享配置,方便联机
![game-step7](/assets/game-step7.png)
- 1.1.5 合并额外插件于 "增强工具" 窗口 新增了网卡跃点设置 关闭防火墙功能 ping功能
![game-step8](/assets/game-step8.png)
![game-step9](/assets/game-step9.png)
## 特性
- 基于easytier组网工具开发,界面清晰简单
- 自带“更新”按钮,在easytier组网工具发布新版本时,点击更新即可(需要出国工具)
- 第一次使用,输入一个“主机名”点击启动联机即可,后续可以自建服务器或者使用群友的服务器
- 配置简单,也含有高级功能,同时也支持自定义配置文件启动
- 默认开启 **WinIPBroadcast** 不再怕联机找不到房间(例如:无主之地3)
- 已经测试 **艾尔登法环学习版**,**无主之地3**,**深岩银河**,**怪物猎人世界** 等都可稳定联机游玩
## 系统支持
支持Windows 11 、Windows 10 、 Windows 7
## 请不要将本程序和仓库代码用于任何违法用途,由此产生的一切后果,仓库所有者和参与开发的人员不承担任何责任
2. 生产环境发布
执行 pnpm tauri build
产物位于 src-tauri/target/release 下 easytier-game.exe
+5
View File
@@ -4,4 +4,9 @@
</NuxtLayout> </NuxtLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import useMainStore from "@/stores/index";
import { initTheme } from "@/composables/theme";
const mainStore = useMainStore();
// console.log(mainStore.theme)
initTheme(mainStore.theme);
</script> </script>
+9 -18
View File
@@ -1,14 +1,14 @@
@tailwind base; /* @tailwind base; */
/* /*
用于进行主题更换,如果要更换elemenet-plus-ui的主题记得加上!important,不然更换会失败 用于进行主题更换,如果要更换elemenet-plus-ui的主题记得加上!important,不然更换会失败
*/ */
@layer base { /* @layer base {
html { html {
font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti, "Microsoft YaHei"; font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti, "Microsoft YaHei";
} }
} } */
html, html,
body { body {
@@ -19,25 +19,16 @@ body {
* { * {
outline: none; outline: none;
} }
body {
background-color: #fff;
}
html.dark body {
background-color: #000;
}
#__easytier { #__easytier {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background-color: #fff;
box-sizing: border-box; box-sizing: border-box;
padding: 3px 5px; padding: 3px 5px;
} }
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background-color: #0003;
border-radius: 10px;
transition: all 0.2s ease-in-out;
}
::-webkit-scrollbar-track {
border-radius: 10px;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+8
View File
@@ -0,0 +1,8 @@
// const infoDialog = new WebviewWindow("log", {
// title: "日志",
// resizable: false,
// width: 600,
// height: 400,
// parent: appWindow,
// url: "/log"
// });
+48
View File
@@ -0,0 +1,48 @@
import { invoke } from "@tauri-apps/api/core";
import { Command } from "@tauri-apps/plugin-shell";
import { ElMessage, type TabPaneName } from "element-plus";
import useMainStore from "@/stores/index";
const getWinIpBroadcastPid = async () => {
const mainStore = useMainStore();
const pid = await invoke("search_pid_by_pname", { target_process_name: "WinIPBroadcast" });
mainStore.winipBcPid = (pid as number) || 0;
if (mainStore.winipBcPid && mainStore.winipBcPid > 0) {
mainStore.winipBcStart = true;
} else {
mainStore.winipBcStart = false;
}
};
export const handleWinipBcStart = async () => {
const mainStore = useMainStore();
if (!mainStore.winipBcStart) {
try {
await invoke("stop_command", { child_id: mainStore.winipBcPid || 0 });
const child = await Command.create("WinIPBroadcast", ["run"]).spawn();
mainStore.winipBcPid = child.pid || 0;
if (mainStore.winipBcPid) {
mainStore.winipBcStart = true;
mainStore.winIpBcAutoStart = true;
} else {
ElMessage.error(`启动失败`);
}
} catch (err) {
ElMessage.error(`启动失败`);
console.log(err);
}
} else {
mainStore.winIpBcAutoStart = false;
await invoke("stop_command", { child_id: mainStore.winipBcPid || 0 });
await getWinIpBroadcastPid();
}
};
export const initStartWinIpBroadcast = async () => {
const mainStore = useMainStore();
await getWinIpBroadcastPid();
// console.log(mainStore.winipBcStart, mainStore.winIpBcAutoStart)
if (mainStore.winIpBcAutoStart && !mainStore.winipBcStart) {
await handleWinipBcStart();
}
};
+21
View File
@@ -0,0 +1,21 @@
import { useDark, useToggle, usePreferredDark } from "@vueuse/core";
import { isFunction } from "lodash-es";
// const isDark = usePreferredDark();
const darkConfig = useDark({
selector: "html",
attribute: "class",
valueDark: "dark",
valueLight: "",
});
const toggleDark = useToggle(darkConfig);
export const initTheme = (isDark: boolean) => {
if (isDark && isFunction(toggleDark)) {
toggleDark(isDark);
}
};
export const setTheme = (dark: boolean) => {
if (isFunction(toggleDark)) {
toggleDark(dark);
}
}
+106 -85
View File
@@ -1,105 +1,126 @@
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu' import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { TrayIcon } from '@tauri-apps/api/tray' import { TrayIcon } from "@tauri-apps/api/tray";
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from "@tauri-apps/api/window";
import pkg from '@/package.json' import pkg from "@/package.json";
import { setTheme } from "~/composables/theme";
import useMainStore from "@/stores/index";
const DEFAULT_TRAY_NAME = 'main' const DEFAULT_TRAY_NAME = "main";
async function toggleVisibility() { async function toggleVisibility() {
if (await getCurrentWindow().isVisible()) { if (await getCurrentWindow().isVisible()) {
await getCurrentWindow().hide() await getCurrentWindow().hide();
} } else {
else { await getCurrentWindow().show();
await getCurrentWindow().show() await getCurrentWindow().setFocus();
await getCurrentWindow().setFocus() }
}
} }
export async function useTray(init: boolean = false) { export async function useTray(init: boolean = false, beforExit: Function) {
let tray let tray;
try { try {
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME) tray = await TrayIcon.getById(DEFAULT_TRAY_NAME);
if (!tray) { if (!tray) {
tray = await TrayIcon.new({ tray = await TrayIcon.new({
tooltip: `EasyTier\n${pkg.version}`, tooltip: `EasyTierGame\n${pkg.version}`,
title: `EasyTier\n${pkg.version}`, title: `EasyTierGame\n${pkg.version}`,
id: DEFAULT_TRAY_NAME, id: DEFAULT_TRAY_NAME,
menu: await Menu.new({ menu: await Menu.new({
id: 'main', id: "main",
items: await generateMenuItem(), items: await generateMenuItem(beforExit),
}), }),
action: async () => { action: async (e) => {
toggleVisibility() toggleVisibility();
}, },
}) });
} }
} } catch (error) {
catch (error) { console.warn("Error while creating tray icon:", error);
console.warn('Error while creating tray icon:', error) return null;
return null }
}
if (init) { if (init) {
tray.setTooltip(`EasyTier\n${pkg.version}`) tray.setTooltip(`EasyTierGame\n${pkg.version}`);
tray.setMenuOnLeftClick(false) tray.setMenuOnLeftClick(false);
tray.setMenu(await Menu.new({ tray.setMenu(
id: 'main', await Menu.new({
items: await generateMenuItem(), id: "main",
})) items: await generateMenuItem(beforExit),
} })
);
}
return tray return tray;
} }
export async function generateMenuItem() { export async function generateMenuItem(beforExit: Function) {
return [ return [
await MenuItemExit('Exit'), await MenuItemShow("显示 / 隐藏"),
await PredefinedMenuItem.new({ item: 'Separator' }), await MenuItemTheme(),
await MenuItemShow('Show / Hide'), await PredefinedMenuItem.new({ item: "Separator" }),
] await MenuItemExit("退出", beforExit),
];
} }
export async function MenuItemExit(text: string) { export async function MenuItemExit(text: string, beforExit: Function) {
return await PredefinedMenuItem.new({ return await MenuItem.new({
text, id: "quit",
item: 'Quit', text,
}) action: async () => {
if (beforExit) {
await beforExit();
}
await getCurrentWindow().close();
},
});
} }
export async function MenuItemShow(text: string) { export async function MenuItemShow(text: string) {
return await MenuItem.new({ return await MenuItem.new({
id: 'show', id: "show",
text, text,
action: async () => { action: async () => {
await toggleVisibility() await toggleVisibility();
}, },
}) });
} }
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) { export async function MenuItemTheme() {
const tray = await useTray() return await MenuItem.new({
if (!tray) id: "theme",
return text: "主题切换",
const menu = await Menu.new({ action: async () => {
id: 'main', const mainStore = useMainStore();
items: items || await generateMenuItem(), mainStore.theme = !mainStore.theme;
}) // const appWindow = getCurrentWindow();
tray.setMenu(menu) // appWindow.emitTo("main", "config", { theme: mainStore.theme });
await setTheme(mainStore.theme);
},
});
} }
export async function setTrayRunState(isRunning: boolean = false) { // export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
const tray = await useTray() // // const tray = await useTray()
if (!tray) // const tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
return // if (!tray)
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico') // return
// const menu = await Menu.new({
// id: 'main',
// items: items || await generateMenuItem(),
// })
// tray.setMenu(menu)
// }
export async function setTrayRunState(tray: TrayIcon | null, isRunning: boolean = false) {
if (!tray) return;
await tray.setIcon(isRunning ? "easytier/icons/icon-inactive.ico" : "easytier/icons/icon.ico");
} }
export async function setTrayTooltip(tooltip: string) { export async function setTrayTooltip(tray: TrayIcon | null, tooltip?: string | null) {
if (tooltip) { if (!tray) return;
const tray = await useTray() if (tooltip) {
if (!tray) await tray.setTooltip(`EasyTierGame\n${pkg.version}\n${tooltip}`);
return } else {
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`) await tray.setTooltip(`EasyTierGame\n${pkg.version}`);
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`) }
} }
}
+53
View File
@@ -0,0 +1,53 @@
import { type UnlistenFn } from "@tauri-apps/api/event";
import { getCurrentWindow, PhysicalPosition, type WindowOptions, Window } from "@tauri-apps/api/window";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import type { WebviewLabel, WebviewOptions, } from "@tauri-apps/api/webview";
export default async (
label: WebviewLabel,
options?: Omit<WebviewOptions, "x" | "y" | "width" | "height"> & WindowOptions,
afterCreatedFunc?: (webviewWindow: WebviewWindow, appWindow: Window) => void | null,
beforeCloseFunc?: () => void | null
) => {
let defaultOpts = {
parent: undefined,
closable: true,
resizable: true,
decorations: true,
maximizable: false,
minimizable: false,
x: 0,
y: 0
};
const appWindow = getCurrentWindow();
if (appWindow) {
const appSize = await appWindow.outerSize();
const factor = await appWindow.scaleFactor();
const appPosition = await appWindow.outerPosition();
const logicalPosition = new PhysicalPosition(appPosition.x + appSize.width, appPosition.y).toLogical(factor);
defaultOpts.parent = appWindow as any;
defaultOpts.x = logicalPosition.x;
defaultOpts.y = logicalPosition.y;
}
let unListenlogClose: UnlistenFn | null = null;
let unlistenLogCreated: UnlistenFn | null = null;
let dialog = await WebviewWindow.getByLabel(label);
if (!dialog) {
dialog = new WebviewWindow(label, {...defaultOpts, ...options});
unlistenLogCreated = await dialog.once("tauri://webview-created", async () => {
if (dialog) {
afterCreatedFunc && (await afterCreatedFunc(dialog, appWindow));
await dialog.show();
}
});
unListenlogClose = await dialog.onCloseRequested(async () => {
beforeCloseFunc && (await beforeCloseFunc());
unListenlogClose && (await unListenlogClose());
});
} else {
const visible = await dialog.isVisible();
if (visible) {
await dialog.close();
unlistenLogCreated && (await (unlistenLogCreated as Function)());
}
}
};
+3
View File
@@ -0,0 +1,3 @@
VITE_CONFIG_PATH=easytier/config/
VITE_LOG_PATH=easytier/logs/
VITE_AUTO_START_SERVICE_NAME=easytierGameAutoStart
+3
View File
@@ -0,0 +1,3 @@
VITE_CONFIG_PATH=easytier/config/
VITE_LOG_PATH=easytier/logs/
VITE_AUTO_START_SERVICE_NAME=easytierGameAutoStart
+5 -18
View File
@@ -3,23 +3,10 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { onBeforeUnmount } from "vue"; //屏蔽右键菜单
import { getCurrentWindow } from "@tauri-apps/api/window"; document.addEventListener("contextmenu", (e: MouseEvent) => {
import { listen } from "@tauri-apps/api/event"; if (import.meta.env.PROD) {
e.preventDefault();
const appWindow = getCurrentWindow(); }
const unlisten = listen("window-close-event", (event) => {
closeApp();
}); });
onBeforeUnmount(() => {
unlisten.then((unlisten) => {
unlisten && unlisten instanceof Function && unlisten();
});
});
const closeApp = async () => {
appWindow.hide();
};
</script> </script>
+2 -2
View File
@@ -22,7 +22,7 @@ export default defineNuxtConfig({
autoImport: false autoImport: false
}, },
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css", 'element-plus/theme-chalk/dark/css-vars.css'],
modules: ["@element-plus/nuxt", "@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt", "@nuxtjs/tailwindcss"], modules: ["@element-plus/nuxt", "@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt", "@nuxtjs/tailwindcss"],
alias: { alias: {
@@ -70,7 +70,7 @@ export default defineNuxtConfig({
sourcemap: !!process.env.TAURI_DEBUG sourcemap: !!process.env.TAURI_DEBUG
}, },
esbuild: { esbuild: {
pure: ["console.log"], // pure: ["console.log"],
drop: ["debugger"] drop: ["debugger"]
} }
}, },
+11 -6
View File
@@ -1,9 +1,9 @@
{ {
"name": "easytier-game", "name": "easytier-game",
"private": true, "private": true,
"author": "luoleixin@digisky.com", "author": "leizi97",
"description": "A simple network initiator based on Easytier", "description": "A simple network initiator based on Easytier",
"version": "1.0.3", "version": "1.1.5",
"scripts": { "scripts": {
"dev": "nuxt dev --dotenv env/.env.dev --host 0.0.0.0", "dev": "nuxt dev --dotenv env/.env.dev --host 0.0.0.0",
"build": "nuxt generate --dotenv env/.env.prod" "build": "nuxt generate --dotenv env/.env.prod"
@@ -18,14 +18,15 @@
"@tauri-apps/cli": "^2.0.2", "@tauri-apps/cli": "^2.0.2",
"@tauri-apps/plugin-cli": "^2.0.0", "@tauri-apps/plugin-cli": "^2.0.0",
"@tauri-apps/plugin-http": "^2.0.0", "@tauri-apps/plugin-http": "^2.0.0",
"@tauri-apps/plugin-shell": "~2",
"@tinymce/tinymce-vue": "^5.1.1", "@tinymce/tinymce-vue": "^5.1.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/qs": "^6.9.15", "@types/qs": "^6.9.15",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
"@vueuse/core": "^11.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"defu": "^6.1.4", "defu": "^6.1.4",
"element-plus": "^2.7.1", "element-plus": "^2.8.6",
"javascript-obfuscator": "^4.1.0",
"less": "^4.2.0", "less": "^4.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nuxt": "^3.11.2", "nuxt": "^3.11.2",
@@ -35,7 +36,11 @@
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vue": "^3.4.24", "vue": "^3.4.24",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5"
"@tauri-apps/plugin-shell": "~2" },
"dependencies": {
"@tauri-apps/plugin-autostart": "~2",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
"@tauri-apps/plugin-fs": "~2"
} }
} }
+70
View File
@@ -0,0 +1,70 @@
<template>
<div class="h-full overflow-auto flex flex-col items-start px-[25px]">
<div><ElCheckbox v-model="mainStore.config.disableIpv6">不使用IPv6</ElCheckbox></div>
<div class="flex items-center gap-[10px]">
<ElCheckbox v-model="mainStore.config.devName">自定义网卡名</ElCheckbox>
<ElInput maxlength="10" v-model="mainStore.config.devNameValue" placeholder="请输入网卡名"/>
</div>
<div><ElCheckbox v-model="mainStore.config.disbleListenner">不监听任何端口只连接到对等节点</ElCheckbox></div>
<div><ElCheckbox v-model="mainStore.config.coonectAfterStart">软件启动后自动"启动联机"(搭配开机自启无感联机)</ElCheckbox></div>
<div class="flex items-center gap-[15px] flex-nowrap">
<ElCheckbox v-model="mainStore.config.saveErrorLog">输出日志到本地</ElCheckbox>
<div class="w-[140px]">
<ElSelect
v-model="mainStore.config.logLevel"
placeholder="请选择日志等级"
class="ml-[5px]">
<ElOption
v-for="item in data"
:key="item"
:label="`level - ${item}`"
:value="item"></ElOption>
</ElSelect>
</div>
<ElButton
@click="openLogDir"
size="small">
打开日志目录
</ElButton>
</div>
<ElDivider />
<div><ElCheckbox v-model="mainStore.config.enablExitNode">允许此节点成为出口节点</ElCheckbox></div>
<div><ElCheckbox v-model="mainStore.config.disableEncryption">禁用对等节点通信的加密默认为false必须与对等节点相同</ElCheckbox></div>
<div><ElCheckbox v-model="mainStore.config.multiThread">使用多线程运行时默认为单线程</ElCheckbox></div>
<ElDivider />
<div><ElCheckbox v-model="mainStore.config.noTun">不创建TUN设备可以使用子网代理访问节点</ElCheckbox></div>
<div><ElCheckbox v-model="mainStore.config.useSmoltcp">为子网代理启用smoltcp堆栈</ElCheckbox></div>
<div><ElCheckbox v-model="mainStore.config.latencyfirst">延迟优先模式将尝试使用最低延迟路径转发流量默认使用最短路径</ElCheckbox></div>
<div><ElCheckbox v-model="mainStore.config.disableUdpHolePunching">禁用UDP打洞功能</ElCheckbox></div>
<div>
<ElCheckbox v-model="mainStore.config.relayAllPeerrpc">转发所有对等节点的RPC数据包即使对等节点不在转发网络白名单内</ElCheckbox>
</div>
</div>
</template>
<script setup lang="ts">
import useMainStore from "@/stores/index";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { resourceDir as getResourceDir, join } from "@tauri-apps/api/path";
import { Command } from "@tauri-apps/plugin-shell";
import { exists, mkdir, BaseDirectory } from "@tauri-apps/plugin-fs";
const mainStore = useMainStore();
const appWindow = getCurrentWindow();
const data = ["trace", "debug", "info", "warn", "error", "off"];
const openLogDir = async () => {
const resourceDir = await getResourceDir();
const logPath = import.meta.env.VITE_LOG_PATH;
const logDirPath = await join(resourceDir, logPath);
const isExists = await exists(logPath, { baseDir: BaseDirectory.Resource });
if (!isExists) {
try {
await mkdir(logPath, { baseDir: BaseDirectory.Resource });
} catch (err) {}
}
await Command.create("explorer", [logDirPath]).execute();
};
mainStore.$subscribe((...a) => {
// console.log("subscribe", a);
appWindow.emitTo("main", "config", { config: { ...mainStore.config } });
});
</script>
+25
View File
@@ -0,0 +1,25 @@
<template>
<div class="h-full flex flex-col gap-[10px]">
<ElRadioGroup v-model="mainStore.cidrEnable">
<ElRadioButton :value="true">开启</ElRadioButton>
<ElRadioButton :value="false">关闭</ElRadioButton>
</ElRadioGroup>
<div class="flex-1 overflow-auto">
<ElInput
placeholder="例如: 192.168.1.0/24 一行一个"
v-model="mainStore.config.proxyNetworks"
type="textarea"
:rows="30"
resize="none"></ElInput>
</div>
</div>
</template>
<script setup lang="ts">
import useMainStore from "@/stores/index";
import { getCurrentWindow } from "@tauri-apps/api/window";
const mainStore = useMainStore();
const appWindow = getCurrentWindow();
mainStore.$subscribe((...a) => {
appWindow.emitTo("main", "config", { cidrEnable: mainStore.cidrEnable, config: { ...mainStore.config } });
});
</script>
+642 -100
View File
@@ -2,37 +2,53 @@
<ElForm <ElForm
size="small" size="small"
label-position="top" label-position="top"
:model="config"> :model="config"
>
<ElFormItem <ElFormItem
label="服务器" label="服务器"
prop="serverUrl"> prop="serverUrl"
>
<template #label> <template #label>
<div class="flex items-center gap-[0_5px]"> <div class="flex items-center gap-[0_5px]">
<div>服务器</div> <div>服务器</div>
<span>-</span> <span>-</span>
<ElTag <ElTag
effect="dark" effect="dark"
:type="data.isSuccessGetIp ? 'success' : 'info'"> :type="data.isSuccessGetIp ? 'success' : 'info'"
{{ data.isSuccessGetIp ? "联机成功" : "联机中" }} >
{{ data.isSuccessGetIp ? "联机成功" : data.isStart && !data.isSuccessGetIp ? "联机中" : "未联机" }}
</ElTag> </ElTag>
<ElButton <ElButton
v-if="!data.coreVersion" v-if="!data.coreVersion"
@click="getCoreVersion(true)" @click="getCoreVersion(true)"
>获取工具版本</ElButton
> >
获取内核版本
</ElButton>
<ElTag <ElTag
v-else v-else
type="info" type="info"
>{{ data.coreVersion }}</ElTag
> >
<ElButton {{ data.coreVersion }}
:disabled="data.isStart" </ElTag>
:loading="data.update" <ElPopconfirm
type="warning" width="325"
@click="handleUpdateCore" cancel-button-text="取消"
size="small" confirm-button-text="继续"
>{{ data.coreVersion ? "更新" : "下载" }}</ElButton title="内核从github下载,需要出国工具,可能下载缓慢或失败,是否继续?
也可以从官方群里手动下载后解压到easytier-game.exe同级目录下的easytier目录里,全部覆盖即可"
@confirm="handleUpdateCore"
> >
<template #reference>
<ElButton
:disabled="data.isStart"
:loading="data.update"
type="warning"
size="small"
>
{{ data.coreVersion ? "更新内核" : "下载内核" }}
</ElButton>
</template>
</ElPopconfirm>
</div> </div>
</template> </template>
<ElSelect <ElSelect
@@ -40,20 +56,24 @@
filterable filterable
default-first-option default-first-option
v-model="config.serverUrl" v-model="config.serverUrl"
@change="handleServerUrlChange"> @change="handleServerUrlChange"
>
<template #prefix> <template #prefix>
<div :class="config.protocol && config.protocol.length > 1 ? 'w-[120px]' : 'w-[80px]'"> <div :class="config.protocol && config.protocol.length > 1 ? 'w-[120px]' : 'w-[80px]'">
<ElSelect <ElSelect
placeholder="协议" placeholder="协议"
multiple multiple
collapse-tags collapse-tags
@click.stop
v-model="config.protocol" v-model="config.protocol"
@change="handleServerUrlChange"> @change="handleServerUrlChange"
>
<ElOption <ElOption
v-for="item in protocols" v-for="item in protocols"
:key="item" :key="item"
:label="item" :label="item"
:value="item"></ElOption> :value="item"
></ElOption>
</ElSelect> </ElSelect>
</div> </div>
</template> </template>
@@ -61,15 +81,18 @@
v-for="item in mainStore.basePeers" v-for="item in mainStore.basePeers"
:key="item" :key="item"
:label="item" :label="item"
:value="item"> :value="item"
>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span style="float: left">{{ item }}</span> <span style="float: left">{{ item }}</span>
<ElButton <ElButton
@click.stop="handleDeleteServerUrl(item)" @click.stop="handleDeleteServerUrl(item)"
round round
:icon="Delete" :icon="Delete"
type="danger"></ElButton></div type="danger"
></ElOption> ></ElButton>
</div>
</ElOption>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<div class="flex flex-wrap gap-[0_10px] items-center"> <div class="flex flex-wrap gap-[0_10px] items-center">
@@ -86,7 +109,8 @@
<ElInput <ElInput
maxlength="100" maxlength="100"
placeholder="请输入网络名" placeholder="请输入网络名"
v-model="config.networkName"></ElInput> v-model="config.networkName"
></ElInput>
</ElFormItem> </ElFormItem>
</div> </div>
<div class="flex-1"> <div class="flex-1">
@@ -104,7 +128,8 @@
maxlength="100" maxlength="100"
placeholder="请输入网络密码" placeholder="请输入网络密码"
v-model="config.networkPassword" v-model="config.networkPassword"
type="password"></ElInput> type="password"
></ElInput>
</ElFormItem> </ElFormItem>
</div> </div>
</div> </div>
@@ -121,11 +146,13 @@
<ElInput <ElInput
maxlength="100" maxlength="100"
placeholder="例如: Player1" placeholder="例如: Player1"
v-model="config.hostname"></ElInput> v-model="config.hostname"
></ElInput>
</ElFormItem> </ElFormItem>
<ElFormItem <ElFormItem
class="w-[70%]" class="w-[70%]"
label="局域网IP"> label="局域网IP"
>
<template #label> <template #label>
<div class="flex items-center h-[20px]"> <div class="flex items-center h-[20px]">
虚拟网IP 虚拟网IP
@@ -138,101 +165,257 @@
inline-prompt inline-prompt
inactive-text="固定IP" inactive-text="固定IP"
active-text="动态获取IP" active-text="动态获取IP"
size="small"></ElSwitch> size="small"
></ElSwitch>
</div> </div>
</template> </template>
<ElInput <ElInput
maxlength="100" maxlength="100"
:disabled="config.dhcp" :disabled="config.dhcp"
:placeholder="data.isStart ? '等待动态分配IP...' : '例如: 10.126.126.1'" :placeholder="data.isStart ? '等待动态分配IP...' : '例如: 10.126.126.1'"
v-model="config.ipv4"></ElInput> v-model="config.ipv4"
></ElInput>
</ElFormItem> </ElFormItem>
</div> </div>
<div class="flex items-start gap-[0_30px]"> <div class="flex items-start gap-[0_10px]">
<div> <div class="w-[122px]">
<div> <div>
<ElButton <ElDropdown
@command="handleStartCommand"
split-button
size="default"
:type="!data.isStart ? 'primary' : 'danger'" :type="!data.isStart ? 'primary' : 'danger'"
:disabled="data.startLoading || !data.coreVersion || data.update" :disabled="data.startLoading || !data.coreVersion || data.update"
@click="handleConnection" @click="handleConnection"
size="default"> >
{{ !data.isStart ? "开始联机" : "停止联机" }} {{ !data.isStart ? "启动联机" : "停止联机" }}
</ElButton> <template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
command="import_config"
:icon="Link"
>
导入配置
</ElDropdownItem>
<ElDropdownItem
command="share_config"
:icon="Share"
>
分享配置
</ElDropdownItem>
<ElDropdownItem
:icon="Tools"
command="toml"
:disabled="data.isStart"
>
使用外部配置文件
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div> </div>
<div class="mt-[6px]"> <div class="mt-[6px] pl-[2px]">
<ElButton <ElTooltip
type="info" placement="left"
@click="handleShowDialog" content="日志"
size="default"> >
日志信息 <ElButton
</ElButton> :type="!data.logVisible ? 'info' : 'warning'"
@click="handleShowLogDialog"
:icon="List"
size="small"
plain
></ElButton>
</ElTooltip>
<ElTooltip
placement="left"
content="成员"
>
<ElButton
@click="handleShowMemberDialog"
:icon="UserFilled"
plain
type="success"
size="small"
></ElButton>
</ElTooltip>
</div> </div>
</div> </div>
<div> <div>
<ElCheckbox <ElCheckbox
v-model="config.disbleP2p" v-model="config.disbleP2p"
size="small"> size="small"
禁用p2p >
强制中转
</ElCheckbox> </ElCheckbox>
<ElCheckbox <ElCheckbox
v-model="config.disableIpv6" @change="handleAutoStart"
size="small"> :model-value="config.autoStart"
禁用ipv6 size="small"
>
开机自启
</ElCheckbox> </ElCheckbox>
<ElCheckbox
v-model="config.disbleListenner"
size="small">
禁用端口监听
</ElCheckbox>
<ElLink
class="!text-[11px] pb-[2px] ml-[15px]"
type="info"
:underline="false"
@click="open('https://github.com/EasyTier/EasyTier/releases')">
easytier发布页
</ElLink>
<div> <div>
<ElButton
@click="handleShowCidrDialog"
:icon="Share"
>
子网代理
</ElButton>
<ElButton
@click="handleShowAdvanceDialog"
:icon="Setting"
>
高级选项
</ElButton>
</div>
<div class="flex items-center gap-[0_5px]">
<div>
<ElButton
plain
type="primary"
size="small"
:icon="MagicStick"
@click="handleShowToolDialog"
>
增强工具
</ElButton>
</div>
<ElLink <ElLink
class="!text-[11px]" class="!text-[10px] pb-[2px] ml-[8px]"
type="danger" type="info"
:underline="false" :underline="false"
@click="open('https://github.com/dechamps/WinIPBroadcast/releases/tag/winipbroadcast-1.6')"> @click="open('https://github.com/EasyTier/EasytierGame')"
找不到房间建议安装WinIPBroadcast >
EasytierGame主页
</ElLink> </ElLink>
</div> </div>
</div> </div>
</div> </div>
</ElForm> </ElForm>
<ElDialog <ElDialog
title="日志信息" width="95%"
top="0" top="10px"
v-model="data.visible" class="!mb-0"
width="100%" v-model="importConfigData.visible"
class="!h-full !m-0"> :close-on-press-escape="false"
title="导入分享"
>
<ElInput <ElInput
type="textarea" type="textarea"
rows="10" :rows="6"
v-model="data.log" placeholder="请粘贴分享的配置"
resize="none" v-model="importConfigData.data"
readonly /> ></ElInput>
<template #footer>
<div>
<el-text type="danger">导入成功后您当前的配置将被完全替换</el-text>
</div>
<div class="text-right">
<ElButton
size="small"
@click="handleStartImport"
type="primary"
>
导入
</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
width="95%"
top="30px"
class="!mb-0"
v-model="configStart.visible"
:close-on-press-escape="false"
title="配置文件启动"
>
<div class="flex items-center gap-[0_4px]">
<span>启用</span>
<ElSwitch v-model="mainStore.configStartEnable"></ElSwitch>
<ElTooltip content="启用后将完全使用选中的配置文件作为联机配置,其余界面配置不会生效">
<ElIcon class="ml-[3px]"><QuestionFilled /></ElIcon>
</ElTooltip>
<ElButton
@click="openConfigDir"
size="small"
>
打开配置目录
</ElButton>
<ElButton
@click="handleStartCommand('toml')"
type="primary"
:icon="RefreshRight"
size="small"
>
刷新
</ElButton>
</div>
<div class="mt-[5px]">
<ElSelect
v-model="mainStore.configPath"
no-data-text="目录没有配置文件"
placeholder="选择配置文件"
>
<ElOption
v-for="item in configStart.list"
:key="item.path"
:value="item.path"
:label="item.name"
></ElOption>
</ElSelect>
</div>
<template #footer>
<div class="text-right">
<ElButton
size="small"
@click="configStart.visible = false"
type="danger"
>
关闭
</ElButton>
</div>
</template>
</ElDialog> </ElDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-shell"; import { open, Command } from "@tauri-apps/plugin-shell";
import { QuestionFilled, Delete } from "@element-plus/icons-vue"; import { QuestionFilled, Delete, List, UserFilled, Setting, Share, RefreshRight, Link, Tools, MagicStick } from "@element-plus/icons-vue";
import { reactive, onBeforeUnmount, onMounted } from "vue"; import { reactive, onBeforeUnmount, onMounted } from "vue";
import { useTray } from "~/composables/tray"; import { useTray, setTrayRunState, setTrayTooltip } from "~/composables/tray";
import { initStartWinIpBroadcast } from "~/composables/netcard";
import useMainStore from "@/stores/index"; import useMainStore from "@/stores/index";
import { ElMessage } from "element-plus"; import { ElDropdownMenu, ElMessage, ElMessageBox } from "element-plus";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { getAllWebviewWindows } from "@tauri-apps/api/webviewWindow";
import etWindows from "@/composables/windows";
import * as tauriAutoStart from "@tauri-apps/plugin-autostart";
import { resourceDir as getResourceDir, join } from "@tauri-apps/api/path";
import { readDir, exists, mkdir, BaseDirectory } from "@tauri-apps/plugin-fs";
import { writeText, readText } from "@tauri-apps/plugin-clipboard-manager";
let is_close = false;
const tray = await useTray(true, async () => {
is_close = true;
await invoke("stop_command", { child_id: listenObj.thread_id || 0 });
await invoke("stop_command", { child_id: data.winipBcPid || 0 });
});
useTray(true);
const mainStore = useMainStore(); const mainStore = useMainStore();
const config = mainStore.config; const config = mainStore.config;
// console.log(config);
const protocols = ["tcp", "udp", "ws", "wss", "wg"]; const protocols = ["tcp", "udp", "ws", "wss", "wg"];
const data = reactive({ const data = reactive({
visible: false, logVisible: false,
cidrVisible: false,
advanceVisible: false,
toolVisible: false,
winipBcPid: 0, //WinIPBroadcast进程id
winipBcStart: false,
memberVisible: false,
log: "", log: "",
update: false, update: false,
releaseList: [], releaseList: [],
@@ -240,8 +423,35 @@
isSuccessGetIp: false, isSuccessGetIp: false,
startLoading: false, startLoading: false,
isStart: false, isStart: false,
connectionSuccess: false
}); });
const configStart = reactive<{ list: Array<{ path: string; name: string }>; [key: string]: any }>({
visible: false,
loading: false,
list: [] //配置文件列表
});
const importConfigData = reactive<{ data: ""; [key: string]: any }>({
visible: false,
loading: false,
data: ""
});
const closePrevent = async () => {
const appWindow = getCurrentWindow();
if (appWindow.label == "main") {
appWindow.onCloseRequested(async event => {
// console.log(appWindow.label);
if (!is_close) {
// console.log(1);
event.preventDefault();
appWindow.hide();
}
});
}
};
const handleDeleteServerUrl = (url: string) => { const handleDeleteServerUrl = (url: string) => {
const newBasePeers = [...mainStore.basePeers]; const newBasePeers = [...mainStore.basePeers];
const idx = newBasePeers.indexOf(url); const idx = newBasePeers.indexOf(url);
@@ -261,40 +471,52 @@
const listenObj: { [key: string]: any } = { const listenObj: { [key: string]: any } = {
unListenOutPut: null, unListenOutPut: null,
unListenThreadId: null, unListenThreadId: null,
unListenReleaseList: null, unListenConfigStart: null,
thread_id: null, thread_id: null,
async listenOutput() { async listenOutput() {
const unListen = await listen("command-output", (event) => { const appWindow = getCurrentWindow();
const unListen = await listen("command-output", async event => {
data.isStart = true; data.isStart = true;
if (event.payload) { if (event.payload) {
data.startLoading = false; data.startLoading = false;
let ipv4 = /new: Some\((\d+\.\d+\.\d+\.\d+)\/.*\)/g.exec(event.payload as string)?.[1]; let ipv4 = /new: Some\((\d+\.\d+\.\d+\.\d+)\/.*\)/g.exec(event.payload as string)?.[1];
if (ipv4) { if (config.dhcp) {
data.isSuccessGetIp = true; if (ipv4) {
config.ipv4 = ipv4; config.ipv4 = ipv4;
await setTrayRunState(tray, true);
data.isSuccessGetIp = true;
await setTrayTooltip(tray, `IP: ${ipv4}`);
}
} else {
if ((event.payload as string).includes("new peer connection added")) {
await setTrayRunState(tray, true);
data.isSuccessGetIp = true;
await setTrayTooltip(tray, `IP: ${config.ipv4}`);
}
} }
} }
appWindow.emitTo("log", "logs", data.log);
data.log += (event.payload as string) + "\n"; data.log += (event.payload as string) + "\n";
}); });
this.unListenOutPut = unListen; this.unListenOutPut = unListen;
}, },
async listenThreadId() { async listenThreadId() {
const unListen = await listen("thread-id", (event) => { const unListen = await listen("thread-id", event => {
if (event.payload) { if (event.payload) {
this.thread_id = event.payload; this.thread_id = event.payload;
} }
}); });
this.unListenThreadId = unListen; this.unListenThreadId = unListen;
}, },
async listenConfigStart() {
async releaseListListener() { const unListen = await listen("config", event => {
const unListen = await listen("release-list", (event) => { // console.log("config", event.payload);
if (event.payload) { const ipv4 = config.ipv4;
console.log(event.payload); mainStore.$patch(event.payload as any);
} config.ipv4 = ipv4;
}); });
this.unListenReleaseList = unListen; this.unListenConfigStart = unListen;
}, }
}; };
const unListenAll = async () => { const unListenAll = async () => {
@@ -315,11 +537,11 @@
await getReleaseList(); await getReleaseList();
const latestVersionFileName = data.releaseList?.[0]?.[0]?.[1] as string; const latestVersionFileName = data.releaseList?.[0]?.[0]?.[1] as string;
if (latestVersionFileName) { if (latestVersionFileName) {
console.log(latestVersionFileName, /\-v(\d+\.\d+\.\d+)/g.exec(latestVersionFileName)); // console.log(latestVersionFileName, /\-v(\d+\.\d+\.\d+)/g.exec(latestVersionFileName));
const latestVersion = /\-v(\d+\.\d+\.\d+)/g.exec(latestVersionFileName)?.[1]; const latestVersion = /\-v(\d+\.\d+\.\d+)/g.exec(latestVersionFileName)?.[1];
const currentVersion = /(\d+\.\d+\.\d+)/g.exec(data.coreVersion || "")?.[1]; const currentVersion = /(\d+\.\d+\.\d+)/g.exec(data.coreVersion || "")?.[1];
if (latestVersion && currentVersion != latestVersion) { if (latestVersion && currentVersion != latestVersion) {
console.log({ currentVersion, latestVersion }); // console.log({ currentVersion, latestVersion });
ElMessage.success(`更新 -> ${latestVersion}`); ElMessage.success(`更新 -> ${latestVersion}`);
const downloadUrl = data.releaseList?.[0]?.[0]?.[2]; const downloadUrl = data.releaseList?.[0]?.[0]?.[2];
return [true, downloadUrl, latestVersionFileName]; return [true, downloadUrl, latestVersionFileName];
@@ -330,18 +552,19 @@
ElMessage.error(`获取版本失败`); ElMessage.error(`获取版本失败`);
return [false, null, null]; return [false, null, null];
} }
} else {
ElMessage.error(`获取版本失败`);
} }
return [false, null, null]; return [false, null, null];
}; };
const handleUpdateCore = async () => { const handleUpdateCore = async () => {
// easytier-windows-x86_64-v2.0.3.zip
data.update = true; data.update = true;
await getCoreVersion(); await getCoreVersion();
const [isNeedUpdate, downloadUrl, latestVersionFileName] = await checkUpdate(); const [isNeedUpdate, downloadUrl, latestVersionFileName] = await checkUpdate();
if (isNeedUpdate) { if (isNeedUpdate) {
await reset(); await reset();
console.log(downloadUrl); // console.log(downloadUrl);
await invoke("download_easytier_zip", { download_url: downloadUrl, file_name: latestVersionFileName }); await invoke("download_easytier_zip", { download_url: downloadUrl, file_name: latestVersionFileName });
} }
await getCoreVersion(); await getCoreVersion();
@@ -353,20 +576,104 @@
data.releaseList = list as never[]; data.releaseList = list as never[];
}; };
const handleAutoStart = async () => {
let is_enable = await tauriAutoStart.isEnabled();
if (!config.autoStart && !is_enable) {
try {
await tauriAutoStart.enable();
} catch (err) {
ElMessage.error(`开机自启失败`);
}
} else {
try {
await tauriAutoStart.disable();
} catch (err) {
// ElMessage.error(`取消自启失败`);
}
}
is_enable = await tauriAutoStart.isEnabled();
if (!is_enable) {
config.autoStart = false;
} else {
config.autoStart = true;
}
};
const initAutoStart = async () => {
try {
const is_enable = await tauriAutoStart.isEnabled();
if (!is_enable) {
config.autoStart = false;
} else {
config.autoStart = true;
}
} catch (err) {
config.autoStart = false;
}
};
const handleAutoStartByTask = async () => {
await invoke("spawn_autostart", { enabled: !config.autoStart });
const is_enable_by_task = (await invoke("autostart_is_enabled")) as boolean;
config.autoStart = is_enable_by_task;
};
const compatibleInitAutoStart = async () => {
try {
const is_enable_by_task = (await invoke("autostart_is_enabled")) as boolean;
if (is_enable_by_task) {
await tauriAutoStart.enable();
await invoke("spawn_autostart", { enabled: false });
}
const is_enable = await tauriAutoStart.isEnabled();
config.autoStart = is_enable;
} catch (err) {
await invoke("spawn_autostart", { enabled: false });
await tauriAutoStart.disable();
config.autoStart = false;
}
};
const initConnectAfterStart = async () => {
if (config.coonectAfterStart && data.coreVersion) {
await reset();
await handleConnection();
}
};
let logsTimer: NodeJS.Timeout | null = null;
onMounted(async () => { onMounted(async () => {
// await handleUpdateCore(); //默认不自动更新 // await handleUpdateCore(); //默认不自动更新
await compatibleInitAutoStart();
// await initAutoStart();
await initStartWinIpBroadcast();
await getCoreVersion(); await getCoreVersion();
await listenObj.releaseListListener();
await listenObj.listenThreadId(); await listenObj.listenThreadId();
await listenObj.listenConfigStart();
await initConfigDir();
await initConnectAfterStart();
closePrevent();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
unListenAll(); unListenAll();
listenObj.unListenReleaseList && listenObj.unListenReleaseList(); listenObj.unListenReleaseList && listenObj.unListenReleaseList();
listenObj.unListenConfigStart && listenObj.unListenConfigStart();
logsTimer && clearInterval(logsTimer);
}); });
const getArgs = () => { const getArgs = async () => {
// console.log(config.proxyNetworks);
const args = []; const args = [];
if (mainStore.configStartEnable && mainStore.configPath) {
// const resourceDir = await getResourceDir();
// const configPath = await join(resourceDir, mainStore.configPath);
// args.push("-c", configPath);
args.push("-c", mainStore.configPath);
return args;
}
if (config.dhcp) { if (config.dhcp) {
args.push("-d"); args.push("-d");
} }
@@ -384,7 +691,7 @@
} }
if (config.serverUrl) { if (config.serverUrl) {
const formatUrl = config.serverUrl.replace(/\\/g, "/"); const formatUrl = config.serverUrl.replace(/\\/g, "/");
args.push("--peers", ...config.protocol.map((protocol) => `${protocol}://${formatUrl}`)); args.push("--peers", ...config.protocol.map(protocol => `${protocol}://${formatUrl}`));
} }
if (config.disbleP2p) { if (config.disbleP2p) {
args.push("--disable-p2p"); args.push("--disable-p2p");
@@ -395,13 +702,68 @@
if (config.disbleListenner) { if (config.disbleListenner) {
args.push("--no-listener"); args.push("--no-listener");
} }
if (mainStore.cidrEnable && config.proxyNetworks) {
// console.log(config.proxyNetworks);
const reg = /\d+\.\d+\.\d+\.\d+\/\d+/g;
const formatProxyNetworks = config.proxyNetworks
.split("\n")
.map(item => item.trim())
.filter(item => item && reg.test(item));
args.push("--proxy-networks", ...formatProxyNetworks);
config.proxyNetworks = formatProxyNetworks.join("\n");
}
if (config.disableEncryption) {
args.push("--disable-encryption");
}
if (config.multiThread) {
args.push("--multi-thread");
}
if (config.enablExitNode) {
args.push("--enable-exit-node");
}
if (config.noTun) {
args.push("--no-tun");
}
if (config.latencyfirst) {
args.push("--latency-first");
}
if (config.useSmoltcp) {
args.push("--use-smoltcp");
}
if (config.disableUdpHolePunching) {
args.push("--disable-udp-hole-punching");
}
if (config.relayAllPeerrpc) {
args.push("--relay-all-peer-rpc");
}
if (config.saveErrorLog) {
args.push("--file-log-level", config.logLevel, "--file-log-dir", import.meta.env.VITE_LOG_PATH);
}
if (config.devName && config.devNameValue) {
args.push("--dev-name", config.devNameValue);
}
return args; return args;
}; };
const reset = async () => { const reset = async () => {
data.isStart = false; data.isStart = false;
data.isSuccessGetIp = false; data.isSuccessGetIp = false;
config.ipv4 = ""; if (config.dhcp) {
config.ipv4 = "";
}
const memberDialog = await getAllWebviewWindows();
const memberDialogs = memberDialog.filter(item => item.label === "member");
if (memberDialogs && memberDialogs.length > 0) {
data.memberVisible = false;
for (const memberDialog of memberDialogs) {
const isVisible = await memberDialog.isVisible();
if (isVisible) {
await memberDialog.destroy();
}
}
}
await setTrayRunState(tray, false);
await setTrayTooltip(tray);
await unListenAll(); await unListenAll();
}; };
@@ -409,17 +771,197 @@
if (data.isStart) { if (data.isStart) {
await reset(); await reset();
} else { } else {
const args = await getArgs();
if (!args || args.length <= 0) {
return ElMessage.error("无配置");
}
data.log = ""; //清空日志
data.startLoading = true; data.startLoading = true;
await unListenAll(); await unListenAll();
await listenObj.listenOutput(); await listenObj.listenOutput();
const args = getArgs();
await invoke("run_command", { await invoke("run_command", {
args, args
}); });
} }
}; };
const handleShowDialog = async () => { //configStart
data.visible = true; const handleStartCommand = async (command: string | number | object) => {
if (command === "toml") {
configStart.visible = true;
const path = import.meta.env.VITE_CONFIG_PATH;
const isExists = await exists(path, { baseDir: BaseDirectory.Resource });
if (isExists) {
const entries = await readDir(path, { baseDir: BaseDirectory.Resource });
configStart.list = entries
.filter(item => item.isFile)
.map(item => ({
name: item.name,
path: `${path}${item.name}`
})) as any;
} else {
mainStore.configStartEnable = false;
mainStore.configPath = "";
try {
await mkdir(path, { baseDir: BaseDirectory.Resource });
} catch (err) {}
}
}
if (command === "share_config") {
try {
await writeText(btoa(JSON.stringify({ config: mainStore.config })));
ElMessage.success("配置已复制");
} catch (err) {
ElMessage.error("分享失败");
}
}
if (command === "import_config") {
importConfigData.data = "";
importConfigData.visible = true;
}
};
const handleStartImport = async () => {
try {
await ElMessageBox.confirm("确定导入?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消"
});
const payload = JSON.parse(atob(importConfigData.data));
mainStore.$patch(payload);
ElMessage.success("导入成功");
importConfigData.visible = false;
mainStore.basePeers = [...new Set([config.serverUrl, ...mainStore.basePeers])];
} catch (err) {
if (err !== "cancel") {
ElMessage.error("导入失败");
}
}
};
const initConfigDir = async () => {
const path = import.meta.env.VITE_CONFIG_PATH;
const isExists = await exists(path, { baseDir: BaseDirectory.Resource });
if (!isExists) {
mainStore.configStartEnable = false;
mainStore.configPath = "";
try {
await mkdir(path, { baseDir: BaseDirectory.Resource });
} catch (err) {}
}
};
const openConfigDir = async () => {
const resourceDir = await getResourceDir();
const configPath = await join(resourceDir, import.meta.env.VITE_CONFIG_PATH);
// console.log(configPath);
await Command.create("explorer", [configPath]).execute();
};
const handleShowMemberDialog = async () => {
if (!data.isStart) {
return ElMessage.warning("请先开始联机");
}
if (!mainStore.config.ipv4) {
return ElMessage.warning("请等待获取IP");
}
await etWindows(
"member",
{
title: "成员列表",
width: 470,
height: 380,
url: "#/member"
},
() => {
data.memberVisible = true;
},
() => {
data.memberVisible = false;
}
);
};
const handleShowLogDialog = async () => {
await etWindows(
"log",
{
title: "联机日志",
width: 600,
height: 380,
resizable: false,
url: "#/log"
},
(_, appWindow) => {
data.logVisible = true;
logsTimer && clearInterval(logsTimer);
logsTimer = setInterval(() => {
appWindow.emitTo("log", "logs", data.log ? (data.log as string).split("\n").slice(-1000).join("\n") : "");
}, 600);
},
() => {
data.logVisible = false;
}
);
};
const handleShowCidrDialog = async () => {
await etWindows(
"cidr",
{
title: "子网代理",
width: 600,
height: 380,
resizable: false,
url: "#/cidr"
},
(_, appWindow) => {
data.cidrVisible = true;
},
() => {
data.cidrVisible = false;
}
);
};
const handleShowAdvanceDialog = async () => {
await etWindows(
"advance",
{
title: "高级选项",
width: 600,
height: 380,
resizable: false,
url: "#/advance"
},
(_, appWindow) => {
data.advanceVisible = true;
},
() => {
data.advanceVisible = false;
}
);
};
const handleShowToolDialog = async () => {
await etWindows(
"tool",
{
title: "增强工具",
width: 460,
height: 480,
resizable: true,
url: "#/tool"
},
(_, appWindow) => {
data.toolVisible = true;
},
() => {
data.toolVisible = false;
}
);
}; };
</script> </script>
+33
View File
@@ -0,0 +1,33 @@
<template>
<ElInput
type="textarea"
:rows="17"
v-model="data.log"
resize="none"
readonly
placeholder="等待日志中,请先'启动联机'..."
/>
</template>
<script setup lang="ts">
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { reactive, onMounted, onBeforeUnmount } from "vue";
const data = reactive({
log: ""
});
const listenStart = async () => {
const unListen = await listen("logs", event => {
data.log = (event.payload as string) || "";
});
return unListen;
};
let unlistenStart: UnlistenFn | null = null;
onMounted(async () => {
unlistenStart = await listenStart();
});
onBeforeUnmount(() => {
unlistenStart && unlistenStart();
});
</script>
+78
View File
@@ -0,0 +1,78 @@
<template>
<div class="overflow-auto h-full">
<div class="w-[1200px]">
<span v-if="!data.member || data.member.length <= 0">{{ "等待成员信息中..." }}</span>
<ElTable
stripe
border
:data="data.member"
v-else>
<ElTableColumn
v-for="prop in showTableHeader"
sortable
:prop="prop[0]"
:label="prop[1]"
:key="prop[0]">
<template #default="{ row }">
<span v-if="prop[0] == 'loss_rate'">
{{ row[prop[0]] && row[prop[0]] != "-" ? Number(row[prop[0]] * 100).toFixed(2) + "%" : row[prop[0]] }}
</span>
<span v-else-if="prop[0] != 'cost'">{{ row[prop[0]] }}</span>
<ElTag
v-else
effect="dark"
:type="row[prop[0]] == 'p2p' ? 'success' : 'info'">
{{ row[prop[0]] }}
</ElTag>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { reactive, onMounted, onBeforeUnmount } from "vue";
import { parsePeerInfo } from "@/utils";
const data = reactive<{ member: any[] }>({
member: [],
});
const showTableHeader = [
["hostname", "主机名"],
["lat_ms", "延迟/ms"],
["ipv4", "虚拟网IP"],
["cost", "路由"],
["loss_rate", "丢包率"],
["nat_type", "NAT类型"],
["version", "版本"],
];
const listenOutput = async () => {
const member = await invoke("get_members_by_cli");
const peerInfo = parsePeerInfo(member as string);
peerInfo.forEach((value) => {
if (value.cost === "Local") {
value.cost = "本机";
}
if (value.ipv4 && value.ipv4.includes("/")) {
value.ipv4 = value.ipv4.split("/")[0];
}
});
// console.log(peerInfo);
data.member = peerInfo;
};
let timer: NodeJS.Timeout | null = null;
onMounted(async () => {
listenOutput();
timer = setInterval(() => {
listenOutput();
}, 1000 * 10);
});
onBeforeUnmount(() => {
timer && clearInterval(timer);
});
</script>
+323
View File
@@ -0,0 +1,323 @@
<template>
<div class="h-full overflow-auto">
<ElTabs
v-model="data.activeTab"
@tab-change="handleTabsChange"
class="h-full"
>
<ElTabPane
label="联机失败解决办法"
name="netcard"
class="flex flex-col h-full"
>
<div>
<div>
<ElTooltip
content="找不到游戏房间时,就开启它后再尝试搜索房间(默认开启)
例如: 一个游戏玩家,有时通过本地局域网联机,有时通过某些联机工具进行联机,但是他只能在其中一个联机方式里看到朋友的游戏房间,而另一个里看不到,这时就可以使用这个工具,让他在两个地方都能看到游戏房间"
>
<ElText>
方案1
<ElIcon class="ml-[3px]"><QuestionFilled /></ElIcon>
</ElText>
</ElTooltip>
</div>
<ElLink
class="!text-[15px] mr-[10px]"
type="info"
:underline="false"
@click="open('https://github.com/dechamps/WinIPBroadcast/releases/tag/winipbroadcast-1.6')"
>
WinIPBroadcast
</ElLink>
<ElSwitch
inline-prompt
:model-value="mainStore.winipBcStart"
@change="handleWinipBcStart"
size="small"
label="WinIPBroadcast"
active-text="已开启"
inactive-text="已关闭"
></ElSwitch>
</div>
<div>
<ElTooltip content="设置网卡的跃点,提升网卡优先级,尝试将您联机使用的网卡跃点设置为最小,请先查询网卡信息">
<ElText>
方案2
<ElIcon class="ml-[3px]"><QuestionFilled /></ElIcon>
</ElText>
</ElTooltip>
</div>
<div class="flex items-center mt-[5px]">
<ElButton
@click="initNetCardInfo"
size="small"
type="primary"
>
查询网卡信息
</ElButton>
<div
v-if="data.row.name"
class="ml-auto flex items-center flex-nowrap gap-[10px]"
>
<ElTooltip content="跃点越小,网卡优先级越高,范围(1-9999)">
<ElInput
class="!w-[80px]"
autofocus
size="small"
v-model="data.row.metric"
placeholder="请输入跃点"
/>
</ElTooltip>
<ElButton
type="success"
size="small"
@click="handleSaveMetric"
>
保存
</ElButton>
</div>
</div>
<div class="flex-1 overflow-auto mt-[5px]">
<ElTable
height="100%"
empty-text="暂无数据"
highlight-current-row
:data="data.netcardList"
@row-click="handleRowClick"
border
>
<ElTableColumn
sortable
label="网卡名"
prop="name"
></ElTableColumn>
<ElTableColumn
sortable
label="跃点"
prop="metric"
></ElTableColumn>
</ElTable>
</div>
</ElTabPane>
<ElTabPane
label="防火墙"
name="firewall"
class="flex flex-col h-full"
>
<div class="flex flex-col items-center">
<div class="mb-[10px]">
<ElButton
type="warning"
size="small"
>
一键关闭防火墙
</ElButton>
</div>
<div class="mb-[10px]">
<ElText class="!mx-[10px]">域防火墙</ElText>
<ElTag :type="data.firewallStatus.domain ? 'danger' : 'success'">
{{ data.firewallStatus.domain ? "已开启" : "已关闭" }}
</ElTag>
</div>
<div class="mb-[10px]">
<ElText class="!mx-[10px]">公用防火墙</ElText>
<ElTag :type="data.firewallStatus.public ? 'danger' : 'success'">
{{ data.firewallStatus.public ? "已开启" : "已关闭" }}
</ElTag>
</div>
<div>
<ElText class="!mx-[10px]">专用防火墙</ElText>
<ElTag :type="data.firewallStatus.dedicated ? 'danger' : 'success'">
{{ data.firewallStatus.dedicated ? "已开启" : "已关闭" }}
</ElTag>
</div>
</div>
<ElDivider>Ping测试</ElDivider>
<div class="flex items-center gap-[10px]">
<ElInput
style="width: 150px"
size="small"
v-model="data.pingIp"
placeholder="请输入IP地址"
/>
<ElInputNumber
controls-position="right"
size="small"
:precision="0"
v-model="data.pingNum"
:min="1"
:max="10"
>
<template #suffix>
<span></span>
</template>
</ElInputNumber>
<ElButton
size="small"
@click="handleTestPing"
>
Ping一下
</ElButton>
</div>
<div class="mt-[5px] flex-1 overflow-auto">
<ElInput
placeholder="日志信息"
type="textarea"
:autosize="{ minRows: 9 }"
resize="none"
v-model="data.pingLog"
/>
</div>
</ElTabPane>
</ElTabs>
</div>
</template>
<script setup lang="ts">
import { open, Command } from "@tauri-apps/plugin-shell";
import { ElMessage, type TabPaneName } from "element-plus";
import { QuestionFilled } from "@element-plus/icons-vue";
import { onMounted, reactive } from "vue";
import useMainStore from "@/stores/index";
import { handleWinipBcStart, initStartWinIpBroadcast } from "@/composables/netcard";
import { getCurrentWindow } from "@tauri-apps/api/window";
const mainStore = useMainStore();
const data = reactive({
activeTab: "netcard",
winipBcPid: 0,
pingNum: 1,
winipBcStart: false,
pingIp: "",
pingLog: "",
firewallStatus: {
domain: false,
public: false,
dedicated: false
},
row: {
name: "",
metric: ""
},
netcardList: []
});
const handleTabsChange = async (tabPaneName: TabPaneName) => {
if (tabPaneName === "netcard") {
// await initNetCardInfo();
} else if (tabPaneName == "firewall") {
await initFirewall();
}
};
const handleRowClick = async (row: any, column: any, event: Event) => {
data.row = { ...row };
};
const handleSaveMetric = async () => {
if (data.row.metric && data.row.name) {
try {
const output = await Command.create("netsh", ["interface", "ipv4", "set", "interface", data.row.name, "metric=", data.row.metric], {
encoding: "gb2312"
}).execute();
ElMessage.success("设置成功");
await initNetCardInfo();
} catch (err) {
console.log(err);
}
}
};
const handleTestPing = async () => {
/^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$/;
if (data.pingIp) {
data.pingLog = "";
try {
for (let i = 0; i < data.pingNum; i++) {
const output = await Command.create("ping", ["-n", "1", data.pingIp], {
encoding: "gb2312"
}).execute();
data.pingLog = data.pingLog.slice(-1000);
if (output.stdout) {
data.pingLog += output.stdout.split("\n")[2];
}
if (output.stderr) {
data.pingLog += output.stderr;
}
}
} catch (err) {
ElMessage.error("Ping发生错误");
console.log(err);
}
}
};
const initNetCardInfo = async () => {
try {
const output = await Command.create("netsh", ["interface", "ipv4", "show", "interface"], {
encoding: "gb2312"
}).execute();
data.netcardList = output.stdout
.split("\n")
.map(el => el.trim().replace(/\s+/g, " "))
.filter(el => el)
.slice(2)
.map(el => {
const [idx, Met, MTU, status, ...rest] = el.split(" ");
return {
index: idx,
metric: Met,
MTU,
status,
name: rest.join(" ")
};
}) as any;
} catch (err) {
ElMessage.error("网卡获取失败");
console.log(err);
}
};
const initFirewall = async () => {
try {
const output = await Command.create("netsh", ["advfirewall", "show", "allprofiles", "state"], {
encoding: "gb2312"
}).execute();
const state = output.stdout
.trim()
.replace(/[\-]+/g, "")
.split("\n")
.map(el => el.trim())
.filter(el => el)
.slice(0, -1);
for (let idx = 0; idx < state.length; idx += 2) {
const status = state[idx + 1].replace(/状态\s+/, "");
const matchName = /域|公用|专用/.exec(state[idx]);
if (matchName) {
const name = matchName[0];
if (name == "域") {
data.firewallStatus.domain = status != "关闭";
} else if (name == "公用") {
data.firewallStatus.public = status != "关闭";
} else if (name == "专用") {
data.firewallStatus.dedicated = status != "关闭";
}
}
}
} catch (err) {
ElMessage.error("获取防火墙状态失败");
console.log(err);
}
};
const appWindow = getCurrentWindow();
mainStore.$subscribe((...a) => {
// console.log("subscribe", a);
appWindow.emitTo("main", "config", { winIpBcAutoStart: mainStore.winIpBcAutoStart, config: { ...mainStore.config } });
});
onMounted(() => {
initStartWinIpBroadcast();
});
</script>
+86 -430
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -2,5 +2,4 @@
# will have compiled files and executables # will have compiled files and executables
/target/ /target/
/gen/schemas /gen/schemas
/easytier-windows-x86_64-v2.0.3.zip /easytier/
/easytier-windows-x86_64/
+948 -69
View File
File diff suppressed because it is too large Load Diff
+26 -9
View File
@@ -1,35 +1,52 @@
[package] [package]
name = "easytier-game" name = "easytier-game"
version = "1.0.3" version = "1.1.5"
homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasytierGame"
description = "A simple network initiator based on Easytier" description = "A simple network initiator based on Easytier"
authors = ["you"] authors = ["leizi97"]
license = "" license = "LICENSE"
repository = ""
edition = "2021" edition = "2021"
rust-version = "1.77.2" rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
name = "app_lib" name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.1", features = [] } tauri-build = { version = "2.0.2", features = [] }
# prost-build = "0.13.2"
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4" log = "0.4"
tauri = { version = "2.0.2", features = [ tauri = { version = "2.0.6", features = [
"tray-icon", "tray-icon",
"image-png", "image-png",
"image-ico", "image-ico",
] } ] }
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2" tauri-plugin-log = "2.0.1"
tauri-plugin-shell = "2.0.2"
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
zip = "2.2.0" zip = "2.2.0"
sysinfo = '0.32.0'
planif = { git = "https://github.com/mattrobineau/planif", tag = "1.0.1" }
whoami = "1.5.2"
tauri-plugin-fs = "2"
tauri-plugin-clipboard-manager = "2.0.0-rc"
# prost = "0.13"
# prost-types = "0.13"
[dependencies.windows]
version = "0.58.0"
features = ["Win32_System_TaskScheduler"]
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-single-instance = "2" tauri-plugin-autostart = "2.0.1"
tauri-plugin-single-instance = "2.0.1"
+1
View File
@@ -0,0 +1 @@
../LICENSE
+79 -3
View File
@@ -3,13 +3,89 @@
"identifier": "default", "identifier": "default",
"description": "enables the default permissions", "description": "enables the default permissions",
"windows": [ "windows": [
"main" "main",
"log",
"member",
"cidr",
"advance",
"tool"
], ],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-minimize", "core:window:allow-minimize",
"core:window:allow-hide", "core:window:allow-hide",
"core:window:allow-close", "core:window:allow-close",
"shell:allow-open" "shell:allow-open",
"core:window:allow-show",
"core:window:allow-create",
"core:window:allow-set-position",
"core:window:allow-destroy",
"core:webview:allow-create-webview-window",
"core:webview:allow-set-webview-size",
"core:webview:allow-set-webview-position",
"core:webview:allow-create-webview",
"core:webview:allow-webview-show",
"core:webview:allow-webview-hide",
"core:webview:allow-webview-close",
"autostart:default",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:tray:allow-get-by-id",
"core:tray:allow-set-icon",
"core:tray:allow-new",
{
"identifier": "shell:allow-spawn",
"allow": [
{
"args": [
"run"
],
"cmd": "easytier/tool/WinIPBroadcast",
"name": "WinIPBroadcast"
}
]
},
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": [
"run"
],
"cmd": "easytier/tool/WinIPBroadcast",
"name": "WinIPBroadcast"
},
{
"args": true,
"cmd": "easytier/tool/nssm",
"name": "nssm"
},
{
"args": true,
"cmd": "explorer",
"name": "explorer"
},
{
"args": true,
"cmd": "netsh",
"name": "netsh"
},
{
"args": true,
"cmd": "ping",
"name": "ping"
}
]
},
"fs:default",
"fs:allow-read-dir",
"fs:allow-exists",
"fs:allow-open",
"fs:allow-resource-read",
"fs:allow-resource-read-recursive",
"clipboard-manager:allow-clear",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text"
] ]
} }
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
这里存放每次打包需要用到的最新版本的easytier 和 相关工具
+23
View File
@@ -0,0 +1,23 @@
hostname = "Test1"
instance_name = "default"
instance_id = "8803362c-e4f1-4df1-93e8-721653945f77"
dhcp = true
listeners = [
"tcp://0.0.0.0:11010",
"udp://0.0.0.0:11010",
"wg://0.0.0.0:11011",
"ws://0.0.0.0:11011/",
"wss://0.0.0.0:11012/",
]
exit_nodes = []
rpc_portal = "0.0.0.0:15888"
[network_identity]
network_name = "default"
network_secret = ""
[[peer]]
uri = "tcp://public.easytier.top:11010"
[[peer]]
uri = "udp://public.easytier.top:11010"
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.
Binary file not shown.
+289 -50
View File
@@ -1,5 +1,10 @@
use planif::enums::TaskCreationFlags;
use planif::schedule::TaskScheduler as planIfTaskScheduler;
use planif::schedule_builder::{Action, ScheduleBuilder};
use planif::settings::{Duration, LogonType, PrincipalSettings, RunLevel};
use reqwest::{Client, Error}; use reqwest::{Client, Error};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@@ -9,9 +14,15 @@ use std::sync::{
mpsc, Arc, mpsc, Arc,
}; };
use std::{path, thread}; use std::{path, thread};
use sysinfo::System;
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::Emitter; use tauri::Emitter;
use tauri::{Manager, WindowEvent}; use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
use windows::core::{BSTR, VARIANT};
use windows::Win32::Foundation::VARIANT_BOOL;
use windows::Win32::System::Com::*;
use windows::Win32::System::TaskScheduler::*;
// 定义GitHub Release的结构体 // 定义GitHub Release的结构体
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -47,29 +58,9 @@ pub async fn fetch_releases() -> Result<Vec<Release>, Error> {
response.json::<Vec<Release>>().await response.json::<Vec<Release>>().await
} }
static mut IS_CLOSE: bool = false;
#[tauri::command(rename_all = "snake_case")]
fn my_commit_output(commit_msg: String, window: tauri::Window) -> Result<bool, String> {
println!("{}", commit_msg);
unsafe {
IS_CLOSE = true;
}
window.close().unwrap();
return Ok(true);
}
#[tauri::command(rename_all = "snake_case")]
fn force_close(window: tauri::Window) {
unsafe {
IS_CLOSE = true;
}
window.close().unwrap();
}
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
fn get_core_version() -> String { fn get_core_version() -> String {
match Command::new("easytier-core.exe") match Command::new("easytier/easytier-core.exe")
.arg("--version") .arg("--version")
.creation_flags(0x08000000) .creation_flags(0x08000000)
.output() .output()
@@ -82,13 +73,82 @@ fn get_core_version() -> String {
} }
} }
#[tauri::command(rename_all = "snake_case")]
fn get_cli_version() -> String {
match Command::new("easytier/easytier-cli.exe")
.arg("--version")
.creation_flags(0x08000000)
.output()
{
Ok(output) => {
let output_str = String::from_utf8_lossy(&output.stdout);
return output_str.trim().to_string();
}
Err(_e) => return "".to_string(),
}
}
#[derive(Serialize, Deserialize, Debug)]
struct RoutePeerInfo {
peer_id: u64,
cost: u64,
hostname: Option<String>,
udp_stun_info: String,
easytier_version: String,
network_length: u64,
}
#[derive(Serialize, Deserialize, Debug)]
struct RouteTable {
peer_infos: HashMap<u32, RoutePeerInfo>,
ipv4_peer_id_map: HashMap<String, u64>,
}
#[derive(Serialize, Deserialize, Debug)]
struct PeerRouteServiceImpl {
route_table_with_cost: RouteTable,
}
#[derive(Serialize, Deserialize, Debug)]
struct PeerRoute {
my_peer_id: u64,
service_impl: PeerRouteServiceImpl,
}
#[derive(Serialize, Deserialize, Debug)]
struct MyResponse {
response: PeerRoute,
}
#[tauri::command(rename_all = "snake_case")]
fn get_members_by_cli() -> String {
match Command::new("easytier/easytier-cli.exe")
.arg("peer")
.arg("list")
.creation_flags(0x08000000)
.output()
{
Ok(output) => {
let output_str = String::from_utf8_lossy(&output.stdout);
return output_str.trim().to_string();
}
Err(_e) => return _e.to_string(),
}
}
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
async fn download_easytier_zip(download_url: String, file_name: String) { async fn download_easytier_zip(download_url: String, file_name: String) {
let target = format!("https://ghp.ci/?q={}",download_url); let target = format!("{}", download_url);
let response = reqwest::get(target) let response = reqwest::get(target)
.await .await
.expect("error to download easytier url"); .expect("error to download easytier url");
let file_path = format!("./{}", file_name); let file_path = format!("./easytier/{}", file_name);
let easytier_dir = path::Path::new("./easytier");
if !easytier_dir.exists() {
fs::create_dir_all(&easytier_dir).unwrap();
}
let path = path::Path::new(&file_path); let path = path::Path::new(&file_path);
let mut file = match File::create(&path) { let mut file = match File::create(&path) {
@@ -137,7 +197,14 @@ fn unzip(fname: &path::Path) {
// fs::create_dir_all(p).unwrap(); // fs::create_dir_all(p).unwrap();
// } // }
// } // }
let out_file_path = path::Path::new("./").join(outpath.file_name().clone().unwrap());
let easytier_dir = path::Path::new("./easytier");
// if !easytier_dir.exists() {
// fs::create_dir_all(&easytier_dir).unwrap();
// }
// let out_file_path = path::Path::new("./").join(outpath.file_name().clone().unwrap());
let out_file_path = easytier_dir.join(outpath.file_name().clone().unwrap());
println!("outFilePath: {}", out_file_path.display()); println!("outFilePath: {}", out_file_path.display());
let mut outfile = fs::File::create(&out_file_path).unwrap(); let mut outfile = fs::File::create(&out_file_path).unwrap();
std::io::copy(&mut file, &mut outfile).unwrap(); std::io::copy(&mut file, &mut outfile).unwrap();
@@ -150,18 +217,16 @@ fn run_command(
args: Vec<String>, args: Vec<String>,
stop_signal: tauri::State<Arc<AtomicBool>>, stop_signal: tauri::State<Arc<AtomicBool>>,
) { ) {
// if !path::Path::new("easytier-core.exe").exists() {
// app_handle.emit("command-output", "easytier-core.exe 不存在");
// return
// }
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
stop_signal.store(false, Ordering::Relaxed); stop_signal.store(false, Ordering::Relaxed);
let app_handle1 = app_handle.clone(); let app_handle1 = app_handle.clone();
let app_handle2 = app_handle.clone(); let app_handle2 = app_handle.clone();
let stop_signal = Arc::clone(&stop_signal); let stop_signal1 = Arc::clone(&stop_signal);
let stop_signal2 = Arc::clone(&stop_signal);
let args2 = args.clone(); let args2 = args.clone();
thread::spawn(move || { thread::spawn(move || {
let mut child = Command::new("easytier-core.exe") // trace, debug, info, warn, error, off
let mut child = Command::new("easytier/easytier-core.exe")
.args(args) .args(args)
.creation_flags(0x08000000) .creation_flags(0x08000000)
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@@ -189,13 +254,13 @@ fn run_command(
} }
} }
} }
stop_signal1.store(true, Ordering::Relaxed);
println!("end"); println!("end");
}); });
thread::spawn(move || { thread::spawn(move || {
while let Ok(line) = rx.recv() { while let Ok(line) = rx.recv() {
if stop_signal.load(Ordering::Relaxed) { if stop_signal2.load(Ordering::Relaxed) {
break; break;
} }
app_handle2 app_handle2
@@ -206,8 +271,7 @@ fn run_command(
} }
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
fn stop_command(child_id: u32, stop_signal: tauri::State<Arc<AtomicBool>>) { fn stop_command(child_id: u32) {
println!("stop command");
if child_id != 0 { if child_id != 0 {
let output = Command::new("taskkill") let output = Command::new("taskkill")
.arg("/F") .arg("/F")
@@ -223,8 +287,9 @@ fn stop_command(child_id: u32, stop_signal: tauri::State<Arc<AtomicBool>>) {
} else { } else {
eprintln!("Failed to terminate process {}.", child_id); eprintln!("Failed to terminate process {}.", child_id);
} }
} else {
println!("child id is 0");
} }
stop_signal.store(true, Ordering::Relaxed);
} }
#[tauri::command(rename_all = "snake_case")] #[tauri::command(rename_all = "snake_case")]
@@ -262,23 +327,193 @@ fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
} }
} }
#[tauri::command(rename_all = "snake_case")]
fn search_pid_by_pname(target_process_name: String) -> u32 {
// 创建一个新的 System 实例
let mut system = System::new_all();
// 刷新所有进程信息
system.refresh_all();
// 遍历所有进程
for (pid, process) in system.processes() {
if process
.name()
.to_string_lossy()
.to_lowercase()
.contains(&target_process_name.to_lowercase())
{
println!("Found process '{}' with PID: {}", target_process_name, pid);
return pid.as_u32();
}
}
return 0;
}
#[tauri::command]
fn get_exe_directory() -> Vec<String> {
let mut ret_vec: Vec<String> = Vec::new();
match std::env::current_exe() {
Ok(exe_path) => {
println!("Path of this executable is: {}", exe_path.display());
ret_vec.push(exe_path.display().to_string());
ret_vec.push(exe_path.parent().unwrap().display().to_string());
return ret_vec;
}
Err(e) => {
println!("failed to get current exe path: {e}");
ret_vec.push("".to_string());
ret_vec.push("".to_string());
return ret_vec;
}
};
}
#[tauri::command]
fn spawn_autostart(enabled: bool) {
let (tx, rx) = mpsc::channel();
thread::spawn(move || match autostart(enabled) {
Ok(_) => {
let _ = tx.send(true);
}
Err(e) => {
println!("Error: {}", e);
let _ = tx.send(false);
}
});
match rx.recv() {
Ok(_) => {
// println!("autostart enabled: {}", enabled);
}
Err(_e) => {}
}
}
fn autostart_enabled() -> Result<bool, Box<dyn std::error::Error>> {
unsafe {
let task_service: ITaskService = CoCreateInstance(&TaskScheduler, None, CLSCTX_ALL)?;
task_service.Connect(
&VARIANT::default(),
&VARIANT::default(),
&VARIANT::default(),
&VARIANT::default(),
)?;
// 指定要删除的任务文件夹路径
let folder_path = BSTR::from("\\easytierGame");
// let root = BSTR::from("\\");
let task_name = BSTR::from("auto start");
let mut penabled = VARIANT_BOOL::from(false);
let bool_ptr: *mut VARIANT_BOOL = &mut penabled;
// 获取任务文件夹F
let task_folder: ITaskFolder = task_service.GetFolder(&folder_path)?;
let task = task_folder.GetTask(&task_name)?;
task.Definition()?
.Triggers()?
.get_Item(1)?
.Enabled(bool_ptr)?;
// 释放 COM 库
CoUninitialize();
// return penabled.as_bool();
Ok(penabled.as_bool())
}
}
#[tauri::command]
fn autostart_is_enabled() -> bool {
match autostart_enabled() {
Ok(enabled) => {
println!("autostart enabled: {}", enabled);
return enabled;
}
Err(e) => {
println!("autostart enabled: false -> {}", e);
return false;
}
}
}
fn autostart(enabled: bool) -> std::result::Result<(), Box<dyn std::error::Error>> {
if !enabled {
unsafe {
let task_service: ITaskService = CoCreateInstance(&TaskScheduler, None, CLSCTX_ALL)?;
task_service.Connect(
&VARIANT::default(),
&VARIANT::default(),
&VARIANT::default(),
&VARIANT::default(),
)?;
// 指定要删除的任务文件夹路径
let folder_path = BSTR::from("\\easytierGame");
let root = BSTR::from("\\");
let task_name = BSTR::from("auto start");
// 获取任务文件夹
let task_folder: ITaskFolder = task_service.GetFolder(&folder_path)?;
task_folder.DeleteTask(&task_name, 0)?;
println!("Task AutoStart Task deleted successfully.");
let task_folder: ITaskFolder = task_service.GetFolder(&root)?;
// 删除任务文件夹
task_folder.DeleteFolder(&folder_path, 0)?;
println!("Task folder easytierGame deleted successfully.");
// 释放 COM 库
CoUninitialize();
}
} else {
let ts = planIfTaskScheduler::new()?;
let com = ts.get_com();
let sb = ScheduleBuilder::new(&com).unwrap();
let exe = std::env::current_exe()?;
let exe = exe.to_str().unwrap();
let settings = PrincipalSettings {
display_name: "".to_string(),
group_id: None,
id: "".to_string(),
logon_type: LogonType::InteractiveToken,
run_level: RunLevel::Highest,
user_id: Some(whoami::username()),
};
sb.create_logon()
.author("heixiansen")?
.trigger("trigger", enabled)?
.action(Action::new("auto start", exe, "", ""))?
.in_folder("easytierGame")?
.principal(settings)?
.delay(Duration {
seconds: Some(6),
..Default::default()
})?
.build()?
.register("auto start", TaskCreationFlags::CreateOrUpdate as i32)?;
}
Ok(())
}
pub const AUTOSTART_ARG: &str = "--autostart";
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let stop_signal = Arc::new(AtomicBool::new(false)); // 创建一个原子布尔值,用于控制命令的停止 let stop_signal = Arc::new(AtomicBool::new(false)); // 创建一个原子布尔值,用于控制命令的停止
let stop_signal_clone = Arc::clone(&stop_signal); // 创建一个原子布尔值的克隆,用于传递给命令 let stop_signal_clone = Arc::clone(&stop_signal); // 创建一个原子布尔值的克隆,用于传递给命令
let context = tauri::generate_context!(); let context = tauri::generate_context!();
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![AUTOSTART_ARG]),
))
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.on_window_event(move |window, event| {
if let WindowEvent::CloseRequested { api, .. } = event {
unsafe {
if !IS_CLOSE {
api.prevent_close();
window.emit("window-close-event", "").unwrap();
}
}
}
})
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
let _ = app let _ = app
.get_webview_window("main") .get_webview_window("main")
@@ -316,13 +551,17 @@ pub fn run() {
}) })
.manage(stop_signal_clone) .manage(stop_signal_clone)
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
my_commit_output,
force_close,
run_command, run_command,
stop_command, stop_command,
get_core_version, get_core_version,
fetch_easytier_list, fetch_easytier_list,
download_easytier_zip download_easytier_zip,
get_cli_version,
get_members_by_cli,
search_pid_by_pname,
get_exe_directory,
spawn_autostart,
autostart_is_enabled
]) ])
.run(context) .run(context)
.expect("error while running tauri application"); .expect("error while running tauri application");
+21 -6
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "easytier-game", "productName": "easytier-game",
"version": "1.0.3", "version": "1.1.5",
"identifier": "com.tauri.easytier-game", "identifier": "com.tauri.easytier-game",
"build": { "build": {
@@ -13,14 +13,14 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "easytier-game", "title": "easytier-game 1.1.5",
"width": 331, "width": 335,
"height": 285, "height": 305,
"resizable": false, "resizable": false,
"fullscreen": false, "fullscreen": false,
"decorations": true, "decorations": true,
"maximizable": false,
"center": true, "center": true,
"maximizable": false,
"transparent": false "transparent": false
} }
], ],
@@ -28,10 +28,25 @@
"csp": null "csp": null
} }
}, },
"plugins": {},
"bundle": { "bundle": {
"active": false, "active": false,
"targets": "all", "targets": "all",
"resources": [
"easytier/config/",
"easytier/icons/",
"easytier/tool/WinIPBroadcast.exe",
"easytier/easytier-cli.exe",
"easytier/easytier-core.exe",
"easytier/Packet.dll",
"easytier/wintun.dll"
],
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
},
"createUpdaterArtifacts": false, "createUpdaterArtifacts": false,
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"] "icon": ["icons/icon.png", "icons/icon.rgba", "icons/icon.icns", "icons/icon.ico"]
} }
} }
+42 -1
View File
@@ -9,26 +9,67 @@ export default defineStore("main", {
networkPassword: "", networkPassword: "",
hostname: "", hostname: "",
ipv4: "", ipv4: "",
proxyNetworks: "", // 子网代理
autoStart: false, // 是否自动启动
coonectAfterStart: false, //软件打开后,是否自动连接
disableIpv6: false, // 是否禁用IPv6 disableIpv6: false, // 是否禁用IPv6
disbleListenner: false, // 是否禁用监听 disbleListenner: false, // 是否禁用监听
disbleP2p: true, // 是否使用P2P disableEncryption: false, // 是否禁用加密
multiThread: false, //使用多线程
enablExitNode: false, // 是否启用退出节点
noTun: false, // 是否使用TUN
latencyfirst: false, // 是否优先延迟
useSmoltcp: false, // 是否为子网代理启用smoltcp堆栈
disableUdpHolePunching: false, // 是否禁用UDP打洞
relayAllPeerrpc: false, // 是否启用所有对等RPC
disbleP2p: false, // 是否使用P2P
dhcp: true, // 是否使用DHCP dhcp: true, // 是否使用DHCP
saveErrorLog: true, // 是否保存错误日志
logLevel: "error", //日志等级
devName: false, //自定义网卡名
devNameValue: "", //自定义网卡名
}, },
theme: false, //主题 false light true dark
configStartEnable: false, //使用配置文件启动
configPath: "", //配置文件路径
cidrEnable: false,
winipBcPid: 0,
winipBcStart: false,
winIpBcAutoStart: true,
basePeers: ["public.easytier.top:11010"], basePeers: ["public.easytier.top:11010"],
}; };
}, },
persist: { persist: {
paths: [ paths: [
"basePeers", "basePeers",
"cidrEnable",
"theme",
"winIpBcAutoStart",
"config.proxyNetworks",
"config.serverUrl", "config.serverUrl",
"config.networkName", "config.networkName",
"config.protocol", "config.protocol",
"config.networkPassword", "config.networkPassword",
"config.disbleP2p", "config.disbleP2p",
"config.autoStart",
"config.coonectAfterStart",
"config.disableIpv6", "config.disableIpv6",
"config.disbleListenner", "config.disbleListenner",
"config.disableEncryption",
"config.multiThread",
"config.enablExitNode",
"config.noTun",
"config.latencyfirst",
"config.useSmoltcp",
"config.disableUdpHolePunching",
"config.relayAllPeerrpc",
"config.hostname", "config.hostname",
"config.ipv4",
"config.dhcp", "config.dhcp",
"config.saveErrorLog",
"config.logLevel",
"config.devName",
"config.devNameValue",
], ],
}, },
}); });
+1
View File
@@ -6,6 +6,7 @@
"components/**/*.ts", "components/**/*.ts",
"components/**/*.js", "components/**/*.js",
"components/**/*.vue", "components/**/*.vue",
"app.vue",
"pages/**/*.ts", "pages/**/*.ts",
"pages/**/*.js", "pages/**/*.js",
"pages/**/*.vue", "pages/**/*.vue",
+6
View File
@@ -1 +1,7 @@
interface ImportMetaEnv {
readonly VITE_CONFIG_PATH: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+35 -4
View File
@@ -77,7 +77,38 @@ export const ATJ = (promise: Promise<any>, errorExt: string | undefined = undefi
}); });
}; };
export const openBrowser = (key: string) => { export const parsePeerInfo = (content: string) => {
// logger.info(`${import.meta.env.VITE_BROWSER_JOB_URL}${key}`) // 将表格字符串分割成行
// _launcherApi.openBrowser(`${import.meta.env.VITE_BROWSER_JOB_URL}${key}`); const lines = content.split('\n')
};
// 提取表头(keys
const headers = lines[1]
.split('│')
.slice(1, -1)
.map((h) => h.trim())
// 初始化结果数组
const result: any[] = []
// 遍历数据行
for (let i = 3; i < lines.length - 1; i += 2) {
if (lines[i].trim() === '') continue // 跳过空行
// 分割每一行的数据
const values = lines[i]
.split('│')
.slice(1, -1)
.map((v) => v.trim())
// 创建对象并添加到结果数组
const obj: any = {}
headers.forEach((header, index) => {
obj[header] = values[index] === '-' || values[index] === '' ? null : values[index]
})
// 每行数据都作为一个新对象添加到结果数组中
result.push(obj)
}
return result
}
+70
View File
@@ -0,0 +1,70 @@
import useMainStore from "@/stores/index";
import * as tauriAutoStart from "@tauri-apps/plugin-autostart";
import { open, Command } from "@tauri-apps/plugin-shell";
import { invoke } from "@tauri-apps/api/core";
const getExeAbsPath = async () => {
const aExePath = (await invoke("get_exe_directory")) as [string, string];
return aExePath;
};
const checkServerOnWindows = async () => {
try {
const serverStatus = await Command.create("nssm", ["status", import.meta.env.VITE_AUTO_START_SERVICE_NAME]).execute();
if (serverStatus.stderr.includes("Can't open service")) {
return false;
}
return true;
} catch (err) {
return false;
}
};
const handleAutoStartByNssm = async () => {
const config = useMainStore().config;
const [exeAbsPath, parentDir] = await getExeAbsPath();
const isExist = await checkServerOnWindows();
if (isExist) {
const outputStop = await Command.create("nssm", ["stop", import.meta.env.VITE_AUTO_START_SERVICE_NAME, "confirm"]).execute();
console.log({ outputStop });
const outputRemove = await Command.create("nssm", ["remove", import.meta.env.VITE_AUTO_START_SERVICE_NAME, "confirm"]).execute();
console.log({ outputRemove });
}
if (exeAbsPath) {
const output = await Command.create("nssm", ["install", import.meta.env.VITE_AUTO_START_SERVICE_NAME, exeAbsPath]).execute();
console.log(output);
if (output.stdout && output.stdout.includes("installed successfully")) {
const outputAppDirectory = await Command.create("nssm", [
"set",
import.meta.env.VITE_AUTO_START_SERVICE_NAME,
"AppDirectory",
parentDir
]).execute();
console.log({ outputAppDirectory });
const outputStart = await Command.create("nssm", [
"set",
import.meta.env.VITE_AUTO_START_SERVICE_NAME,
"Start",
"SERVICE_AUTO_START"
]).execute();
console.log({ outputStart });
config.autoStart = true;
}
}
};
// 兼容autostart插件
const compatibleAutoStart = async () => {
const config = useMainStore().config;
const is_enable = await tauriAutoStart.isEnabled();
if (is_enable) {
try {
await tauriAutoStart.disable();
config.autoStart = false;
} catch (err) {
// ElMessage.error(`取消自启失败`);
}
}
const serverStatus = await Command.create("nssm", ["status", import.meta.env.VITE_AUTO_START_SERVICE_NAME]).execute();
console.log(serverStatus);
};