mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 02:09:06 +00:00
feat(gui): add service and remote mode support (#1578)
This PR fundamentally restructures the EasyTier GUI, introducing support for service mode and remote mode, transforming it from a simple desktop application into a powerful network management terminal. This change allows users to persistently run the EasyTier core as a background service or remotely manage multiple EasyTier instances, greatly improving deployment flexibility and manageability.
This commit is contained in:
Vendored
+14
@@ -28,15 +28,20 @@ declare global {
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion']
|
||||
const getNetworkMetas: typeof import('./composables/backend')['getNetworkMetas']
|
||||
const getServiceStatus: typeof import('./composables/backend')['getServiceStatus']
|
||||
const h: typeof import('vue')['h']
|
||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||
const initRpcConnection: typeof import('./composables/backend')['initRpcConnection']
|
||||
const initService: typeof import('./composables/backend')['initService']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isClientRunning: typeof import('./composables/backend')['isClientRunning']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
|
||||
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
|
||||
const loadMode: typeof import('./composables/mode')['loadMode']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
@@ -69,11 +74,13 @@ declare global {
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const runNetworkInstance: typeof import('./composables/backend')['runNetworkInstance']
|
||||
const saveMode: typeof import('./composables/mode')['saveMode']
|
||||
const saveNetworkConfig: typeof import('./composables/backend')['saveNetworkConfig']
|
||||
const sendConfigs: typeof import('./composables/backend')['sendConfigs']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setLoggingLevel: typeof import('./composables/backend')['setLoggingLevel']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setServiceStatus: typeof import('./composables/backend')['setServiceStatus']
|
||||
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
||||
const setTrayRunState: typeof import('./composables/tray')['setTrayRunState']
|
||||
const setTrayTooltip: typeof import('./composables/tray')['setTrayTooltip']
|
||||
@@ -141,15 +148,20 @@ declare module 'vue' {
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']>
|
||||
readonly getNetworkMetas: UnwrapRef<typeof import('./composables/backend')['getNetworkMetas']>
|
||||
readonly getServiceStatus: UnwrapRef<typeof import('./composables/backend')['getServiceStatus']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly initRpcConnection: UnwrapRef<typeof import('./composables/backend')['initRpcConnection']>
|
||||
readonly initService: UnwrapRef<typeof import('./composables/backend')['initService']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isClientRunning: UnwrapRef<typeof import('./composables/backend')['isClientRunning']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
|
||||
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
|
||||
readonly loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
@@ -182,11 +194,13 @@ declare module 'vue' {
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/backend')['runNetworkInstance']>
|
||||
readonly saveMode: UnwrapRef<typeof import('./composables/mode')['saveMode']>
|
||||
readonly saveNetworkConfig: UnwrapRef<typeof import('./composables/backend')['saveNetworkConfig']>
|
||||
readonly sendConfigs: UnwrapRef<typeof import('./composables/backend')['sendConfigs']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/backend')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setServiceStatus: UnwrapRef<typeof import('./composables/backend')['setServiceStatus']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
||||
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import type { Mode, ServiceMode, RemoteMode } from '~/composables/mode';
|
||||
import { appConfigDir, appLogDir } from '@tauri-apps/api/path';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const model = defineModel<Mode>({ required: true })
|
||||
const emit = defineEmits(['uninstall-service', 'stop-service'])
|
||||
|
||||
const defaultConfigDir = ref('')
|
||||
const defaultLogDir = ref('')
|
||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
||||
const isServiceStatusLoaded = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
defaultConfigDir.value = await join(await appConfigDir(), 'config.d')
|
||||
defaultLogDir.value = await appLogDir()
|
||||
})
|
||||
|
||||
const modeOptions = computed(() => [
|
||||
{ label: t('mode.normal'), value: 'normal' },
|
||||
{ label: t('mode.service'), value: 'service' },
|
||||
{ label: t('mode.remote'), value: 'remote' },
|
||||
]);
|
||||
|
||||
const serviceMode = computed({
|
||||
get: () => model.value.mode === 'service' ? model.value as ServiceMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const remoteMode = computed({
|
||||
get: () => model.value.mode === 'remote' ? model.value as RemoteMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const statusColorClass = computed(() => {
|
||||
switch (serviceStatus.value) {
|
||||
case 'Running':
|
||||
return 'text-green-600'
|
||||
case 'Stopped':
|
||||
return 'text-orange-600'
|
||||
case 'NotInstalled':
|
||||
return 'text-gray-600'
|
||||
default:
|
||||
return 'text-gray-600'
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => model.value.mode, async (newMode, oldMode) => {
|
||||
if (newMode === oldMode)
|
||||
return
|
||||
|
||||
if (newMode === 'service' && !isServiceStatusLoaded.value) {
|
||||
serviceStatus.value = await getServiceStatus()
|
||||
isServiceStatusLoaded.value = true
|
||||
}
|
||||
|
||||
const oldModelValue = { ...model.value }
|
||||
|
||||
if (newMode === 'normal') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'normal',
|
||||
}
|
||||
}
|
||||
else if (newMode === 'service') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'service',
|
||||
config_dir: serviceMode.value?.config_dir || defaultConfigDir.value,
|
||||
rpc_portal: serviceMode.value?.rpc_portal || '127.0.0.1:15999',
|
||||
file_log_level: serviceMode.value?.file_log_level || 'off',
|
||||
file_log_dir: serviceMode.value?.file_log_dir || defaultLogDir.value,
|
||||
}
|
||||
}
|
||||
else if (newMode === 'remote') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'remote',
|
||||
remote_rpc_address: remoteMode.value?.remote_rpc_address || 'tcp://127.0.0.1:15999',
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<SelectButton id="mode-select" v-model="model.mode" :options="modeOptions" option-label="label"
|
||||
option-value="value" fluid />
|
||||
</div>
|
||||
|
||||
<!-- Mode descriptions -->
|
||||
<div v-if="model.mode === 'normal'" class="text-sm text-gray-500">
|
||||
{{ t('mode.normal_description') }}
|
||||
</div>
|
||||
<div v-else-if="model.mode === 'service'" class="text-sm text-gray-500">
|
||||
{{ t('mode.service_description') }}
|
||||
</div>
|
||||
<div v-else-if="model.mode === 'remote'" class="text-sm text-gray-500">
|
||||
{{ t('mode.remote_description') }}
|
||||
</div>
|
||||
|
||||
<div v-if="serviceMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="config-dir">{{ t('mode.config_dir') }}</label>
|
||||
<InputText id="config-dir" v-model="serviceMode.config_dir" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="rpc-portal">{{ t('mode.rpc_portal') }}</label>
|
||||
<InputText id="rpc-portal" v-model="serviceMode.rpc_portal" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-level">{{ t('mode.log_level') }}</label>
|
||||
<Select id="log-level" v-model="serviceMode.file_log_level"
|
||||
:options="['off', 'warn', 'info', 'debug', 'trace']" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-dir">{{ t('mode.log_dir') }}</label>
|
||||
<InputText id="log-dir" v-model="serviceMode.file_log_dir" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<label>{{ t('mode.service_status') }}</label>
|
||||
<span :class="statusColorClass">{{ t(`mode.service_status_${serviceStatus.toLowerCase()}`) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button :label="t('mode.stop_service')" icon="pi pi-stop-circle" v-if="serviceStatus === 'Running'"
|
||||
@click="emit('stop-service')" severity="warn" text />
|
||||
<Button :label="t('mode.uninstall_service')" icon="pi pi-trash" v-if="serviceStatus !== 'NotInstalled'"
|
||||
@click="emit('uninstall-service')" severity="danger" text />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="remoteMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="remote-addr">{{ t('mode.remote_rpc_address') }}</label>
|
||||
<InputText id="remote-addr" v-model="remoteMode.remote_rpc_address" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Api, type NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
||||
import { getAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
|
||||
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||
interface ServiceOptions {
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
file_log_level: string
|
||||
file_log_dir: string
|
||||
}
|
||||
|
||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
@@ -61,10 +69,29 @@ export async function getConfig(instanceId: string) {
|
||||
|
||||
export async function sendConfigs() {
|
||||
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
||||
let autoStartInstIds = getAutoLaunchStatusAsync() ? JSON.parse(localStorage.getItem('autoStartInstIds') || '[]') : []
|
||||
return await invoke('load_configs', { configs: networkList, enabledNetworks: autoStartInstIds })
|
||||
return await invoke('load_configs', { configs: networkList, enabledNetworks: [] })
|
||||
}
|
||||
|
||||
export async function getNetworkMetas(instanceIds: string[]) {
|
||||
return await invoke<GetNetworkMetasResponse>('get_network_metas', { instanceIds })
|
||||
}
|
||||
|
||||
export async function initService(opts?: ServiceOptions) {
|
||||
return await invoke('init_service', { opts })
|
||||
}
|
||||
|
||||
export async function setServiceStatus(enable: boolean) {
|
||||
return await invoke('set_service_status', { enable })
|
||||
}
|
||||
|
||||
export async function getServiceStatus() {
|
||||
return await invoke<ServiceStatus>('get_service_status')
|
||||
}
|
||||
|
||||
export async function initRpcConnection(url?: string) {
|
||||
return await invoke('init_rpc_connection', { url })
|
||||
}
|
||||
|
||||
export async function isClientRunning() {
|
||||
return await invoke<boolean>('is_client_running')
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { NetworkTypes } from "easytier-frontend-lib"
|
||||
|
||||
const EVENTS = Object.freeze({
|
||||
SAVE_CONFIGS: 'save_configs',
|
||||
SAVE_ENABLED_NETWORKS: 'save_enabled_networks',
|
||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
|
||||
VPN_SERVICE_STOP: 'vpn_service_stop',
|
||||
@@ -15,11 +14,6 @@ function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||
localStorage.setItem('networkList', JSON.stringify(event.payload));
|
||||
}
|
||||
|
||||
function onSaveEnabledNetworks(event: Event<string[]>) {
|
||||
console.log(`Received event '${EVENTS.SAVE_ENABLED_NETWORKS}': ${event.payload}`);
|
||||
localStorage.setItem('autoStartInstIds', JSON.stringify(event.payload));
|
||||
}
|
||||
|
||||
async function onPreRunNetworkInstance(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService(event.payload);
|
||||
@@ -39,7 +33,6 @@ async function onVpnServiceStop(event: Event<string>) {
|
||||
export async function listenGlobalEvents() {
|
||||
const unlisteners = [
|
||||
await listen(EVENTS.SAVE_CONFIGS, onSaveConfigs),
|
||||
await listen(EVENTS.SAVE_ENABLED_NETWORKS, onSaveEnabledNetworks),
|
||||
await listen(EVENTS.PRE_RUN_NETWORK_INSTANCE, onPreRunNetworkInstance),
|
||||
await listen(EVENTS.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
|
||||
await listen(EVENTS.VPN_SERVICE_STOP, onVpnServiceStop),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
interface NormalMode {
|
||||
mode: 'normal'
|
||||
}
|
||||
|
||||
export interface ServiceMode {
|
||||
mode: 'service'
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||
file_log_dir: string
|
||||
}
|
||||
|
||||
export interface RemoteMode {
|
||||
mode: 'remote'
|
||||
remote_rpc_address: string
|
||||
}
|
||||
|
||||
export function saveMode(mode: Mode) {
|
||||
localStorage.setItem('app_mode', JSON.stringify(mode))
|
||||
}
|
||||
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
export function loadMode(): Mode {
|
||||
if (type() === 'android') {
|
||||
return { mode: 'normal' };
|
||||
}
|
||||
const modeStr = localStorage.getItem('app_mode')
|
||||
if (modeStr) {
|
||||
return JSON.parse(modeStr) as Mode
|
||||
} else {
|
||||
return { mode: 'normal' }
|
||||
}
|
||||
}
|
||||
|
||||
export type Mode = NormalMode | ServiceMode | RemoteMode
|
||||
@@ -9,7 +9,7 @@ import App from '~/App.vue';
|
||||
import 'easytier-frontend-lib/style.css';
|
||||
import { ConfirmationService, DialogService, ToastService } from 'primevue';
|
||||
import '~/styles.css';
|
||||
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch';
|
||||
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
@@ -29,7 +29,6 @@ if (import.meta.env.PROD) {
|
||||
|
||||
async function main() {
|
||||
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
|
||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
if (target_enable) {
|
||||
await enable()
|
||||
}
|
||||
else {
|
||||
// 消除没有配置自启动时进行关闭操作报错
|
||||
try {
|
||||
await disable()
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
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'
|
||||
}
|
||||
@@ -9,16 +9,193 @@ import { I18nUtils, RemoteManagement } from "easytier-frontend-lib"
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { GUIRemoteClient } from '~/modules/api'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
|
||||
import { getDockVisibilityStatus, loadDockVisibilityAsync } from '~/modules/dock_visibility'
|
||||
import { useToast, useConfirm } from 'primevue'
|
||||
import { loadMode, saveMode, type Mode } from '~/composables/mode'
|
||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const confirm = useConfirm()
|
||||
const aboutVisible = ref(false)
|
||||
const modeDialogVisible = ref(false)
|
||||
const currentMode = ref<Mode>({ mode: 'normal' })
|
||||
const editingMode = ref<Mode>({ mode: 'normal' })
|
||||
const isModeSaving = ref(false)
|
||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
||||
|
||||
async function openModeDialog() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||
if (editingMode.value.mode === 'service') {
|
||||
serviceStatus.value = await getServiceStatus()
|
||||
}
|
||||
modeDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onModeSave() {
|
||||
if (isModeSaving.value) {
|
||||
return;
|
||||
}
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await initWithMode(editingMode.value);
|
||||
modeDialogVisible.value = false
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error switching mode", e, currentMode.value, editingMode.value)
|
||||
await initWithMode(currentMode.value);
|
||||
}
|
||||
finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onUninstallService() {
|
||||
confirm.require({
|
||||
message: t('mode.uninstall_service_confirm'),
|
||||
header: t('mode.uninstall_service'),
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectProps: {
|
||||
label: t('web.common.cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: t('mode.uninstall_service'),
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await initWithMode({ ...currentMode.value, mode: 'normal' });
|
||||
await initService(undefined)
|
||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.uninstall_service_success'), life: 3000 })
|
||||
modeDialogVisible.value = false
|
||||
} catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error uninstalling service", e)
|
||||
} finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onStopService() {
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await setServiceStatus(false)
|
||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.stop_service_success'), life: 3000 })
|
||||
modeDialogVisible.value = false
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error stopping service", e)
|
||||
}
|
||||
finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initWithMode(mode: Mode) {
|
||||
if (currentMode.value.mode === 'service' && mode.mode !== 'service') {
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "Running") {
|
||||
await setServiceStatus(false)
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
await initService(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
let url: string | undefined = undefined
|
||||
let retrys = 1
|
||||
switch (mode.mode) {
|
||||
case 'remote':
|
||||
if (!mode.remote_rpc_address) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.remote_rpc_address_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
url = mode.remote_rpc_address
|
||||
break;
|
||||
case 'service':
|
||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||
await initService({
|
||||
config_dir: mode.config_dir,
|
||||
file_log_dir: mode.file_log_dir,
|
||||
file_log_level: mode.file_log_level,
|
||||
rpc_portal: mode.rpc_portal,
|
||||
})
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
await setServiceStatus(true)
|
||||
}
|
||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||
retrys = 5
|
||||
break;
|
||||
}
|
||||
for (let i = 0; i < retrys; i++) {
|
||||
try {
|
||||
await connectRpcClient(url)
|
||||
break;
|
||||
} catch (e) {
|
||||
if (i === retrys - 1) {
|
||||
throw e;
|
||||
}
|
||||
console.error("Error connecting rpc client, retrying...", e)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
currentMode.value = mode
|
||||
saveMode(mode)
|
||||
clientRunning.value = await isClientRunning()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentMode.value = loadMode()
|
||||
initWithMode(currentMode.value);
|
||||
});
|
||||
|
||||
useTray(true)
|
||||
let toast = useToast();
|
||||
|
||||
const remoteClient = computed(() => new GUIRemoteClient());
|
||||
const instanceId = ref<string | undefined>(undefined);
|
||||
const clientRunning = ref(false);
|
||||
|
||||
watch(clientRunning, async (newVal, oldVal) => {
|
||||
if (!newVal && oldVal) {
|
||||
await reconnectClient()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
clientRunning.value = await isClientRunning().catch(() => false)
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
clientRunning.value = await isClientRunning()
|
||||
} catch (e) {
|
||||
clientRunning.value = false
|
||||
console.error("Error checking client running status", e)
|
||||
}
|
||||
}, 1000)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
async function reconnectClient() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()));
|
||||
await onModeSave()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.setTimeout(async () => {
|
||||
@@ -81,11 +258,10 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => getAutoLaunchStatus() ? t('disable_auto_launch') : t('enable_auto_launch'),
|
||||
icon: 'pi pi-desktop',
|
||||
command: async () => {
|
||||
await loadAutoLaunchStatusAsync(!getAutoLaunchStatus())
|
||||
},
|
||||
label: () => `${t('mode.switch_mode')}: ${t('mode.' + currentMode.value.mode)}`,
|
||||
icon: 'pi pi-sync',
|
||||
command: openModeDialog,
|
||||
visible: () => type() !== 'android',
|
||||
},
|
||||
{
|
||||
label: () => getDockVisibilityStatus() ? t('hide_dock_icon') : t('show_dock_icon'),
|
||||
@@ -117,6 +293,12 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
||||
},
|
||||
])
|
||||
|
||||
async function connectRpcClient(url?: string) {
|
||||
await initRpcConnection(url)
|
||||
await sendConfigs()
|
||||
console.log("easytier rpc connection established")
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
@@ -127,7 +309,6 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
const unlisten = await listenGlobalEvents()
|
||||
await sendConfigs()
|
||||
return () => {
|
||||
unlisten()
|
||||
}
|
||||
@@ -140,9 +321,24 @@ onMounted(async () => {
|
||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||
<About />
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="modeDialogVisible" modal :header="t('mode.switch_mode')" :style="{ width: '50vw' }">
|
||||
<ModeSwitcher v-model="editingMode" @uninstall-service="onUninstallService" @stop-service="onStopService" />
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="modeDialogVisible = false" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onModeSave" autofocus :loading="isModeSaving" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
|
||||
|
||||
<RemoteManagement class="flex-1 overflow-y-auto" :api="remoteClient" v-bind:instance-id="instanceId" />
|
||||
<RemoteManagement v-if="clientRunning" class="flex-1 overflow-y-auto" :api="remoteClient"
|
||||
:pause-auto-refresh="isModeSaving" v-bind:instance-id="instanceId" />
|
||||
<div v-else class="empty-state flex-1 flex flex-col items-center py-12">
|
||||
<i class="pi pi-server text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('client.not_running') }}
|
||||
</div>
|
||||
<Button @click="reconnectClient" :loading="isModeSaving" :label="t('client.retry')" icon="pi pi-replay"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
|
||||
<Menubar :model="setting_menu_items" breakpoint="560px">
|
||||
<template #item="{ item, props }">
|
||||
|
||||
Reference in New Issue
Block a user