Perf/front page (#316)

* 🐳 chore: dependencies

* 🐞 fix: minor style issues

fixed background white patches in dark mode
fixed the line height of the status label, which resulted in a bloated appearance

* 🌈 style: lint

*  feat: about
This commit is contained in:
m1m1sha 2024-09-11 09:13:00 +08:00 committed by GitHub
parent 1609c97574
commit 4342be29d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1974 additions and 1526 deletions

View File

@ -11,7 +11,7 @@
}
],
"settings": {
"eslint.experimental.useFlatConfig": true,
"eslint.useFlatConfig": true,
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {

View File

@ -34,7 +34,6 @@ rustup target add aarch64-linux-android
install java 20
```
Java version depend on gradle version specified in (easytier-gui\src-tauri\gen\android\build.gradle.kts)
See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html) for detail .
@ -43,4 +42,4 @@ See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/comp
pnpm install
pnpm tauri android init
pnpm tauri android build
```
```

View File

@ -75,3 +75,12 @@ dhcp_experimental_warning: 实验性警告使用DHCP时如果组网环境中
tray:
show: 显示 / 隐藏
exit: 退出
about:
title: 关于
version: 版本
author: 作者
homepage: 主页
license: 许可证
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
check_update: 检查更新

View File

@ -75,3 +75,12 @@ dhcp_experimental_warning: Experimental warning! if there is an IP conflict in t
tray:
show: Show / Hide
exit: Exit
about:
title: About
version: Version
author: Author
homepage: Homepage
license: License
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
check_update: Check Update

View File

@ -12,50 +12,49 @@
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
},
"dependencies": {
"@primevue/themes": "^4.0.4",
"@tauri-apps/plugin-autostart": "2.0.0-rc.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
"@tauri-apps/plugin-os": "2.0.0-rc.0",
"@tauri-apps/plugin-process": "2.0.0-rc.0",
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
"@primevue/themes": "^4.0.5",
"@tauri-apps/plugin-autostart": "2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
"@tauri-apps/plugin-os": "2.0.0-rc.1",
"@tauri-apps/plugin-process": "2.0.0-rc.1",
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
"aura": "link:@primevue/themes/aura",
"pinia": "^2.2.1",
"pinia": "^2.2.2",
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primevue": "^4.0.4",
"primevue": "^4.0.5",
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice",
"vue": "^3.4.38",
"vue-i18n": "^9.13.1",
"vue": "^3.5.3",
"vue-i18n": "^10.0.0",
"vue-router": "^4.4.3"
},
"devDependencies": {
"@antfu/eslint-config": "^2.25.1",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@primevue/auto-import-resolver": "^4.0.4",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@antfu/eslint-config": "^3.5.0",
"@intlify/unplugin-vue-i18n": "^5.0.0",
"@primevue/auto-import-resolver": "^4.0.5",
"@tauri-apps/api": "2.0.0-rc.0",
"@tauri-apps/cli": "2.0.0-rc.3",
"@types/node": "^20.14.15",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-vue": "^5.1.2",
"@vue-macros/volar": "^0.19.1",
"@types/node": "^22.5.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.1.3",
"@vue-macros/volar": "^0.29.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint": "^9.10.0",
"eslint-plugin-format": "^0.1.2",
"internal-ip": "^8.0.0",
"postcss": "^8.4.41",
"postcss": "^8.4.45",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.4",
"unplugin-auto-import": "^0.17.8",
"typescript": "^5.6.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"unplugin-vue-macros": "^2.11.5",
"unplugin-vue-macros": "^2.11.11",
"unplugin-vue-markdown": "^0.26.2",
"unplugin-vue-router": "^0.8.8",
"uuid": "^9.0.1",
"vite": "^5.4.1",
"vite-plugin-vue-devtools": "^7.3.8",
"unplugin-vue-router": "^0.10.8",
"uuid": "^10.0.0",
"vite": "^5.4.3",
"vite-plugin-vue-devtools": "^7.4.4",
"vite-plugin-vue-layouts": "^0.11.0",
"vue-i18n": "^9.13.1",
"vue-tsc": "^2.0.29"
"vue-i18n": "^10.0.0",
"vue-tsc": "^2.1.6"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -171,6 +171,11 @@ static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
once_cell::sync::Lazy::new(Default::default);
#[tauri::command]
fn easytier_version() -> Result<String, String> {
Ok(easytier::VERSION.to_string())
}
#[tauri::command]
fn is_autostart() -> Result<bool, String> {
let args: Vec<String> = std::env::args().collect();
@ -365,7 +370,8 @@ pub fn run() {
get_os_hostname,
set_logging_level,
set_tun_fd,
is_autostart
is_autostart,
easytier_version
])
.on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))]

View File

@ -24,6 +24,7 @@ declare global {
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
const getOsHostname: typeof import('./composables/network')['getOsHostname']
const h: typeof import('vue')['h']
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
@ -44,8 +45,8 @@ declare global {
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
@ -90,8 +91,8 @@ declare global {
const useI18n: typeof import('vue-i18n')['useI18n']
const useLink: typeof import('vue-router/auto')['useLink']
const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
const useRoute: typeof import('vue-router/auto')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTray: typeof import('./composables/tray')['useTray']
const watch: typeof import('vue')['watch']
@ -121,13 +122,13 @@ declare module 'vue' {
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly definePage: UnwrapRef<typeof import('unplugin-vue-router/runtime')['definePage']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
@ -146,8 +147,8 @@ declare module 'vue' {
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
@ -191,8 +192,8 @@ declare module 'vue' {
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { getEasytierVersion } from '~/composables/network'
const { t } = useI18n()
const etVersion = ref('')
onMounted(async () => {
etVersion.value = await getEasytierVersion()
})
</script>
<template>
<Card>
<template #title>
Easytier - {{ t('about.version') }}: {{ etVersion }}
</template>
<template #content>
<p class="mb-1">
{{ t('about.description') }}
</p>
</template>
</Card>
</template>
<style scoped lang="postcss">
</style>

View File

@ -1,11 +1,10 @@
<script setup lang="ts">
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { getOsHostname } from '~/composables/network'
import { NetworkingMethod } from '~/types/network'
const { t } = useI18n()
import { ping } from 'tauri-plugin-vpnservice-api'
import { getOsHostname } from '~/composables/network'
import { NetworkingMethod } from '~/types/network'
const props = defineProps<{
configInvalid?: boolean
@ -14,6 +13,8 @@ const props = defineProps<{
defineEmits(['runNetwork'])
const { t } = useI18n()
const networking_methods = ref([
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
{ value: NetworkingMethod.Manual, label: () => t('manual') },
@ -32,24 +33,26 @@ const curNetwork = computed(() => {
return networkStore.curNetwork
})
const protos:{ [proto: string] : number; } = {'tcp': 11010, 'udp': 11010, 'wg':11011, 'ws': 11011, 'wss': 11012}
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
function searchUrlSuggestions(e: { query: string }): string[] {
const query = e.query
let ret = []
const ret = []
// if query match "^\w+:.*", then no proto prefix
if (query.match(/^\w+:.*/)) {
// if query is a valid url, then add to suggestions
try {
new URL(query)
ret.push(query)
} catch (e) {}
} else {
for (let proto in protos) {
let item = proto + '://' + query
}
catch (e) {}
}
else {
for (const proto in protos) {
let item = `${proto}://${query}`
// if query match ":\d+$", then no port suffix
if (!query.match(/:\d+$/)) {
item += ':' + protos[proto]
item += `:${protos[proto]}`
}
ret.push(item)
}
@ -58,45 +61,45 @@ function searchUrlSuggestions(e: { query: string }): string[] {
return ret
}
const publicServerSuggestions = ref([''])
const searchPresetPublicServers = (e: { query: string }) => {
const presetPublicServers = [
'tcp://easytier.public.kkrainbow.top:11010',
]
function searchPresetPublicServers(e: { query: string }) {
const presetPublicServers = [
'tcp://easytier.public.kkrainbow.top:11010',
]
let query = e.query
// if query is sub string of presetPublicServers, add to suggestions
let ret = presetPublicServers.filter((item) => item.includes(query))
// add additional suggestions
if (query.length > 0) {
ret = ret.concat(searchUrlSuggestions(e))
}
const query = e.query
// if query is sub string of presetPublicServers, add to suggestions
let ret = presetPublicServers.filter(item => item.includes(query))
// add additional suggestions
if (query.length > 0) {
ret = ret.concat(searchUrlSuggestions(e))
}
publicServerSuggestions.value = ret
publicServerSuggestions.value = ret
}
const peerSuggestions = ref([''])
const searchPeerSuggestions = (e: { query: string }) => {
function searchPeerSuggestions(e: { query: string }) {
peerSuggestions.value = searchUrlSuggestions(e)
}
const listenerSuggestions = ref([''])
const searchListenerSuggestiong = (e: { query: string }) => {
let ret = []
function searchListenerSuggestiong(e: { query: string }) {
const ret = []
for (let proto in protos) {
let item = proto + '://0.0.0.0:';
for (const proto in protos) {
let item = `${proto}://0.0.0.0:`
// if query is a number, use it as port
if (e.query.match(/^\d+$/)) {
item += e.query
} else {
}
else {
item += protos[proto]
}
if (item.includes(e.query)) {
ret.push(item)
}
@ -112,7 +115,7 @@ const searchListenerSuggestiong = (e: { query: string }) => {
function validateHostname() {
if (curNetwork.value.hostname) {
// eslint no-useless-escape
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-zA-Z0-9\-]*/g, '')
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-z0-9\-]*/gi, '')
if (name.length > 32)
name = name.substring(0, 32)
@ -132,7 +135,7 @@ onMounted(async () => {
<template>
<div class="flex flex-column h-full">
<div class="flex flex-column">
<div class="w-10/12 self-center ">
<div class="w-10/12 self-center mb-3">
<Message severity="warn">
{{ t('dhcp_experimental_warning') }}
</Message>
@ -151,8 +154,10 @@ onMounted(async () => {
</label>
</div>
<InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help" />
<InputText
id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help"
/>
<InputGroupAddon>
<span>/24</span>
</InputGroupAddon>
@ -167,23 +172,29 @@ onMounted(async () => {
</div>
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label>
<InputText id="network_secret" v-model="curNetwork.network_secret"
aria-describedby=" network_secret-help" />
<InputText
id="network_secret" v-model="curNetwork.network_secret"
aria-describedby=" network_secret-help"
/>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="nm">{{ t('networking_method') }}</label>
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value"></SelectButton>
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value" />
<div class="items-center flex flex-row p-fluid gap-x-1">
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
<AutoComplete
v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions"/>
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions"
/>
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" :suggestions="publicServerSuggestions"
:virtualScrollerOptions="{ itemSize: 38 }" class="grow" dropdown @complete="searchPresetPublicServers" :completeOnFocus="true"
v-model="curNetwork.public_server_url"/>
<AutoComplete
v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" v-model="curNetwork.public_server_url"
:suggestions="publicServerSuggestions" :virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
@complete="searchPresetPublicServers"
/>
</div>
</div>
</div>
@ -197,64 +208,80 @@ onMounted(async () => {
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="hostname">{{ t('hostname') }}</label>
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
<InputText
id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname"
/>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="username">{{ t('proxy_cidrs') }}</label>
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
<Chips
id="chips" v-model="curNetwork.proxy_cidrs"
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full"
/>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-column gap-2 grow">
<label for="username">VPN Portal</label>
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48"/>
<div class="items-center flex flex-row gap-x-4" v-if="curNetwork.enable_vpn_portal">
<div class="min-w-64">
<InputGroup>
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')" />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
<ToggleButton
v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48"
/>
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
<div class="min-w-64">
<InputGroup>
<InputText
v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')"
/>
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
:format="false" :min="0" :max="65535" class="w-8" fluid/>
</div>
<InputNumber
v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
:format="false" :min="0" :max="65535" class="w-8" fluid
/>
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="listener_urls">{{ t('listener_urls') }}</label>
<AutoComplete id="listener_urls" :suggestions="listenerSuggestions"
class="w-full" dropdown @complete="searchListenerSuggestiong" :completeOnFocus="true"
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
v-model="curNetwork.listener_urls" multiple/>
<AutoComplete
id="listener_urls" v-model="curNetwork.listener_urls"
:suggestions="listenerSuggestions" class="w-full" dropdown :complete-on-focus="true"
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
multiple @complete="searchListenerSuggestiong"
/>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="rpc_port">{{ t('rpc_port') }}</label>
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
:format="false" :min="0" :max="65535" />
<InputNumber
id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
:format="false" :min="0" :max="65535"
/>
</div>
</div>
</div>
</Panel>
<div class="flex pt-4 justify-content-center">
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)" />
<Button
:label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)"
/>
</div>
</div>
</div>

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import type { NodeInfo } from '~/types/network'
const { t } = useI18n()
const props = defineProps<{
instanceId?: string
}>()
const { t } = useI18n()
const networkStore = useNetworkStore()
const curNetwork = computed(() => {

View File

@ -1,183 +1,184 @@
import { addPluginListener } from '@tauri-apps/api/core';
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api';
import { Route } from '~/types/network';
import { addPluginListener } from '@tauri-apps/api/core'
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
import type { Route } from '~/types/network'
const networkStore = useNetworkStore()
interface vpnStatus {
running: boolean
ipv4Addr: string | null | undefined
ipv4Cidr: number | null | undefined
routes: string[]
running: boolean
ipv4Addr: string | null | undefined
ipv4Cidr: number | null | undefined
routes: string[]
}
var curVpnStatus: vpnStatus = {
running: false,
ipv4Addr: undefined,
ipv4Cidr: undefined,
routes: []
const curVpnStatus: vpnStatus = {
running: false,
ipv4Addr: undefined,
ipv4Cidr: undefined,
routes: [],
}
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
let start_time = Date.now()
while (curVpnStatus.running !== target_status) {
if (Date.now() - start_time > timeout_sec * 1000) {
throw new Error('wait vpn status timeout')
}
await new Promise(r => setTimeout(r, 50))
const start_time = Date.now()
while (curVpnStatus.running !== target_status) {
if (Date.now() - start_time > timeout_sec * 1000) {
throw new Error('wait vpn status timeout')
}
await new Promise(r => setTimeout(r, 50))
}
}
async function doStopVpn() {
if (!curVpnStatus.running) {
return
}
console.log('stop vpn')
let stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret)))
await waitVpnStatus(false, 3)
if (!curVpnStatus.running) {
return
}
console.log('stop vpn')
const stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret)))
await waitVpnStatus(false, 3)
curVpnStatus.ipv4Addr = undefined
curVpnStatus.routes = []
curVpnStatus.ipv4Addr = undefined
curVpnStatus.routes = []
}
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
if (curVpnStatus.running) {
return
}
if (curVpnStatus.running) {
return
}
console.log('start vpn')
let start_ret = await start_vpn({
"ipv4Addr": ipv4Addr + '/' + cidr,
"routes": routes,
"disallowedApplications": ["com.kkrainbow.easytier"],
"mtu": 1300,
});
if (start_ret?.errorMsg?.length) {
throw new Error(start_ret.errorMsg)
}
await waitVpnStatus(true, 3)
console.log('start vpn')
const start_ret = await start_vpn({
ipv4Addr: `${ipv4Addr}/${cidr}`,
routes,
disallowedApplications: ['com.kkrainbow.easytier'],
mtu: 1300,
})
if (start_ret?.errorMsg?.length) {
throw new Error(start_ret.errorMsg)
}
await waitVpnStatus(true, 3)
curVpnStatus.ipv4Addr = ipv4Addr
curVpnStatus.routes = routes
curVpnStatus.ipv4Addr = ipv4Addr
curVpnStatus.routes = routes
}
async function onVpnServiceStart(payload: any) {
console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true
if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
}
console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true
if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
}
}
async function onVpnServiceStop(payload: any) {
console.log('vpn service stop', JSON.stringify(payload))
curVpnStatus.running = false
console.log('vpn service stop', JSON.stringify(payload))
curVpnStatus.running = false
}
async function registerVpnServiceListener() {
console.log('register vpn service listener')
await addPluginListener(
'vpnservice',
'vpn_service_start',
onVpnServiceStart
)
console.log('register vpn service listener')
await addPluginListener(
'vpnservice',
'vpn_service_start',
onVpnServiceStart,
)
await addPluginListener(
'vpnservice',
'vpn_service_stop',
onVpnServiceStop
)
await addPluginListener(
'vpnservice',
'vpn_service_stop',
onVpnServiceStop,
)
}
function getRoutesForVpn(routes: Route[]): string[] {
if (!routes) {
return []
}
if (!routes) {
return []
}
let ret = []
for (let r of routes) {
for (let cidr of r.proxy_cidrs) {
if (cidr.indexOf('/') === -1) {
cidr += '/32'
}
ret.push(cidr)
}
const ret = []
for (const r of routes) {
for (let cidr of r.proxy_cidrs) {
if (!cidr.includes('/')) {
cidr += '/32'
}
ret.push(cidr)
}
}
// sort and dedup
return Array.from(new Set(ret)).sort()
// sort and dedup
return Array.from(new Set(ret)).sort()
}
async function onNetworkInstanceChange() {
let insts = networkStore.networkInstanceIds
if (!insts) {
await doStopVpn()
return
const insts = networkStore.networkInstanceIds
if (!insts) {
await doStopVpn()
return
}
const curNetworkInfo = networkStore.networkInfos[insts[0]]
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
await doStopVpn()
return
}
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
if (!virtual_ip || !virtual_ip.length) {
await doStopVpn()
return
}
const routes = getRoutesForVpn(curNetworkInfo?.routes)
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
if (ipChanged || routesChanged) {
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
try {
await doStopVpn()
}
catch (e) {
console.error(e)
}
const curNetworkInfo = networkStore.networkInfos[insts[0]]
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
await doStopVpn()
return
try {
await doStartVpn(virtual_ip, 24, routes)
}
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
if (!virtual_ip || !virtual_ip.length) {
await doStopVpn()
return
}
const routes = getRoutesForVpn(curNetworkInfo?.routes)
var ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
var routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
if (ipChanged || routesChanged) {
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
try {
await doStopVpn()
} catch (e) {
console.error(e)
}
try {
await doStartVpn(virtual_ip, 24, routes)
} catch (e) {
console.error("start vpn failed, clear all network insts.", e)
networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds)
}
return
catch (e) {
console.error('start vpn failed, clear all network insts.', e)
networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds)
}
}
}
async function watchNetworkInstance() {
var subscribe_running = false
networkStore.$subscribe(async () => {
if (subscribe_running) {
return
}
subscribe_running = true
try {
await onNetworkInstanceChange()
} catch (_) {
}
subscribe_running = false
})
let subscribe_running = false
networkStore.$subscribe(async () => {
if (subscribe_running) {
return
}
subscribe_running = true
try {
await onNetworkInstanceChange()
}
catch (_) {
}
subscribe_running = false
})
}
export async function initMobileVpnService() {
await registerVpnServiceListener()
await watchNetworkInstance()
await registerVpnServiceListener()
await watchNetworkInstance()
}
export async function prepareVpnService() {
console.log('prepare vpn')
let prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret)))
if (prepare_ret?.errorMsg?.length) {
throw new Error(prepare_ret.errorMsg)
}
console.log('prepare vpn')
const prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret)))
if (prepare_ret?.errorMsg?.length) {
throw new Error(prepare_ret.errorMsg)
}
}

View File

@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core"
import { invoke } from '@tauri-apps/api/core'
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network'
@ -33,3 +33,7 @@ export async function setLoggingLevel(level: string) {
export async function setTunFd(instanceId: string, fd: number) {
return await invoke('set_tun_fd', { instanceId, fd })
}
export async function getEasytierVersion() {
return await invoke<string>('easytier_version')
}

View File

@ -1,6 +1,6 @@
import { getCurrentWindow } from '@tauri-apps/api/window'
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'
const DEFAULT_TRAY_NAME = 'main'
@ -8,14 +8,15 @@ const DEFAULT_TRAY_NAME = 'main'
async function toggleVisibility() {
if (await getCurrentWindow().isVisible()) {
await getCurrentWindow().hide()
} else {
}
else {
await getCurrentWindow().show()
await getCurrentWindow().setFocus()
}
}
export async function useTray(init: boolean = false) {
let tray;
let tray
try {
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
if (!tray) {
@ -29,17 +30,18 @@ export async function useTray(init: boolean = false) {
}),
action: async () => {
toggleVisibility()
}
},
})
}
} catch (error) {
}
catch (error) {
console.warn('Error while creating tray icon:', error)
return null
}
if (init) {
tray.setTooltip(`EasyTier\n${pkg.version}`)
tray.setMenuOnLeftClick(false);
tray.setMenuOnLeftClick(false)
tray.setMenu(await Menu.new({
id: 'main',
items: await generateMenuItem(),
@ -59,7 +61,7 @@ export async function generateMenuItem() {
export async function MenuItemExit(text: string) {
return await PredefinedMenuItem.new({
text: text,
text,
item: 'Quit',
})
}
@ -69,14 +71,15 @@ export async function MenuItemShow(text: string) {
id: 'show',
text,
action: async () => {
await toggleVisibility();
await toggleVisibility()
},
})
}
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
const tray = await useTray()
if (!tray) return
if (!tray)
return
const menu = await Menu.new({
id: 'main',
items: items || await generateMenuItem(),
@ -86,15 +89,17 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
export async function setTrayRunState(isRunning: boolean = false) {
const tray = await useTray()
if (!tray) return
if (!tray)
return
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
}
export async function setTrayTooltip(tooltip: string) {
if (tooltip) {
const tray = await useTray()
if (!tray) return
if (!tray)
return
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
}
}
}

View File

@ -1,16 +1,16 @@
import { setupLayouts } from 'virtual:generated-layouts'
import { createRouter, createWebHistory } from 'vue-router/auto'
import Aura from '@primevue/themes/aura'
import PrimeVue from 'primevue/config'
import ToastService from 'primevue/toastservice'
import App from '~/App.vue'
import { createRouter, createWebHistory } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'
import App from '~/App.vue'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
import '~/styles.css'
import Aura from '@primevue/themes/aura'
import 'primeicons/primeicons.css'
import 'primeflex/primeflex.css'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch'
if (import.meta.env.PROD) {
document.addEventListener('keydown', (event) => {
@ -18,8 +18,9 @@ if (import.meta.env.PROD) {
event.key === 'F5'
|| (event.ctrlKey && event.key === 'r')
|| (event.metaKey && event.key === 'r')
)
) {
event.preventDefault()
}
})
document.addEventListener('contextmenu', (event) => {
@ -35,7 +36,7 @@ async function main() {
const router = createRouter({
history: createWebHistory(),
extendRoutes: routes => setupLayouts(routes),
routes,
})
app.use(router)
@ -45,11 +46,12 @@ async function main() {
theme: {
preset: Aura,
options: {
prefix: 'p',
darkModeSelector: 'system',
cssLayer: false
}
}})
prefix: 'p',
darkModeSelector: 'system',
cssLayer: false,
},
},
})
app.use(ToastService)
app.mount('#app')
}

View File

@ -1,17 +1,17 @@
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
try {
target_enable ? await enable() : await disable()
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
return isEnabled()
}
catch (e) {
console.error(e)
return false
}
try {
target_enable ? await enable() : await disable()
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
return isEnabled()
}
catch (e) {
console.error(e)
return false
}
}
export function getAutoLaunchStatusAsync(): boolean {
return localStorage.getItem('auto_launch') === 'true'
return localStorage.getItem('auto_launch') === 'true'
}

View File

@ -1,5 +1,5 @@
import type { Locale } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
// Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import

View File

@ -1,24 +1,25 @@
<script setup lang="ts">
import { useToast } from 'primevue/usetoast'
import { exit } from '@tauri-apps/plugin-process'
import TieredMenu from 'primevue/tieredmenu'
import { open } from '@tauri-apps/plugin-shell'
import { appLogDir } from '@tauri-apps/api/path'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { type } from '@tauri-apps/plugin-os'
import { exit } from '@tauri-apps/plugin-process'
import { open } from '@tauri-apps/plugin-shell'
import TieredMenu from 'primevue/tieredmenu'
import { useToast } from 'primevue/usetoast'
import Config from '~/components/Config.vue'
import Status from '~/components/Status.vue'
import { type NetworkConfig, NetworkingMethod } from '~/types/network'
import { loadLanguageAsync } from '~/modules/i18n'
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
import Status from '~/components/Status.vue'
import { isAutostart, setLoggingLevel } from '~/composables/network'
import { useTray } from '~/composables/tray'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
import { loadLanguageAsync } from '~/modules/i18n'
import { type NetworkConfig, NetworkingMethod } from '~/types/network'
const { t, locale } = useI18n()
const visible = ref(false)
const aboutVisible = ref(false)
const tomlConfig = ref('')
useTray(true)
@ -85,7 +86,8 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
if (type() === 'android') {
await prepareVpnService()
networkStore.clearNetworkInstances()
} else {
}
else {
networkStore.removeNetworkInstance(cfg.instance_id)
}
@ -146,7 +148,7 @@ const setting_menu_items = ref([
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
await setTrayMenu([
await MenuItemExit(t('tray.exit')),
await MenuItemShow(t('tray.show'))
await MenuItemShow(t('tray.show')),
])
},
},
@ -193,6 +195,13 @@ const setting_menu_items = ref([
return items
})(),
},
{
label: () => t('about.title'),
icon: 'pi pi-at',
command: async () => {
aboutVisible.value = true
},
},
{
label: () => t('exit'),
icon: 'pi pi-power-off',
@ -249,6 +258,10 @@ function isRunning(id: string) {
</div>
</Dialog>
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
<About />
</Dialog>
<div>
<Toolbar>
<template #start>
@ -259,15 +272,19 @@ function isRunning(id: string) {
<template #center>
<div class="min-w-40">
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
:placeholder="t('select_network')" class="w-full">
<Dropdown
v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
:placeholder="t('select_network')" class="w-full"
>
<template #value="slotProps">
<div class="flex items-start content-center">
<div class="mr-3 flex-column">
<span>{{ slotProps.value.network_name }}</span>
</div>
<Tag class="my-auto" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
<Tag
class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')"
/>
</div>
</template>
<template #option="slotProps">
@ -276,17 +293,23 @@ function isRunning(id: string) {
<div class="mr-3">
{{ t('network_name') }}: {{ slotProps.option.network_name }}
</div>
<Tag class="my-auto" :severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
<Tag
class="my-auto leading-3"
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')"
/>
</div>
<div v-if="slotProps.option.networking_method !== NetworkingMethod.Standalone"
class="max-w-full overflow-hidden text-ellipsis">
<div
v-if="slotProps.option.networking_method !== NetworkingMethod.Standalone"
class="max-w-full overflow-hidden text-ellipsis"
>
{{ slotProps.option.networking_method === NetworkingMethod.Manual
? slotProps.option.peer_urls.join(', ')
: slotProps.option.public_server_url }}
</div>
<div
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')">
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')"
>
{{ networkStore.instances[slotProps.option.instance_id].detail
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
</div>
@ -297,8 +320,10 @@ function isRunning(id: string) {
</template>
<template #end>
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
<Button
icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
aria-controls="overlay_setting_menu" @click="toggle_setting_menu"
/>
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
</template>
</Toolbar>
@ -316,16 +341,20 @@ function isRunning(id: string) {
</StepList>
<StepPanels value="1">
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
@run-network="runNetworkCb($event, () => activateCallback('2'))" />
<Config
:instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
@run-network="runNetworkCb($event, () => activateCallback('2'))"
/>
</StepPanel>
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
<div class="flex flex-column">
<Status :instance-id="networkStore.curNetworkId" />
</div>
<div class="flex pt-4 justify-content-center">
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
<Button
:label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))"
/>
</div>
</StepPanel>
</StepPanels>

View File

@ -108,7 +108,8 @@ export const useNetworkStore = defineStore('networkStore', {
loadAutoStartInstIdsFromLocalStorage() {
try {
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
} catch (e) {
}
catch (e) {
console.error(e)
this.autoStartInstIds = []
}

View File

@ -16,7 +16,6 @@
font-weight: 400;
color: #0f0f0f;
background-color: white;
font-synthesis: none;
text-rendering: optimizeLegibility;

View File

@ -12,7 +12,7 @@ declare module 'vue-router/auto-routes' {
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'unplugin-vue-router/types'
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router

View File

@ -1,19 +1,19 @@
import path from 'node:path'
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import Layouts from 'vite-plugin-vue-layouts'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import process from 'node:process'
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import VueDevTools from 'vite-plugin-vue-devtools'
import VueRouter from 'unplugin-vue-router/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
import Vue from '@vitejs/plugin-vue'
import { internalIpV4Sync } from 'internal-ip'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import { VueRouterAutoImports } from 'unplugin-vue-router'
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { internalIpV4Sync } from 'internal-ip';
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
import VueDevTools from 'vite-plugin-vue-devtools'
import Layouts from 'vite-plugin-vue-layouts'
const host = process.env.TAURI_DEV_HOST;
const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/
export default defineConfig(async () => ({
@ -23,7 +23,6 @@ export default defineConfig(async () => ({
},
},
plugins: [
svelte(),
VueMacros({
plugins: {
vue: Vue({
@ -100,10 +99,10 @@ export default defineConfig(async () => ({
},
hmr: host
? {
protocol: 'ws',
host: internalIpV4Sync(),
port: 1430,
}
protocol: 'ws',
host: internalIpV4Sync(),
port: 1430,
}
: undefined,
},
}))

View File

@ -13,3 +13,5 @@ pub mod launcher;
pub mod rpc;
pub mod tunnel;
pub mod utils;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");