Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92d25ab907 | ||
|
|
1c8314c6fa | ||
|
|
b352d49e0b | ||
|
|
44183b1ad2 | ||
|
|
fe1bc6f89e | ||
|
|
be59e56bd1 | ||
|
|
f556e17f85 | ||
|
|
c7170967ec | ||
|
|
1801082601 | ||
|
|
189f3467c3 | ||
|
|
bf1403bf07 | ||
|
|
7a181200f3 | ||
|
|
9413449c8e | ||
|
|
2eb69352f7 | ||
|
|
9ea0eea0ac | ||
|
|
ef172e4d4c | ||
|
|
c572925a0d | ||
|
|
1a5d2cc8fc | ||
|
|
e4fbaa921c | ||
|
|
cf351b8708 | ||
|
|
ddf4c32ae6 | ||
|
|
76d29cbeeb | ||
|
|
3adf64e251 | ||
|
|
4e4ebf1246 | ||
|
|
6f57a866a4 | ||
|
|
4296f4a338 | ||
|
|
fe9f1c01b7 | ||
|
|
60078f00f7 | ||
|
|
43d681e888 | ||
|
|
d0567e96b5 | ||
|
|
d402f107b0 | ||
|
|
8c911f47f9 | ||
|
|
74fbfed7be | ||
|
|
f3fc37957c | ||
|
|
a4d6fe6e95 | ||
|
|
9b883d686d | ||
|
|
2ac0afb847 | ||
|
|
5b9eed9520 | ||
|
|
a04fae725d | ||
|
|
1ae122ab2a | ||
|
|
3db962f3dc | ||
|
|
a7f1292067 | ||
|
|
3150878dbd | ||
|
|
022e71de01 | ||
|
|
0467074eec | ||
|
|
2e598ac026 | ||
|
|
5350af87ed | ||
|
|
4bb1216a07 | ||
|
|
5d6b7bdf13 | ||
|
|
5740bd08c1 |
@@ -9,4 +9,8 @@ dist
|
||||
.fleet
|
||||
.idea
|
||||
.vscode
|
||||
release
|
||||
release
|
||||
src-tauri/easytier-cli.exe
|
||||
src-tauri/easytier-core.exe
|
||||
src-tauri/Packet.dll
|
||||
src-tauri/wintun.dll
|
||||
|
||||
@@ -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.
|
||||
@@ -1,10 +1,65 @@
|
||||
# EasyiterGame
|
||||
基于easytier的简易游戏联机客户端
|
||||
# [EasyTier游戏联机启动器](https://github.com/EasyTier/EasytierGame)
|
||||
|
||||
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包,个人不喜欢安装包各种写注册表,解压即用就行,目录清爽干净
|
||||
|
||||

|
||||
|
||||
|
||||
## 使用教程
|
||||
|
||||
- 第一次使用,输入一个“主机名”点击启动联机即可,后续可以自建服务器或者使用爱心群友提供的服务器
|
||||

|
||||
|
||||

|
||||
|
||||
- 高级选项里有一些特殊配置,可以自行选择
|
||||

|
||||
|
||||
- 如果还是无法满足您的需求,可以使用配置文件进行启动,具体如何配置,可以查看文档[配置文件](/guide/network/config-file.html)
|
||||

|
||||
|
||||
- easytier内核升级后,可以点击更新插件按钮就可以进行更新,但是需要出国,如果无法更新,可以在群里获取
|
||||

|
||||
|
||||
- 1.1.4更新了 配置分享功能 可以与朋友之间分享配置,方便联机
|
||||
|
||||
|
||||

|
||||
|
||||
- 1.1.5 合并额外插件于 "增强工具" 窗口 新增了网卡跃点设置 关闭防火墙功能 ping功能
|
||||

|
||||
|
||||

|
||||
|
||||
- 1.1.8 新增了 生成 easytier/config.json 的功能 会将一部分配置写入到 config.json 文件中,方便用户自定义配置,你可以在高级设置里启用和关闭这个功能(默认关闭)
|
||||
**需要注意的是,如果config.json存在,每次启动easytierGame默认按照config.json的配置为准**
|
||||
**你可以查看 config_template.json 里的注释进行配置**
|
||||

|
||||
|
||||
## 特性
|
||||
|
||||
- 基于easytier组网工具开发,界面清晰简单
|
||||
- 自带“更新”按钮,在easytier组网工具发布新版本时,点击更新即可(需要出国工具)
|
||||
- 第一次使用,输入一个“主机名”点击启动联机即可,后续可以自建服务器或者使用群友的服务器
|
||||
- 配置简单,也含有高级功能,同时也支持自定义配置文件启动
|
||||
- 默认开启 **WinIPBroadcast** 不再怕联机找不到房间(例如:无主之地3)
|
||||
- 已经测试 **艾尔登法环学习版**,**无主之地3**,**深岩银河**,**怪物猎人世界** 等都可稳定联机游玩
|
||||
|
||||
## 系统支持
|
||||
|
||||
支持Windows 11 、Windows 10 、 Windows 7
|
||||
|
||||
|
||||
|
||||
## 请不要将本程序和仓库代码用于任何违法用途,由此产生的一切后果,仓库所有者和参与开发的人员不承担任何责任
|
||||
|
||||
2. 生产环境发布
|
||||
执行 pnpm tauri build
|
||||
产物位于 src-tauri/target/release 下 easytier-game.exe
|
||||
|
||||
@@ -4,4 +4,9 @@
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
<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>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
@tailwind base;
|
||||
/* @tailwind base; */
|
||||
|
||||
/*
|
||||
用于进行主题更换,如果要更换elemenet-plus-ui的主题记得加上!important,不然更换会失败
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
/* @layer base {
|
||||
html {
|
||||
font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti, "Microsoft YaHei";
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
html,
|
||||
body {
|
||||
@@ -19,25 +19,16 @@ body {
|
||||
* {
|
||||
outline: none;
|
||||
}
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
html.dark body {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
#__easytier {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
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;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,16 @@
|
||||
import { BaseDirectory, writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import useMainStore from "@/stores/index";
|
||||
import { ElMessage } from "element-plus";
|
||||
export const updateConfigJson = async () => {
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const path = import.meta.env.VITE_CONFIG_FILE_NAME;
|
||||
try {
|
||||
const { proxyNetworks, autoStart, relayAllPeerrpc, coonectAfterStart, multiThread, enablExitNode, useSmoltcp, saveErrorLog, logLevel, ...otherConfig } =
|
||||
mainStore.config;
|
||||
await writeTextFile(path, JSON.stringify(otherConfig, null, 4), { baseDir: BaseDirectory.Resource });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
ElMessage.error(`更新config.json失败`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// const infoDialog = new WebviewWindow("log", {
|
||||
// title: "日志",
|
||||
// resizable: false,
|
||||
// width: 600,
|
||||
// height: 400,
|
||||
// parent: appWindow,
|
||||
// url: "/log"
|
||||
// });
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,126 @@
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
|
||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import pkg from '@/package.json'
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { TrayIcon } from "@tauri-apps/api/tray";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
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() {
|
||||
if (await getCurrentWindow().isVisible()) {
|
||||
await getCurrentWindow().hide()
|
||||
}
|
||||
else {
|
||||
await getCurrentWindow().show()
|
||||
await getCurrentWindow().setFocus()
|
||||
}
|
||||
if (await getCurrentWindow().isVisible()) {
|
||||
await getCurrentWindow().hide();
|
||||
} else {
|
||||
await getCurrentWindow().show();
|
||||
await getCurrentWindow().setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
export async function useTray(init: boolean = false) {
|
||||
let tray
|
||||
try {
|
||||
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
|
||||
if (!tray) {
|
||||
tray = await TrayIcon.new({
|
||||
tooltip: `EasyTier\n${pkg.version}`,
|
||||
title: `EasyTier\n${pkg.version}`,
|
||||
id: DEFAULT_TRAY_NAME,
|
||||
menu: await Menu.new({
|
||||
id: 'main',
|
||||
items: await generateMenuItem(),
|
||||
}),
|
||||
action: async () => {
|
||||
toggleVisibility()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Error while creating tray icon:', error)
|
||||
return null
|
||||
}
|
||||
export async function useTray(init: boolean = false, beforExit: Function) {
|
||||
let tray;
|
||||
try {
|
||||
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME);
|
||||
if (!tray) {
|
||||
tray = await TrayIcon.new({
|
||||
tooltip: `EasyTierGame\n${pkg.version}`,
|
||||
title: `EasyTierGame\n${pkg.version}`,
|
||||
id: DEFAULT_TRAY_NAME,
|
||||
menu: await Menu.new({
|
||||
id: "main",
|
||||
items: await generateMenuItem(beforExit),
|
||||
}),
|
||||
action: async (e) => {
|
||||
toggleVisibility();
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error while creating tray icon:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (init) {
|
||||
tray.setTooltip(`EasyTier\n${pkg.version}`)
|
||||
tray.setMenuOnLeftClick(false)
|
||||
tray.setMenu(await Menu.new({
|
||||
id: 'main',
|
||||
items: await generateMenuItem(),
|
||||
}))
|
||||
}
|
||||
if (init) {
|
||||
tray.setTooltip(`EasyTierGame\n${pkg.version}`);
|
||||
tray.setMenuOnLeftClick(false);
|
||||
tray.setMenu(
|
||||
await Menu.new({
|
||||
id: "main",
|
||||
items: await generateMenuItem(beforExit),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return tray
|
||||
return tray;
|
||||
}
|
||||
|
||||
export async function generateMenuItem() {
|
||||
return [
|
||||
await MenuItemExit('Exit'),
|
||||
await PredefinedMenuItem.new({ item: 'Separator' }),
|
||||
await MenuItemShow('Show / Hide'),
|
||||
]
|
||||
export async function generateMenuItem(beforExit: Function) {
|
||||
return [
|
||||
await MenuItemShow("显示 / 隐藏"),
|
||||
await MenuItemTheme(),
|
||||
await PredefinedMenuItem.new({ item: "Separator" }),
|
||||
await MenuItemExit("退出", beforExit),
|
||||
];
|
||||
}
|
||||
|
||||
export async function MenuItemExit(text: string) {
|
||||
return await PredefinedMenuItem.new({
|
||||
text,
|
||||
item: 'Quit',
|
||||
})
|
||||
export async function MenuItemExit(text: string, beforExit: Function) {
|
||||
return await MenuItem.new({
|
||||
id: "quit",
|
||||
text,
|
||||
action: async () => {
|
||||
if (beforExit) {
|
||||
await beforExit();
|
||||
}
|
||||
await getCurrentWindow().close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function MenuItemShow(text: string) {
|
||||
return await MenuItem.new({
|
||||
id: 'show',
|
||||
text,
|
||||
action: async () => {
|
||||
await toggleVisibility()
|
||||
},
|
||||
})
|
||||
return await MenuItem.new({
|
||||
id: "show",
|
||||
text,
|
||||
action: async () => {
|
||||
await toggleVisibility();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
|
||||
const tray = await useTray()
|
||||
if (!tray)
|
||||
return
|
||||
const menu = await Menu.new({
|
||||
id: 'main',
|
||||
items: items || await generateMenuItem(),
|
||||
})
|
||||
tray.setMenu(menu)
|
||||
export async function MenuItemTheme() {
|
||||
return await MenuItem.new({
|
||||
id: "theme",
|
||||
text: "主题切换",
|
||||
action: async () => {
|
||||
const mainStore = useMainStore();
|
||||
mainStore.theme = !mainStore.theme;
|
||||
// const appWindow = getCurrentWindow();
|
||||
// appWindow.emitTo("main", "config", { theme: mainStore.theme });
|
||||
await setTheme(mainStore.theme);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function setTrayRunState(isRunning: boolean = false) {
|
||||
const tray = await useTray()
|
||||
if (!tray)
|
||||
return
|
||||
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
|
||||
// export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
|
||||
// // const tray = await useTray()
|
||||
// const tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
|
||||
// if (!tray)
|
||||
// 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) {
|
||||
if (tooltip) {
|
||||
const tray = await useTray()
|
||||
if (!tray)
|
||||
return
|
||||
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||
}
|
||||
}
|
||||
export async function setTrayTooltip(tray: TrayIcon | null, tooltip?: string | null) {
|
||||
if (!tray) return;
|
||||
if (tooltip) {
|
||||
await tray.setTooltip(`EasyTierGame\n${pkg.version}\n${tooltip}`);
|
||||
} else {
|
||||
await tray.setTooltip(`EasyTierGame\n${pkg.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow, PhysicalPosition, type WindowOptions, Window, currentMonitor } 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 unListenlogClose: UnlistenFn | null = null;
|
||||
let unlistenLogCreated: UnlistenFn | null = null;
|
||||
let dialog = await WebviewWindow.getByLabel(label);
|
||||
if (!dialog) {
|
||||
let defaultOpts: {[key:string]: any, parent: Window | undefined} = {
|
||||
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 monitor = await currentMonitor();
|
||||
// console.log(monitor)
|
||||
const factor = await appWindow.scaleFactor();
|
||||
const appPosition = await appWindow.outerPosition();
|
||||
// console.log((appPosition.x + appSize.width) / 1.25);
|
||||
const logicalPosition = new PhysicalPosition(appPosition.x + appSize.width, appPosition.y).toLogical(factor);
|
||||
defaultOpts.parent = appWindow;
|
||||
// console.log(logicalPosition);
|
||||
defaultOpts.x = logicalPosition.x;
|
||||
defaultOpts.y = logicalPosition.y;
|
||||
}
|
||||
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)());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
VITE_CONFIG_PATH=easytier/config/
|
||||
VITE_CONFIG_FILE_NAME=easytier/config.json
|
||||
VITE_LOG_PATH=easytier/logs/
|
||||
VITE_AUTO_START_SERVICE_NAME=easytierGameAutoStart
|
||||
@@ -0,0 +1,4 @@
|
||||
VITE_CONFIG_PATH=easytier/config/
|
||||
VITE_CONFIG_FILE_NAME=easytier/config.json
|
||||
VITE_LOG_PATH=easytier/logs/
|
||||
VITE_AUTO_START_SERVICE_NAME=easytierGameAutoStart
|
||||
@@ -3,23 +3,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
|
||||
const unlisten = listen("window-close-event", (event) => {
|
||||
closeApp();
|
||||
//屏蔽右键菜单
|
||||
document.addEventListener("contextmenu", (e: MouseEvent) => {
|
||||
if (import.meta.env.PROD) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unlisten.then((unlisten) => {
|
||||
unlisten && unlisten instanceof Function && unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
const closeApp = async () => {
|
||||
appWindow.hide();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { defineNuxtConfig } from "nuxt/config";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import vueJSX from "@vitejs/plugin-vue-jsx";
|
||||
const isDev = process?.argv?.[2] == "_dev" || process?.argv?.[2] == "dev";
|
||||
const optimizeDepsElementPlusIncludes = ["element-plus/es"];
|
||||
fs.readdirSync("node_modules/element-plus/es/components").map(dirname => {
|
||||
fs.access(`node_modules/element-plus/es/components/${dirname}/style/css.mjs`, err => {
|
||||
if (!err) {
|
||||
optimizeDepsElementPlusIncludes.push(`element-plus/es/components/${dirname}/style/css`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
|
||||
@@ -22,8 +13,19 @@ export default defineNuxtConfig({
|
||||
autoImport: false
|
||||
},
|
||||
|
||||
css: ["~/assets/css/main.css"],
|
||||
modules: ["@element-plus/nuxt", "@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt", "@nuxtjs/tailwindcss"],
|
||||
css: ["~/assets/css/main.css", "element-plus/theme-chalk/dark/css-vars.css"],
|
||||
modules: [
|
||||
"@element-plus/nuxt",
|
||||
"@pinia/nuxt",
|
||||
[
|
||||
"pinia-plugin-persistedstate/nuxt",
|
||||
{
|
||||
key: "__glj_persisted_%id",
|
||||
storage: "localStorage"
|
||||
}
|
||||
],
|
||||
"@nuxtjs/tailwindcss"
|
||||
],
|
||||
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./")
|
||||
@@ -44,12 +46,10 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
vite: {
|
||||
plugins: [
|
||||
vueJSX({}),
|
||||
],
|
||||
plugins: [vueJSX({})],
|
||||
envDir: "env",
|
||||
optimizeDeps: {
|
||||
include: [...optimizeDepsElementPlusIncludes]
|
||||
// include: [...optimizeDepsElementPlusIncludes]
|
||||
},
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
@@ -70,7 +70,7 @@ export default defineNuxtConfig({
|
||||
sourcemap: !!process.env.TAURI_DEBUG
|
||||
},
|
||||
esbuild: {
|
||||
pure: ["console.log"],
|
||||
// pure: ["console.log"],
|
||||
drop: ["debugger"]
|
||||
}
|
||||
},
|
||||
@@ -84,7 +84,7 @@ export default defineNuxtConfig({
|
||||
|
||||
app: {
|
||||
rootId: "__easytier",
|
||||
cdnURL: "./",
|
||||
cdnURL: "./",
|
||||
buildAssetsDir: "__easytier/",
|
||||
head: {
|
||||
meta: [
|
||||
@@ -102,5 +102,6 @@ export default defineNuxtConfig({
|
||||
// script: [],
|
||||
// noscript: []
|
||||
}
|
||||
}
|
||||
},
|
||||
compatibilityDate: "2024-11-12"
|
||||
});
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
{
|
||||
"name": "easytier-game",
|
||||
"private": true,
|
||||
"author": "luoleixin@digisky.com",
|
||||
"author": "leizi97",
|
||||
"description": "A simple network initiator based on Easytier",
|
||||
"version": "1.0.3",
|
||||
"version": "1.1.8",
|
||||
"scripts": {
|
||||
"dev": "nuxt dev --dotenv env/.env.dev --host 0.0.0.0",
|
||||
"build": "nuxt generate --dotenv env/.env.prod"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@element-plus/nuxt": "^1.0.9",
|
||||
"@element-plus/nuxt": "^1.1.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"@pinia-plugin-persistedstate/nuxt": "^1.2.0",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@tauri-apps/api": "^2.0.2",
|
||||
"@tauri-apps/cli": "^2.0.2",
|
||||
"@pinia/nuxt": "^0.7.0",
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@tauri-apps/plugin-autostart": "~2",
|
||||
"@tauri-apps/plugin-cli": "^2.0.0",
|
||||
"@tauri-apps/plugin-http": "^2.0.0",
|
||||
"@tinymce/tinymce-vue": "^5.1.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "~2",
|
||||
"@tauri-apps/plugin-http": "^2.0.1",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"defu": "^6.1.4",
|
||||
"element-plus": "^2.7.1",
|
||||
"javascript-obfuscator": "^4.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.0",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"element-plus": "^2.8.7",
|
||||
"less": "^4.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nuxt": "^3.11.2",
|
||||
"pinia": "^2.1.7",
|
||||
"nuxt": "^3.13.2",
|
||||
"pinia": "^2.2.6",
|
||||
"pinia-plugin-persistedstate": "^4.1.3",
|
||||
"postcss": "^8.4.38",
|
||||
"qs": "^6.12.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vue": "^3.4.24",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"@tauri-apps/plugin-shell": "~2"
|
||||
"typescript": "^5.6.3",
|
||||
"vue-clipboard3": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<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 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>
|
||||
<div><ElCheckbox v-model="mainStore.config.coonectAfterStart">软件启动后,自动"启动联机"(搭配开机自启,无感联机)</ElCheckbox></div>
|
||||
<div class="flex items-center gap-[15px] flex-nowrap">
|
||||
<ElCheckbox v-model="mainStore.createConfigInEasytier">自动生成界面配置文件easytier/config.json</ElCheckbox>
|
||||
<ElButton
|
||||
@click="openConfigJsonDir"
|
||||
size="small"
|
||||
>
|
||||
打开config.json目录
|
||||
</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";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
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();
|
||||
};
|
||||
const openConfigJsonDir = async () => {
|
||||
const resourceDir = await getResourceDir();
|
||||
const easytierDir = await join(resourceDir, "easytier/");
|
||||
const isExists = await exists("easytier/", { baseDir: BaseDirectory.Resource });
|
||||
if (isExists) {
|
||||
await Command.create("explorer", [easytierDir]).execute();
|
||||
} else {
|
||||
ElMessage.error("config.json的目录不存在");
|
||||
}
|
||||
};
|
||||
mainStore.$subscribe(async (...a) => {
|
||||
// console.log("subscribe", a);
|
||||
await appWindow.emitTo("main", "config", { config: { ...mainStore.config }, createConfigInEasytier: mainStore.createConfigInEasytier });
|
||||
// if(mainStore.createConfigInEasytier) {
|
||||
// updateConfigJson();
|
||||
// }
|
||||
});
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,341 @@
|
||||
<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"
|
||||
:disabled="data.isPing"
|
||||
@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";
|
||||
import { isValidIP } from "~/utils";
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const data = reactive({
|
||||
activeTab: "netcard",
|
||||
winipBcPid: 0,
|
||||
pingNum: 1,
|
||||
winipBcStart: false,
|
||||
pingIp: "",
|
||||
isPing: false,
|
||||
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 awaitTime = (time: number) => {
|
||||
return new Promise(res => {
|
||||
setTimeout(res, time);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestPing = async () => {
|
||||
// const is = isValidIP(data.pingIp);
|
||||
if (data.pingIp) {
|
||||
data.pingLog = "";
|
||||
data.isPing = true;
|
||||
try {
|
||||
for (let i = 0; i < data.pingNum; i++) {
|
||||
const output = await Command.create("ping", ["-n", "1", data.pingIp], {
|
||||
encoding: "gb2312"
|
||||
}).execute();
|
||||
if (output.stdout) {
|
||||
data.pingLog += `${i + 1} - ${output.stdout.split("\n")[2]}`;
|
||||
}
|
||||
if (output.stderr) {
|
||||
data.pingLog += `${i + 1}error - ${output.stderr}`;
|
||||
}
|
||||
if(i != data.pingNum - 1) {
|
||||
await awaitTime(1000);
|
||||
}
|
||||
}
|
||||
data.pingLog += '完毕'
|
||||
} catch (err) {
|
||||
ElMessage.error("Ping发生错误");
|
||||
console.log(err);
|
||||
} finally {
|
||||
data.isPing = false;
|
||||
}
|
||||
}else {
|
||||
ElMessage.error("请输入正确的IP");
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
// netsh advfirewall firewall add rule name="EXE名称" dir=out action=allow program="EXE绝对路径"
|
||||
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(() => {
|
||||
data.pingIp = mainStore.config.ipv4;
|
||||
initStartWinIpBroadcast();
|
||||
});
|
||||
</script>
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createPersistedState } from "pinia-plugin-persistedstate";
|
||||
import { defineNuxtPlugin } from "nuxt/app";
|
||||
export default defineNuxtPlugin((nuxtApp: any) => {
|
||||
nuxtApp.$pinia.use(
|
||||
createPersistedState({
|
||||
storage: localStorage,
|
||||
key: id => `__glj_persisted_${id}`
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -2,5 +2,4 @@
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
/easytier-windows-x86_64-v2.0.3.zip
|
||||
/easytier-windows-x86_64/
|
||||
/easytier/logs
|
||||
@@ -1,35 +1,52 @@
|
||||
[package]
|
||||
name = "easytier-game"
|
||||
version = "1.0.3"
|
||||
version = "1.1.8"
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasytierGame"
|
||||
description = "A simple network initiator based on Easytier"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
authors = ["leizi97"]
|
||||
license = "LICENSE"
|
||||
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.1", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
# prost-build = "0.13.2"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.0.2", features = [
|
||||
tauri = { version = "2.1.1", features = [
|
||||
"tray-icon",
|
||||
"image-png",
|
||||
"image-ico",
|
||||
] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
tauri-plugin-log = "2.0.2"
|
||||
tauri-plugin-shell = "2.0.2"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
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.2"
|
||||
# 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]
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-autostart = "2.0.1"
|
||||
tauri-plugin-single-instance = "2.0.1"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -3,13 +3,91 @@
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
"main",
|
||||
"log",
|
||||
"member",
|
||||
"cidr",
|
||||
"advance",
|
||||
"tool"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-hide",
|
||||
"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",
|
||||
"fs:allow-resource-write",
|
||||
"fs:allow-resource-write-recursive",
|
||||
"clipboard-manager:allow-clear",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
这里存放每次打包需要用到的最新版本的easytier 和 相关工具
|
||||
@@ -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"
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// 如果您的配置过于复杂,可以参考 使用配置文件进行联机的办法,以下是基础的界面配置,用于快速分享联机
|
||||
"protocol": [
|
||||
"tcp",
|
||||
"udp"
|
||||
], // 协议类型
|
||||
"serverUrl": "public.easytier.top:11010", // easytier服务地址
|
||||
"networkName": "", // 网络名
|
||||
"networkPassword": "", // 网络密码
|
||||
"hostname": "configTest", // 主机名
|
||||
"ipv4": "10.126.126.1", // IP地址(选填,下面有dhcp)
|
||||
"disableIpv6": false, // 禁用ipv6
|
||||
"disbleListenner": true, // 禁用端口监听
|
||||
"disableEncryption": false, // 禁用加密
|
||||
"noTun": false, // 禁用tun
|
||||
"latencyfirst": false, // 延迟优先
|
||||
"disableUdpHolePunching": false, // 禁用udp打洞
|
||||
"disbleP2p": false, // 禁用p2p, 强制中转
|
||||
"dhcp": false, // 动态分配IP
|
||||
"devName": false, // 是否启用自定义网卡名
|
||||
"devNameValue": "" // 网卡名(启用devName之后才生效)
|
||||
}
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 68 KiB |
@@ -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 serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -9,9 +14,15 @@ use std::sync::{
|
||||
mpsc, Arc,
|
||||
};
|
||||
use std::{path, thread};
|
||||
use sysinfo::System;
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
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的结构体
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -47,29 +58,9 @@ pub async fn fetch_releases() -> Result<Vec<Release>, Error> {
|
||||
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")]
|
||||
fn get_core_version() -> String {
|
||||
match Command::new("easytier-core.exe")
|
||||
match Command::new("easytier/easytier-core.exe")
|
||||
.arg("--version")
|
||||
.creation_flags(0x08000000)
|
||||
.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")]
|
||||
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)
|
||||
.await
|
||||
.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 mut file = match File::create(&path) {
|
||||
@@ -137,7 +197,14 @@ fn unzip(fname: &path::Path) {
|
||||
// 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());
|
||||
let mut outfile = fs::File::create(&out_file_path).unwrap();
|
||||
std::io::copy(&mut file, &mut outfile).unwrap();
|
||||
@@ -150,18 +217,16 @@ fn run_command(
|
||||
args: Vec<String>,
|
||||
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();
|
||||
stop_signal.store(false, Ordering::Relaxed);
|
||||
let app_handle1 = 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();
|
||||
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)
|
||||
.creation_flags(0x08000000)
|
||||
.stdout(Stdio::piped())
|
||||
@@ -189,13 +254,13 @@ fn run_command(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop_signal1.store(true, Ordering::Relaxed);
|
||||
println!("end");
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(line) = rx.recv() {
|
||||
if stop_signal.load(Ordering::Relaxed) {
|
||||
if stop_signal2.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
app_handle2
|
||||
@@ -206,8 +271,7 @@ fn run_command(
|
||||
}
|
||||
|
||||
#[tauri::command(rename_all = "snake_case")]
|
||||
fn stop_command(child_id: u32, stop_signal: tauri::State<Arc<AtomicBool>>) {
|
||||
println!("stop command");
|
||||
fn stop_command(child_id: u32) {
|
||||
if child_id != 0 {
|
||||
let output = Command::new("taskkill")
|
||||
.arg("/F")
|
||||
@@ -223,8 +287,9 @@ fn stop_command(child_id: u32, stop_signal: tauri::State<Arc<AtomicBool>>) {
|
||||
} else {
|
||||
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")]
|
||||
@@ -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)]
|
||||
pub fn run() {
|
||||
let stop_signal = Arc::new(AtomicBool::new(false)); // 创建一个原子布尔值,用于控制命令的停止
|
||||
let stop_signal_clone = Arc::clone(&stop_signal); // 创建一个原子布尔值的克隆,用于传递给命令
|
||||
let context = tauri::generate_context!();
|
||||
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())
|
||||
.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| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
@@ -316,13 +551,17 @@ pub fn run() {
|
||||
})
|
||||
.manage(stop_signal_clone)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
my_commit_output,
|
||||
force_close,
|
||||
run_command,
|
||||
stop_command,
|
||||
get_core_version,
|
||||
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)
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "easytier-game",
|
||||
"version": "1.0.3",
|
||||
"version": "1.1.8",
|
||||
"identifier": "com.tauri.easytier-game",
|
||||
|
||||
"build": {
|
||||
@@ -13,14 +13,18 @@
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "easytier-game",
|
||||
"width": 331,
|
||||
"height": 285,
|
||||
"resizable": false,
|
||||
"title": "easytier-game 1.1.8",
|
||||
"minWidth": 340,
|
||||
"width": 340,
|
||||
"height": 305,
|
||||
"minHeight": 305,
|
||||
"maxHeight": 305,
|
||||
"maxWidth": 360,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": true,
|
||||
"maximizable": false,
|
||||
"center": true,
|
||||
"maximizable": false,
|
||||
"transparent": false
|
||||
}
|
||||
],
|
||||
@@ -28,10 +32,26 @@
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"plugins": {},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"targets": "all",
|
||||
"resources": [
|
||||
"easytier/config/",
|
||||
"easytier/icons/",
|
||||
"easytier/config_template.json",
|
||||
"easytier/tool/WinIPBroadcast.exe",
|
||||
"easytier/easytier-cli.exe",
|
||||
"easytier/easytier-core.exe",
|
||||
"easytier/Packet.dll",
|
||||
"easytier/wintun.dll"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "embedBootstrapper"
|
||||
}
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,26 +9,44 @@ export default defineStore("main", {
|
||||
networkPassword: "",
|
||||
hostname: "",
|
||||
ipv4: "",
|
||||
proxyNetworks: "", // 子网代理
|
||||
autoStart: false, // 是否自动启动
|
||||
coonectAfterStart: false, //软件打开后,是否自动连接
|
||||
disableIpv6: false, // 是否禁用IPv6
|
||||
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
|
||||
saveErrorLog: true, // 是否保存错误日志
|
||||
logLevel: "error", //日志等级
|
||||
devName: false, //自定义网卡名
|
||||
devNameValue: "" //自定义网卡名
|
||||
},
|
||||
cidrEnable: false,
|
||||
basePeers: ["public.easytier.top:11010"],
|
||||
theme: false, //主题 false light true dark
|
||||
configStartEnable: false, //使用配置文件启动
|
||||
configPath: "", //配置文件路径
|
||||
winIpBcAutoStart: true,
|
||||
createConfigInEasytier: false, //在easytier目录生成config.json文件吗
|
||||
|
||||
winipBcPid: 0,
|
||||
winipBcStart: false,
|
||||
|
||||
};
|
||||
},
|
||||
persist: {
|
||||
paths: [
|
||||
"basePeers",
|
||||
"config.serverUrl",
|
||||
"config.networkName",
|
||||
"config.protocol",
|
||||
"config.networkPassword",
|
||||
"config.disbleP2p",
|
||||
"config.disableIpv6",
|
||||
"config.disbleListenner",
|
||||
"config.hostname",
|
||||
"config.dhcp",
|
||||
],
|
||||
},
|
||||
// 除了这些,其他都要存下来
|
||||
omit: [
|
||||
"winipBcPid",
|
||||
"winipBcStart"
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"components/**/*.ts",
|
||||
"components/**/*.js",
|
||||
"components/**/*.vue",
|
||||
"composables/**/*.ts",
|
||||
"app.vue",
|
||||
"pages/**/*.ts",
|
||||
"pages/**/*.js",
|
||||
"pages/**/*.vue",
|
||||
@@ -27,16 +29,16 @@
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
// "types": ["element-plus/global"]
|
||||
"resolveJsonModule": true,
|
||||
// 从 Vue 3.4 开始,Vue 不再隐式注册全局 JSX 命名空间。要指示 TypeScript 使用 Vue 的 JSX 类型定义,请确保在你的 tsconfig.json 中包含以下内容
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"types": ["@pinia/nuxt", "vite/client", "element-plus/global"],
|
||||
"types": ["@pinia/nuxt", "vite/client", "element-plus/global", "pinia-plugin-persistedstate"],
|
||||
"allowJs": true, // 允许编译器编译JS,JSX文件
|
||||
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
|
||||
"checkJs": false, // 允许在JS文件中报错,通常与allowJS一起使用
|
||||
"esModuleInterop": true, // 允许export=导出,由import from 导入
|
||||
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_CONFIG_PATH: string;
|
||||
readonly VITE_CONFIG_FILE_NAME: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
|
||||
import { ElMessage } from "element-plus";
|
||||
import Clipboard from "vue-clipboard3";
|
||||
|
||||
export const ENV = import.meta.env;
|
||||
|
||||
export const clipBoardCopy = async (text: string) => {
|
||||
const { toClipboard } = Clipboard();
|
||||
try {
|
||||
await toClipboard(text);
|
||||
ElMessage.success("复制成功");
|
||||
} catch (e) {
|
||||
ElMessage.error("复制失败");
|
||||
}
|
||||
};
|
||||
|
||||
//防抖
|
||||
export const bounce = (time = 3000) => {
|
||||
@@ -77,7 +64,45 @@ export const ATJ = (promise: Promise<any>, errorExt: string | undefined = undefi
|
||||
});
|
||||
};
|
||||
|
||||
export const openBrowser = (key: string) => {
|
||||
// logger.info(`${import.meta.env.VITE_BROWSER_JOB_URL}${key}`)
|
||||
// _launcherApi.openBrowser(`${import.meta.env.VITE_BROWSER_JOB_URL}${key}`);
|
||||
export const parsePeerInfo = (content: string) => {
|
||||
// 将表格字符串分割成行
|
||||
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;
|
||||
};
|
||||
|
||||
export function isValidIP(ip: string) {
|
||||
const ipv4Pattern =
|
||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
const ipv6Pattern = /^(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$/;
|
||||
return ipv4Pattern.test(ip) || ipv6Pattern.test(ip);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||