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": { "settings": {
"eslint.experimental.useFlatConfig": true, "eslint.useFlatConfig": true,
"prettier.enable": false, "prettier.enable": false,
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {

View File

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

View File

@ -75,3 +75,12 @@ dhcp_experimental_warning: 实验性警告使用DHCP时如果组网环境中
tray: tray:
show: 显示 / 隐藏 show: 显示 / 隐藏
exit: 退出 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: tray:
show: Show / Hide show: Show / Hide
exit: Exit 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" "lint:fix": "eslint . --ignore-pattern src-tauri --fix"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "^4.0.4", "@primevue/themes": "^4.0.5",
"@tauri-apps/plugin-autostart": "2.0.0-rc.0", "@tauri-apps/plugin-autostart": "2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0", "@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
"@tauri-apps/plugin-os": "2.0.0-rc.0", "@tauri-apps/plugin-os": "2.0.0-rc.1",
"@tauri-apps/plugin-process": "2.0.0-rc.0", "@tauri-apps/plugin-process": "2.0.0-rc.1",
"@tauri-apps/plugin-shell": "2.0.0-rc.0", "@tauri-apps/plugin-shell": "2.0.0-rc.1",
"aura": "link:@primevue/themes/aura", "aura": "link:@primevue/themes/aura",
"pinia": "^2.2.1", "pinia": "^2.2.2",
"primeflex": "^3.3.1", "primeflex": "^3.3.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.0.4", "primevue": "^4.0.5",
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice", "tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice",
"vue": "^3.4.38", "vue": "^3.5.3",
"vue-i18n": "^9.13.1", "vue-i18n": "^10.0.0",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^2.25.1", "@antfu/eslint-config": "^3.5.0",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^5.0.0",
"@primevue/auto-import-resolver": "^4.0.4", "@primevue/auto-import-resolver": "^4.0.5",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tauri-apps/api": "2.0.0-rc.0", "@tauri-apps/api": "2.0.0-rc.0",
"@tauri-apps/cli": "2.0.0-rc.3", "@tauri-apps/cli": "2.0.0-rc.3",
"@types/node": "^20.14.15", "@types/node": "^22.5.4",
"@types/uuid": "^9.0.8", "@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue": "^5.1.3",
"@vue-macros/volar": "^0.19.1", "@vue-macros/volar": "^0.29.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.9.0", "eslint": "^9.10.0",
"eslint-plugin-format": "^0.1.2", "eslint-plugin-format": "^0.1.2",
"internal-ip": "^8.0.0", "internal-ip": "^8.0.0",
"postcss": "^8.4.41", "postcss": "^8.4.45",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"typescript": "^5.5.4", "typescript": "^5.6.2",
"unplugin-auto-import": "^0.17.8", "unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4", "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-markdown": "^0.26.2",
"unplugin-vue-router": "^0.8.8", "unplugin-vue-router": "^0.10.8",
"uuid": "^9.0.1", "uuid": "^10.0.0",
"vite": "^5.4.1", "vite": "^5.4.3",
"vite-plugin-vue-devtools": "^7.3.8", "vite-plugin-vue-devtools": "^7.4.4",
"vite-plugin-vue-layouts": "^0.11.0", "vite-plugin-vue-layouts": "^0.11.0",
"vue-i18n": "^9.13.1", "vue-i18n": "^10.0.0",
"vue-tsc": "^2.0.29" "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>> = static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
once_cell::sync::Lazy::new(Default::default); once_cell::sync::Lazy::new(Default::default);
#[tauri::command]
fn easytier_version() -> Result<String, String> {
Ok(easytier::VERSION.to_string())
}
#[tauri::command] #[tauri::command]
fn is_autostart() -> Result<bool, String> { fn is_autostart() -> Result<bool, String> {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
@ -365,7 +370,8 @@ pub fn run() {
get_os_hostname, get_os_hostname,
set_logging_level, set_logging_level,
set_tun_fd, set_tun_fd,
is_autostart is_autostart,
easytier_version
]) ])
.on_window_event(|_win, event| match event { .on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]

View File

@ -24,6 +24,7 @@ declare global {
const getActivePinia: typeof import('pinia')['getActivePinia'] const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
const getOsHostname: typeof import('./composables/network')['getOsHostname'] const getOsHostname: typeof import('./composables/network')['getOsHostname']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService'] const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
@ -44,8 +45,8 @@ declare global {
const nextTick: typeof import('vue')['nextTick'] const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated'] const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount'] const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave'] const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate'] const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated'] const onDeactivated: typeof import('vue')['onDeactivated']
@ -90,8 +91,8 @@ declare global {
const useI18n: typeof import('vue-i18n')['useI18n'] const useI18n: typeof import('vue-i18n')['useI18n']
const useLink: typeof import('vue-router/auto')['useLink'] const useLink: typeof import('vue-router/auto')['useLink']
const useNetworkStore: typeof import('./stores/network')['useNetworkStore'] const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
const useRoute: typeof import('vue-router/auto')['useRoute'] const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots'] const useSlots: typeof import('vue')['useSlots']
const useTray: typeof import('./composables/tray')['useTray'] const useTray: typeof import('./composables/tray')['useTray']
const watch: typeof import('vue')['watch'] const watch: typeof import('vue')['watch']
@ -121,13 +122,13 @@ declare module 'vue' {
readonly customRef: UnwrapRef<typeof import('vue')['customRef']> readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']> readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']> 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 defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']> readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']> readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']> readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']> readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
@ -146,8 +147,8 @@ declare module 'vue' {
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']> readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']> readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']> readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']> readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']> readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
@ -191,8 +192,8 @@ declare module 'vue' {
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']> readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']> readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']> readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']> readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']> readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']> readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
readonly watch: UnwrapRef<typeof import('vue')['watch']> 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"> <script setup lang="ts">
import InputGroup from 'primevue/inputgroup' import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon' 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 { ping } from 'tauri-plugin-vpnservice-api'
import { getOsHostname } from '~/composables/network'
import { NetworkingMethod } from '~/types/network'
const props = defineProps<{ const props = defineProps<{
configInvalid?: boolean configInvalid?: boolean
@ -14,6 +13,8 @@ const props = defineProps<{
defineEmits(['runNetwork']) defineEmits(['runNetwork'])
const { t } = useI18n()
const networking_methods = ref([ const networking_methods = ref([
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') }, { value: NetworkingMethod.PublicServer, label: () => t('public_server') },
{ value: NetworkingMethod.Manual, label: () => t('manual') }, { value: NetworkingMethod.Manual, label: () => t('manual') },
@ -32,24 +33,26 @@ const curNetwork = computed(() => {
return networkStore.curNetwork 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[] { function searchUrlSuggestions(e: { query: string }): string[] {
const query = e.query const query = e.query
let ret = [] const ret = []
// if query match "^\w+:.*", then no proto prefix // if query match "^\w+:.*", then no proto prefix
if (query.match(/^\w+:.*/)) { if (query.match(/^\w+:.*/)) {
// if query is a valid url, then add to suggestions // if query is a valid url, then add to suggestions
try { try {
new URL(query) new URL(query)
ret.push(query) ret.push(query)
} catch (e) {} }
} else { catch (e) {}
for (let proto in protos) { }
let item = proto + '://' + query else {
for (const proto in protos) {
let item = `${proto}://${query}`
// if query match ":\d+$", then no port suffix // if query match ":\d+$", then no port suffix
if (!query.match(/:\d+$/)) { if (!query.match(/:\d+$/)) {
item += ':' + protos[proto] item += `:${protos[proto]}`
} }
ret.push(item) ret.push(item)
} }
@ -58,42 +61,42 @@ function searchUrlSuggestions(e: { query: string }): string[] {
return ret return ret
} }
const publicServerSuggestions = ref(['']) const publicServerSuggestions = ref([''])
const searchPresetPublicServers = (e: { query: string }) => { function searchPresetPublicServers(e: { query: string }) {
const presetPublicServers = [ const presetPublicServers = [
'tcp://easytier.public.kkrainbow.top:11010', 'tcp://easytier.public.kkrainbow.top:11010',
] ]
let query = e.query const query = e.query
// if query is sub string of presetPublicServers, add to suggestions // if query is sub string of presetPublicServers, add to suggestions
let ret = presetPublicServers.filter((item) => item.includes(query)) let ret = presetPublicServers.filter(item => item.includes(query))
// add additional suggestions // add additional suggestions
if (query.length > 0) { if (query.length > 0) {
ret = ret.concat(searchUrlSuggestions(e)) ret = ret.concat(searchUrlSuggestions(e))
} }
publicServerSuggestions.value = ret publicServerSuggestions.value = ret
} }
const peerSuggestions = ref(['']) const peerSuggestions = ref([''])
const searchPeerSuggestions = (e: { query: string }) => { function searchPeerSuggestions(e: { query: string }) {
peerSuggestions.value = searchUrlSuggestions(e) peerSuggestions.value = searchUrlSuggestions(e)
} }
const listenerSuggestions = ref(['']) const listenerSuggestions = ref([''])
const searchListenerSuggestiong = (e: { query: string }) => { function searchListenerSuggestiong(e: { query: string }) {
let ret = [] const ret = []
for (let proto in protos) { for (const proto in protos) {
let item = proto + '://0.0.0.0:'; let item = `${proto}://0.0.0.0:`
// if query is a number, use it as port // if query is a number, use it as port
if (e.query.match(/^\d+$/)) { if (e.query.match(/^\d+$/)) {
item += e.query item += e.query
} else { }
else {
item += protos[proto] item += protos[proto]
} }
@ -112,7 +115,7 @@ const searchListenerSuggestiong = (e: { query: string }) => {
function validateHostname() { function validateHostname() {
if (curNetwork.value.hostname) { if (curNetwork.value.hostname) {
// eslint no-useless-escape // 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) if (name.length > 32)
name = name.substring(0, 32) name = name.substring(0, 32)
@ -132,7 +135,7 @@ onMounted(async () => {
<template> <template>
<div class="flex flex-column h-full"> <div class="flex flex-column h-full">
<div class="flex flex-column"> <div class="flex flex-column">
<div class="w-10/12 self-center "> <div class="w-10/12 self-center mb-3">
<Message severity="warn"> <Message severity="warn">
{{ t('dhcp_experimental_warning') }} {{ t('dhcp_experimental_warning') }}
</Message> </Message>
@ -151,8 +154,10 @@ onMounted(async () => {
</label> </label>
</div> </div>
<InputGroup> <InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp" <InputText
aria-describedby="virtual_ipv4-help" /> id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help"
/>
<InputGroupAddon> <InputGroupAddon>
<span>/24</span> <span>/24</span>
</InputGroupAddon> </InputGroupAddon>
@ -167,23 +172,29 @@ onMounted(async () => {
</div> </div>
<div class="flex flex-column gap-2 basis-5/12 grow"> <div class="flex flex-column gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label> <label for="network_secret">{{ t('network_secret') }}</label>
<InputText id="network_secret" v-model="curNetwork.network_secret" <InputText
aria-describedby=" network_secret-help" /> id="network_secret" v-model="curNetwork.network_secret"
aria-describedby=" network_secret-help"
/>
</div> </div>
</div> </div>
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow"> <div class="flex flex-column gap-2 basis-5/12 grow">
<label for="nm">{{ t('networking_method') }}</label> <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"> <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'])" 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" <AutoComplete
:virtualScrollerOptions="{ itemSize: 38 }" class="grow" dropdown @complete="searchPresetPublicServers" :completeOnFocus="true" v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" v-model="curNetwork.public_server_url"
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> </div>
</div> </div>
@ -197,64 +208,80 @@ onMounted(async () => {
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow"> <div class="flex flex-column gap-2 basis-5/12 grow">
<label for="hostname">{{ t('hostname') }}</label> <label for="hostname">{{ t('hostname') }}</label>
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true" <InputText
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" /> id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname"
/>
</div> </div>
</div> </div>
<div class="flex flex-row gap-x-9 flex-wrap w-full"> <div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-column gap-2 grow p-fluid"> <div class="flex flex-column gap-2 grow p-fluid">
<label for="username">{{ t('proxy_cidrs') }}</label> <label for="username">{{ t('proxy_cidrs') }}</label>
<Chips id="chips" v-model="curNetwork.proxy_cidrs" <Chips
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" /> id="chips" v-model="curNetwork.proxy_cidrs"
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full"
/>
</div> </div>
</div> </div>
<div class="flex flex-row gap-x-9 flex-wrap "> <div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-column gap-2 grow"> <div class="flex flex-column gap-2 grow">
<label for="username">VPN Portal</label> <label for="username">VPN Portal</label>
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times" <ToggleButton
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48"/> v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
<div class="items-center flex flex-row gap-x-4" v-if="curNetwork.enable_vpn_portal"> :on-label="t('off_text')" :off-label="t('on_text')" class="w-48"
<div class="min-w-64"> />
<InputGroup> <div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
<InputText v-model="curNetwork.vpn_portal_client_network_addr" <div class="min-w-64">
:placeholder="t('vpn_portal_client_network')" /> <InputGroup>
<InputGroupAddon> <InputText
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span> v-model="curNetwork.vpn_portal_client_network_addr"
</InputGroupAddon> :placeholder="t('vpn_portal_client_network')"
</InputGroup> />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" <InputNumber
:format="false" :min="0" :max="65535" class="w-8" fluid/> v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
</div> :format="false" :min="0" :max="65535" class="w-8" fluid
/>
</div> </div>
</div>
</div> </div>
</div> </div>
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 grow p-fluid"> <div class="flex flex-column gap-2 grow p-fluid">
<label for="listener_urls">{{ t('listener_urls') }}</label> <label for="listener_urls">{{ t('listener_urls') }}</label>
<AutoComplete id="listener_urls" :suggestions="listenerSuggestions" <AutoComplete
class="w-full" dropdown @complete="searchListenerSuggestiong" :completeOnFocus="true" 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'])" :placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
v-model="curNetwork.listener_urls" multiple/> multiple @complete="searchListenerSuggestiong"
/>
</div> </div>
</div> </div>
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow"> <div class="flex flex-column gap-2 basis-5/12 grow">
<label for="rpc_port">{{ t('rpc_port') }}</label> <label for="rpc_port">{{ t('rpc_port') }}</label>
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help" <InputNumber
:format="false" :min="0" :max="65535" /> id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
:format="false" :min="0" :max="65535"
/>
</div> </div>
</div> </div>
</div> </div>
</Panel> </Panel>
<div class="flex pt-4 justify-content-center"> <div class="flex pt-4 justify-content-center">
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid" <Button
@click="$emit('runNetwork', curNetwork)" /> :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)"
/>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -1,183 +1,184 @@
import { addPluginListener } from '@tauri-apps/api/core'; import { addPluginListener } from '@tauri-apps/api/core'
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'; import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
import { Route } from '~/types/network'; import type { Route } from '~/types/network'
const networkStore = useNetworkStore() const networkStore = useNetworkStore()
interface vpnStatus { interface vpnStatus {
running: boolean running: boolean
ipv4Addr: string | null | undefined ipv4Addr: string | null | undefined
ipv4Cidr: number | null | undefined ipv4Cidr: number | null | undefined
routes: string[] routes: string[]
} }
var curVpnStatus: vpnStatus = { const curVpnStatus: vpnStatus = {
running: false, running: false,
ipv4Addr: undefined, ipv4Addr: undefined,
ipv4Cidr: undefined, ipv4Cidr: undefined,
routes: [] routes: [],
} }
async function waitVpnStatus(target_status: boolean, timeout_sec: number) { async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
let start_time = Date.now() const start_time = Date.now()
while (curVpnStatus.running !== target_status) { while (curVpnStatus.running !== target_status) {
if (Date.now() - start_time > timeout_sec * 1000) { if (Date.now() - start_time > timeout_sec * 1000) {
throw new Error('wait vpn status timeout') throw new Error('wait vpn status timeout')
}
await new Promise(r => setTimeout(r, 50))
} }
await new Promise(r => setTimeout(r, 50))
}
} }
async function doStopVpn() { async function doStopVpn() {
if (!curVpnStatus.running) { if (!curVpnStatus.running) {
return return
} }
console.log('stop vpn') console.log('stop vpn')
let stop_ret = await stop_vpn() const stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret))) console.log('stop vpn', JSON.stringify((stop_ret)))
await waitVpnStatus(false, 3) await waitVpnStatus(false, 3)
curVpnStatus.ipv4Addr = undefined curVpnStatus.ipv4Addr = undefined
curVpnStatus.routes = [] curVpnStatus.routes = []
} }
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) { async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
if (curVpnStatus.running) { if (curVpnStatus.running) {
return return
} }
console.log('start vpn') console.log('start vpn')
let start_ret = await start_vpn({ const start_ret = await start_vpn({
"ipv4Addr": ipv4Addr + '/' + cidr, ipv4Addr: `${ipv4Addr}/${cidr}`,
"routes": routes, routes,
"disallowedApplications": ["com.kkrainbow.easytier"], disallowedApplications: ['com.kkrainbow.easytier'],
"mtu": 1300, mtu: 1300,
}); })
if (start_ret?.errorMsg?.length) { if (start_ret?.errorMsg?.length) {
throw new Error(start_ret.errorMsg) throw new Error(start_ret.errorMsg)
} }
await waitVpnStatus(true, 3) await waitVpnStatus(true, 3)
curVpnStatus.ipv4Addr = ipv4Addr curVpnStatus.ipv4Addr = ipv4Addr
curVpnStatus.routes = routes curVpnStatus.routes = routes
} }
async function onVpnServiceStart(payload: any) { async function onVpnServiceStart(payload: any) {
console.log('vpn service start', JSON.stringify(payload)) console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true curVpnStatus.running = true
if (payload.fd) { if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd) setTunFd(networkStore.networkInstanceIds[0], payload.fd)
} }
} }
async function onVpnServiceStop(payload: any) { async function onVpnServiceStop(payload: any) {
console.log('vpn service stop', JSON.stringify(payload)) console.log('vpn service stop', JSON.stringify(payload))
curVpnStatus.running = false curVpnStatus.running = false
} }
async function registerVpnServiceListener() { async function registerVpnServiceListener() {
console.log('register vpn service listener') console.log('register vpn service listener')
await addPluginListener( await addPluginListener(
'vpnservice', 'vpnservice',
'vpn_service_start', 'vpn_service_start',
onVpnServiceStart onVpnServiceStart,
) )
await addPluginListener( await addPluginListener(
'vpnservice', 'vpnservice',
'vpn_service_stop', 'vpn_service_stop',
onVpnServiceStop onVpnServiceStop,
) )
} }
function getRoutesForVpn(routes: Route[]): string[] { function getRoutesForVpn(routes: Route[]): string[] {
if (!routes) { if (!routes) {
return [] return []
} }
let ret = [] const ret = []
for (let r of routes) { for (const r of routes) {
for (let cidr of r.proxy_cidrs) { for (let cidr of r.proxy_cidrs) {
if (cidr.indexOf('/') === -1) { if (!cidr.includes('/')) {
cidr += '/32' cidr += '/32'
} }
ret.push(cidr) ret.push(cidr)
}
} }
}
// sort and dedup // sort and dedup
return Array.from(new Set(ret)).sort() return Array.from(new Set(ret)).sort()
} }
async function onNetworkInstanceChange() { async function onNetworkInstanceChange() {
let insts = networkStore.networkInstanceIds const insts = networkStore.networkInstanceIds
if (!insts) { if (!insts) {
await doStopVpn() await doStopVpn()
return 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]] try {
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) { await doStartVpn(virtual_ip, 24, routes)
await doStopVpn()
return
} }
catch (e) {
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4 console.error('start vpn failed, clear all network insts.', e)
if (!virtual_ip || !virtual_ip.length) { networkStore.clearNetworkInstances()
await doStopVpn() await retainNetworkInstance(networkStore.networkInstanceIds)
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
} }
}
} }
async function watchNetworkInstance() { async function watchNetworkInstance() {
var subscribe_running = false let subscribe_running = false
networkStore.$subscribe(async () => { networkStore.$subscribe(async () => {
if (subscribe_running) { if (subscribe_running) {
return return
} }
subscribe_running = true subscribe_running = true
try { try {
await onNetworkInstanceChange() await onNetworkInstanceChange()
} catch (_) { }
} catch (_) {
subscribe_running = false }
}) subscribe_running = false
})
} }
export async function initMobileVpnService() { export async function initMobileVpnService() {
await registerVpnServiceListener() await registerVpnServiceListener()
await watchNetworkInstance() await watchNetworkInstance()
} }
export async function prepareVpnService() { export async function prepareVpnService() {
console.log('prepare vpn') console.log('prepare vpn')
let prepare_ret = await prepare_vpn() const prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret))) console.log('prepare vpn', JSON.stringify((prepare_ret)))
if (prepare_ret?.errorMsg?.length) { if (prepare_ret?.errorMsg?.length) {
throw new Error(prepare_ret.errorMsg) 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' 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) { export async function setTunFd(instanceId: string, fd: number) {
return await invoke('set_tun_fd', { instanceId, fd }) 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 { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
import { TrayIcon } from '@tauri-apps/api/tray' import { TrayIcon } from '@tauri-apps/api/tray'
import { getCurrentWindow } from '@tauri-apps/api/window'
import pkg from '~/../package.json' import pkg from '~/../package.json'
const DEFAULT_TRAY_NAME = 'main' const DEFAULT_TRAY_NAME = 'main'
@ -8,14 +8,15 @@ const DEFAULT_TRAY_NAME = 'main'
async function toggleVisibility() { async function toggleVisibility() {
if (await getCurrentWindow().isVisible()) { if (await getCurrentWindow().isVisible()) {
await getCurrentWindow().hide() await getCurrentWindow().hide()
} else { }
else {
await getCurrentWindow().show() await getCurrentWindow().show()
await getCurrentWindow().setFocus() await getCurrentWindow().setFocus()
} }
} }
export async function useTray(init: boolean = false) { export async function useTray(init: boolean = false) {
let tray; let tray
try { try {
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME) tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
if (!tray) { if (!tray) {
@ -29,17 +30,18 @@ export async function useTray(init: boolean = false) {
}), }),
action: async () => { action: async () => {
toggleVisibility() toggleVisibility()
} },
}) })
} }
} catch (error) { }
catch (error) {
console.warn('Error while creating tray icon:', error) console.warn('Error while creating tray icon:', error)
return null return null
} }
if (init) { if (init) {
tray.setTooltip(`EasyTier\n${pkg.version}`) tray.setTooltip(`EasyTier\n${pkg.version}`)
tray.setMenuOnLeftClick(false); tray.setMenuOnLeftClick(false)
tray.setMenu(await Menu.new({ tray.setMenu(await Menu.new({
id: 'main', id: 'main',
items: await generateMenuItem(), items: await generateMenuItem(),
@ -59,7 +61,7 @@ export async function generateMenuItem() {
export async function MenuItemExit(text: string) { export async function MenuItemExit(text: string) {
return await PredefinedMenuItem.new({ return await PredefinedMenuItem.new({
text: text, text,
item: 'Quit', item: 'Quit',
}) })
} }
@ -69,14 +71,15 @@ export async function MenuItemShow(text: string) {
id: 'show', id: 'show',
text, text,
action: async () => { action: async () => {
await toggleVisibility(); await toggleVisibility()
}, },
}) })
} }
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) { export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
const tray = await useTray() const tray = await useTray()
if (!tray) return if (!tray)
return
const menu = await Menu.new({ const menu = await Menu.new({
id: 'main', id: 'main',
items: items || await generateMenuItem(), items: items || await generateMenuItem(),
@ -86,14 +89,16 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
export async function setTrayRunState(isRunning: boolean = false) { export async function setTrayRunState(isRunning: boolean = false) {
const tray = await useTray() const tray = await useTray()
if (!tray) return if (!tray)
return
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico') tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
} }
export async function setTrayTooltip(tooltip: string) { export async function setTrayTooltip(tooltip: string) {
if (tooltip) { if (tooltip) {
const tray = await useTray() const tray = await useTray()
if (!tray) return if (!tray)
return
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`) tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
tray.setTitle(`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 Aura from '@primevue/themes/aura'
import { createRouter, createWebHistory } from 'vue-router/auto'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import ToastService from 'primevue/toastservice' 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 '~/styles.css'
import Aura from '@primevue/themes/aura'
import 'primeicons/primeicons.css' import 'primeicons/primeicons.css'
import 'primeflex/primeflex.css' import 'primeflex/primeflex.css'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch'
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
@ -18,8 +18,9 @@ if (import.meta.env.PROD) {
event.key === 'F5' event.key === 'F5'
|| (event.ctrlKey && event.key === 'r') || (event.ctrlKey && event.key === 'r')
|| (event.metaKey && event.key === 'r') || (event.metaKey && event.key === 'r')
) ) {
event.preventDefault() event.preventDefault()
}
}) })
document.addEventListener('contextmenu', (event) => { document.addEventListener('contextmenu', (event) => {
@ -35,7 +36,7 @@ async function main() {
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
extendRoutes: routes => setupLayouts(routes), routes,
}) })
app.use(router) app.use(router)
@ -45,11 +46,12 @@ async function main() {
theme: { theme: {
preset: Aura, preset: Aura,
options: { options: {
prefix: 'p', prefix: 'p',
darkModeSelector: 'system', darkModeSelector: 'system',
cssLayer: false cssLayer: false,
} },
}}) },
})
app.use(ToastService) app.use(ToastService)
app.mount('#app') app.mount('#app')
} }

View File

@ -1,17 +1,17 @@
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart' import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> { export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
try { try {
target_enable ? await enable() : await disable() target_enable ? await enable() : await disable()
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled())) localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
return isEnabled() return isEnabled()
} }
catch (e) { catch (e) {
console.error(e) console.error(e)
return false return false
} }
} }
export function getAutoLaunchStatusAsync(): boolean { 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 { createI18n } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
// Import i18n resources // Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import // https://vitejs.dev/guide/features.html#glob-import

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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