diff --git a/web/src/components/ui/avatar/Avatar.vue b/web/src/components/ui/avatar/Avatar.vue
deleted file mode 100644
index bb7e669f..00000000
--- a/web/src/components/ui/avatar/Avatar.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/avatar/AvatarFallback.vue b/web/src/components/ui/avatar/AvatarFallback.vue
deleted file mode 100644
index 16b588ae..00000000
--- a/web/src/components/ui/avatar/AvatarFallback.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/avatar/AvatarImage.vue b/web/src/components/ui/avatar/AvatarImage.vue
deleted file mode 100644
index 24a81664..00000000
--- a/web/src/components/ui/avatar/AvatarImage.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/avatar/index.ts b/web/src/components/ui/avatar/index.ts
deleted file mode 100644
index cf0e0031..00000000
--- a/web/src/components/ui/avatar/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as Avatar } from "./Avatar.vue"
-export { default as AvatarFallback } from "./AvatarFallback.vue"
-export { default as AvatarImage } from "./AvatarImage.vue"
diff --git a/web/src/components/ui/card/CardAction.vue b/web/src/components/ui/card/CardAction.vue
deleted file mode 100644
index c91638b6..00000000
--- a/web/src/components/ui/card/CardAction.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/card/index.ts b/web/src/components/ui/card/index.ts
index 16277580..e5c7cb24 100644
--- a/web/src/components/ui/card/index.ts
+++ b/web/src/components/ui/card/index.ts
@@ -1,5 +1,4 @@
export { default as Card } from "./Card.vue"
-export { default as CardAction } from "./CardAction.vue"
export { default as CardContent } from "./CardContent.vue"
export { default as CardDescription } from "./CardDescription.vue"
export { default as CardFooter } from "./CardFooter.vue"
diff --git a/web/src/components/ui/dialog/DialogScrollContent.vue b/web/src/components/ui/dialog/DialogScrollContent.vue
deleted file mode 100644
index d56eaa12..00000000
--- a/web/src/components/ui/dialog/DialogScrollContent.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
- {
- const originalEvent = event.detail.originalEvent;
- const target = originalEvent.target as HTMLElement;
- if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
- event.preventDefault();
- }
- }"
- >
-
-
-
-
- Close
-
-
-
-
-
diff --git a/web/src/components/ui/dialog/DialogTrigger.vue b/web/src/components/ui/dialog/DialogTrigger.vue
deleted file mode 100644
index 49667e99..00000000
--- a/web/src/components/ui/dialog/DialogTrigger.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dialog/index.ts b/web/src/components/ui/dialog/index.ts
index 6768b090..f83f37c5 100644
--- a/web/src/components/ui/dialog/index.ts
+++ b/web/src/components/ui/dialog/index.ts
@@ -5,6 +5,4 @@ export { default as DialogDescription } from "./DialogDescription.vue"
export { default as DialogFooter } from "./DialogFooter.vue"
export { default as DialogHeader } from "./DialogHeader.vue"
export { default as DialogOverlay } from "./DialogOverlay.vue"
-export { default as DialogScrollContent } from "./DialogScrollContent.vue"
export { default as DialogTitle } from "./DialogTitle.vue"
-export { default as DialogTrigger } from "./DialogTrigger.vue"
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
deleted file mode 100644
index 25692d39..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
deleted file mode 100644
index da634ec0..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
deleted file mode 100644
index 8bca83c4..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
deleted file mode 100644
index fe82cadd..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
deleted file mode 100644
index db2c5b89..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
deleted file mode 100644
index 60be75cc..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue
deleted file mode 100644
index 7472e77f..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
deleted file mode 100644
index 40a8422a..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
deleted file mode 100644
index c470c3f6..00000000
--- a/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/dropdown-menu/index.ts b/web/src/components/ui/dropdown-menu/index.ts
index 955fe3aa..f88046b8 100644
--- a/web/src/components/ui/dropdown-menu/index.ts
+++ b/web/src/components/ui/dropdown-menu/index.ts
@@ -1,16 +1,7 @@
export { default as DropdownMenu } from "./DropdownMenu.vue"
-export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
-export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
-export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
-export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
-export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
-export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
-export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
-export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
-export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
export { DropdownMenuPortal } from "reka-ui"
diff --git a/web/src/components/ui/native-select/NativeSelect.vue b/web/src/components/ui/native-select/NativeSelect.vue
deleted file mode 100644
index 7e0fdb9d..00000000
--- a/web/src/components/ui/native-select/NativeSelect.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/native-select/NativeSelectOption.vue b/web/src/components/ui/native-select/NativeSelectOption.vue
deleted file mode 100644
index 5fe52c6a..00000000
--- a/web/src/components/ui/native-select/NativeSelectOption.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/web/src/components/ui/native-select/index.ts b/web/src/components/ui/native-select/index.ts
deleted file mode 100644
index cf632e5f..00000000
--- a/web/src/components/ui/native-select/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as NativeSelect } from "./NativeSelect.vue"
-export { default as NativeSelectOption } from "./NativeSelectOption.vue"
diff --git a/web/src/components/ui/select/SelectGroup.vue b/web/src/components/ui/select/SelectGroup.vue
deleted file mode 100644
index e981c6c5..00000000
--- a/web/src/components/ui/select/SelectGroup.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/select/SelectItemText.vue b/web/src/components/ui/select/SelectItemText.vue
deleted file mode 100644
index b6700b17..00000000
--- a/web/src/components/ui/select/SelectItemText.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/select/SelectLabel.vue b/web/src/components/ui/select/SelectLabel.vue
deleted file mode 100644
index 5b6650cb..00000000
--- a/web/src/components/ui/select/SelectLabel.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/select/SelectSeparator.vue b/web/src/components/ui/select/SelectSeparator.vue
deleted file mode 100644
index 4b5c8857..00000000
--- a/web/src/components/ui/select/SelectSeparator.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
diff --git a/web/src/components/ui/select/index.ts b/web/src/components/ui/select/index.ts
index 96eae60c..611f838f 100644
--- a/web/src/components/ui/select/index.ts
+++ b/web/src/components/ui/select/index.ts
@@ -1,11 +1,7 @@
export { default as Select } from "./Select.vue"
export { default as SelectContent } from "./SelectContent.vue"
-export { default as SelectGroup } from "./SelectGroup.vue"
export { default as SelectItem } from "./SelectItem.vue"
-export { default as SelectItemText } from "./SelectItemText.vue"
-export { default as SelectLabel } from "./SelectLabel.vue"
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"
-export { default as SelectSeparator } from "./SelectSeparator.vue"
export { default as SelectTrigger } from "./SelectTrigger.vue"
export { default as SelectValue } from "./SelectValue.vue"
diff --git a/web/src/components/ui/toggle/Toggle.vue b/web/src/components/ui/toggle/Toggle.vue
deleted file mode 100644
index aea8e17d..00000000
--- a/web/src/components/ui/toggle/Toggle.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/components/ui/toggle/index.ts b/web/src/components/ui/toggle/index.ts
index 7bf08dce..466c8fd2 100644
--- a/web/src/components/ui/toggle/index.ts
+++ b/web/src/components/ui/toggle/index.ts
@@ -1,7 +1,6 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
-export { default as Toggle } from "./Toggle.vue"
export const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
diff --git a/web/src/composables/settings/useRemoteAccessSettings.ts b/web/src/composables/settings/useRemoteAccessSettings.ts
new file mode 100644
index 00000000..9f68159b
--- /dev/null
+++ b/web/src/composables/settings/useRemoteAccessSettings.ts
@@ -0,0 +1,433 @@
+import { computed, onScopeDispose, ref, type Ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { toast } from 'vue-sonner'
+import {
+ rtspConfigApi, rustdeskConfigApi, vncConfigApi,
+ type RustDeskConfigResponse, type RustDeskStatusResponse, type RustDeskPasswordResponse,
+ type RtspStatusResponse, type RtspConfigUpdate, type VncConfigUpdate, type VncStatusResponse,
+} from '@/api'
+import { useConfigStore } from '@/stores/config'
+import { useClipboard } from '@/composables/useClipboard'
+import { formatHostForUrl } from '@/lib/url'
+
+type RemoteService = 'rtsp' | 'vnc' | 'rustdesk'
+
+interface RemoteAccessSettingsOptions {
+ loading: Ref
+ showValidationError: (message: string) => boolean
+}
+
+export function useRemoteAccessSettings({ loading, showValidationError }: RemoteAccessSettingsOptions) {
+ const { t } = useI18n()
+ const configStore = useConfigStore()
+ const { copy: clipboardCopy } = useClipboard()
+ const serviceSaving = ref(null)
+ let copiedTimer: ReturnType | undefined
+ onScopeDispose(() => clearTimeout(copiedTimer))
+
+ const rustdeskConfig = ref(null)
+ const rustdeskStatus = ref(null)
+ const rustdeskPassword = ref(null)
+ const rustdeskLoading = ref(false)
+ const rustdeskCopied = ref<'id' | 'password' | null>(null)
+ const rustdeskLocalConfig = ref({
+ enabled: false,
+ mode: 'id' as 'id' | 'direct_ip',
+ codec: 'h264' as 'h264' | 'h265',
+ direct_access_port: 21118,
+ rendezvous_server: '',
+ relay_server: '',
+ relay_key: '',
+ })
+
+ const rustdeskValidationMessage = computed(() => {
+ if (
+ rustdeskLocalConfig.value.mode === 'id' &&
+ !rustdeskLocalConfig.value.rendezvous_server?.trim()
+ ) {
+ return t('extensions.rustdesk.rendezvousServerRequired')
+ }
+ if (
+ rustdeskLocalConfig.value.mode === 'direct_ip' &&
+ (rustdeskLocalConfig.value.direct_access_port < 1 ||
+ rustdeskLocalConfig.value.direct_access_port > 65535)
+ ) {
+ return t('extensions.rustdesk.directAccessPortInvalid')
+ }
+ return ''
+ })
+
+ const rtspStatus = ref(null)
+ const rtspLoading = ref(false)
+ const rtspLocalConfig = ref({
+ enabled: false,
+ bind: '0.0.0.0',
+ port: 8554,
+ path: 'live',
+ allow_one_client: true,
+ codec: 'h264',
+ username: '',
+ password: '',
+ })
+
+ const vncStatus = ref(null)
+ const vncLoading = ref(false)
+ const vncLocalConfig = ref({
+ enabled: false,
+ bind: '0.0.0.0',
+ port: 5900,
+ encoding: 'tight_jpeg',
+ allow_one_client: true,
+ password: '',
+ })
+
+ const rtspStreamUrl = computed(() => {
+ const host = formatHostForUrl(window.location.hostname || '127.0.0.1')
+ const path = normalizeRtspPath(rtspLocalConfig.value.path || 'live')
+ const port = Number(rtspLocalConfig.value.port) || 8554
+ return `rtsp://${host}:${port}/${path}`
+ })
+
+ const vncStreamUrl = computed(() => {
+ const host = formatHostForUrl(window.location.hostname || '127.0.0.1')
+ const port = Number(vncLocalConfig.value.port) || 5900
+ return `${host}:${port}`
+ })
+
+ function applyRustdeskStatus(status: RustDeskStatusResponse) {
+ const config = status.config
+ rustdeskConfig.value = config
+ rustdeskStatus.value = status
+ rustdeskLocalConfig.value = {
+ enabled: config.enabled,
+ mode: config.mode || 'id',
+ codec: config.codec || 'h264',
+ direct_access_port: config.direct_access_port || 21118,
+ rendezvous_server: config.rendezvous_server,
+ relay_server: config.relay_server || '',
+ relay_key: config.relay_key || '',
+ }
+ }
+
+ async function loadRustdeskConfig() {
+ rustdeskLoading.value = true
+ try {
+ const status = await configStore.refreshRustdeskStatus()
+ applyRustdeskStatus(status)
+ } catch {
+ } finally {
+ rustdeskLoading.value = false
+ }
+ }
+
+ async function loadRustdeskPassword() {
+ try {
+ rustdeskPassword.value = await configStore.refreshRustdeskPassword()
+ } catch {}
+ }
+
+ function normalizeRustdeskServer(value: string, defaultPort: number): string {
+ const trimmed = value.trim()
+ if (!trimmed) return ''
+ if (trimmed.includes(':')) return trimmed
+ return `${trimmed}:${defaultPort}`
+ }
+
+ /** Strip line breaks from pasted keys. */
+ function normalizeRustdeskRelayKey(value: string): string {
+ return value.replace(/\r?\n/g, '').trim()
+ }
+
+ function validateRustdeskConfig(): boolean {
+ return !rustdeskValidationMessage.value || showValidationError(rustdeskValidationMessage.value)
+ }
+
+ function validateVncConfig(enabled = vncLocalConfig.value.enabled): boolean {
+ const password = (vncLocalConfig.value.password || '').trim()
+ if (enabled && !vncStatus.value?.config.has_password && !password) {
+ return showValidationError(t('extensions.vnc.passwordRequired'))
+ }
+ if (password.length > 8) {
+ return showValidationError(t('extensions.vnc.passwordMaxLength'))
+ }
+ return true
+ }
+
+ function normalizeRtspPath(path: string): string {
+ return path.trim().replace(/^\/+|\/+$/g, '') || 'live'
+ }
+
+ function rustdeskUpdatePayload(enabled = rustdeskLocalConfig.value.enabled) {
+ return {
+ enabled,
+ mode: rustdeskLocalConfig.value.mode,
+ codec: rustdeskLocalConfig.value.codec,
+ direct_access_port: rustdeskLocalConfig.value.direct_access_port,
+ rendezvous_server: normalizeRustdeskServer(rustdeskLocalConfig.value.rendezvous_server, 21116),
+ relay_server: normalizeRustdeskServer(rustdeskLocalConfig.value.relay_server, 21117),
+ relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key),
+ }
+ }
+
+ async function saveService(
+ service: RemoteService,
+ persist: () => Promise,
+ reload: () => Promise,
+ ) {
+ serviceSaving.value = service
+ loading.value = true
+ try {
+ await persist()
+ await reload()
+ toast.success(t('settingsUi.saveDone'))
+ } catch {
+ // API requests report their own errors.
+ } finally {
+ loading.value = false
+ serviceSaving.value = null
+ }
+ }
+
+ async function saveRustdeskConfig() {
+ if (
+ (rustdeskLocalConfig.value.enabled || rustdeskLocalConfig.value.mode === 'direct_ip') &&
+ !validateRustdeskConfig()
+ )
+ return
+
+ await saveService(
+ 'rustdesk', () => configStore.updateRustdesk(rustdeskUpdatePayload()), loadRustdeskConfig,
+ )
+ }
+
+ async function regenerateRustdeskId() {
+ if (!confirm(t('extensions.rustdesk.confirmRegenerateId'))) return
+ rustdeskLoading.value = true
+ try {
+ await configStore.regenerateRustdeskId()
+ await loadRustdeskConfig()
+ await loadRustdeskPassword()
+ } catch {
+ } finally {
+ rustdeskLoading.value = false
+ }
+ }
+
+ async function regenerateRustdeskPassword() {
+ if (!confirm(t('extensions.rustdesk.confirmRegeneratePassword'))) return
+ rustdeskLoading.value = true
+ try {
+ await configStore.regenerateRustdeskPassword()
+ await loadRustdeskConfig()
+ await loadRustdeskPassword()
+ } catch {
+ } finally {
+ rustdeskLoading.value = false
+ }
+ }
+
+ async function startRustdesk() {
+ if (!validateRustdeskConfig()) return
+
+ rustdeskLoading.value = true
+ try {
+ await configStore.updateRustdesk(rustdeskUpdatePayload())
+ const status = await rustdeskConfigApi.start()
+ applyRustdeskStatus(status)
+ } catch {
+ } finally {
+ rustdeskLoading.value = false
+ }
+ }
+
+ async function stopRustdesk() {
+ rustdeskLoading.value = true
+ try {
+ const status = await rustdeskConfigApi.stop()
+ applyRustdeskStatus(status)
+ } catch {
+ } finally {
+ rustdeskLoading.value = false
+ }
+ }
+
+ async function copyToClipboard(text: string, type: 'id' | 'password') {
+ const success = await clipboardCopy(text)
+ if (success) {
+ rustdeskCopied.value = type
+ clearTimeout(copiedTimer)
+ copiedTimer = setTimeout(() => (rustdeskCopied.value = null), 2000)
+ }
+ }
+
+ function applyRtspStatus(status: RtspStatusResponse) {
+ rtspStatus.value = status
+ rtspLocalConfig.value = {
+ enabled: status.config.enabled,
+ bind: status.config.bind,
+ port: status.config.port,
+ path: status.config.path,
+ allow_one_client: status.config.allow_one_client,
+ codec: status.config.codec,
+ username: status.config.username || '',
+ password: status.config.password || '',
+ }
+ }
+
+ async function loadRtspConfig() {
+ rtspLoading.value = true
+ try {
+ const status = await configStore.refreshRtspStatus()
+ applyRtspStatus(status)
+ } catch {
+ } finally {
+ rtspLoading.value = false
+ }
+ }
+
+ function rtspUpdatePayload(enabled = !!rtspLocalConfig.value.enabled): RtspConfigUpdate {
+ return {
+ enabled,
+ bind: rtspLocalConfig.value.bind?.trim() || '0.0.0.0',
+ port: Number(rtspLocalConfig.value.port) || 8554,
+ path: normalizeRtspPath(rtspLocalConfig.value.path || 'live'),
+ allow_one_client: !!rtspLocalConfig.value.allow_one_client,
+ codec: rtspLocalConfig.value.codec || 'h264',
+ username: (rtspLocalConfig.value.username || '').trim(),
+ password: (rtspLocalConfig.value.password || '').trim(),
+ }
+ }
+
+ async function saveRtspConfig() {
+ await saveService(
+ 'rtsp', () => configStore.updateRtsp(rtspUpdatePayload()), loadRtspConfig,
+ )
+ }
+
+ async function startRtsp() {
+ rtspLoading.value = true
+ try {
+ await configStore.updateRtsp(rtspUpdatePayload())
+ const status = await rtspConfigApi.start()
+ applyRtspStatus(status)
+ } catch {
+ } finally {
+ rtspLoading.value = false
+ }
+ }
+
+ async function stopRtsp() {
+ rtspLoading.value = true
+ try {
+ const status = await rtspConfigApi.stop()
+ applyRtspStatus(status)
+ } catch {
+ } finally {
+ rtspLoading.value = false
+ }
+ }
+
+ function applyVncStatus(status: VncStatusResponse) {
+ vncStatus.value = status
+ vncLocalConfig.value = {
+ enabled: status.config.enabled,
+ bind: status.config.bind,
+ port: status.config.port,
+ encoding: status.config.encoding,
+ allow_one_client: status.config.allow_one_client,
+ password: '',
+ }
+ }
+
+ async function loadVncConfig() {
+ vncLoading.value = true
+ try {
+ const status = await configStore.refreshVncStatus()
+ applyVncStatus(status)
+ } catch {
+ } finally {
+ vncLoading.value = false
+ }
+ }
+
+ function vncUpdatePayload(enabled = !!vncLocalConfig.value.enabled): VncConfigUpdate {
+ const update: VncConfigUpdate = {
+ enabled,
+ bind: vncLocalConfig.value.bind?.trim() || '0.0.0.0',
+ port: Number(vncLocalConfig.value.port) || 5900,
+ encoding: vncLocalConfig.value.encoding || 'tight_jpeg',
+ allow_one_client: !!vncLocalConfig.value.allow_one_client,
+ }
+ const password = (vncLocalConfig.value.password || '').trim()
+ if (password) update.password = password
+ return update
+ }
+
+ async function saveVncConfig() {
+ if (!validateVncConfig()) return
+
+ await saveService(
+ 'vnc', () => configStore.updateVnc(vncUpdatePayload()), loadVncConfig,
+ )
+ }
+
+ async function startVnc() {
+ if (!validateVncConfig(true)) return
+
+ vncLoading.value = true
+ try {
+ await configStore.updateVnc(vncUpdatePayload())
+ const status = await vncConfigApi.start()
+ applyVncStatus(status)
+ } catch {
+ } finally {
+ vncLoading.value = false
+ }
+ }
+
+ async function stopVnc() {
+ vncLoading.value = true
+ try {
+ const status = await vncConfigApi.stop()
+ applyVncStatus(status)
+ } catch {
+ } finally {
+ vncLoading.value = false
+ }
+ }
+
+ return {
+ rustdeskConfig,
+ rustdeskStatus,
+ rustdeskPassword,
+ rustdeskLoading,
+ rustdeskCopied,
+ rustdeskLocalConfig,
+ rustdeskValidationMessage,
+ rtspStatus,
+ rtspLoading,
+ rtspLocalConfig,
+ vncStatus,
+ vncLoading,
+ vncLocalConfig,
+ rtspStreamUrl,
+ vncStreamUrl,
+ serviceSaving,
+ loadRustdeskConfig,
+ loadRustdeskPassword,
+ saveRustdeskConfig,
+ regenerateRustdeskId,
+ regenerateRustdeskPassword,
+ startRustdesk,
+ stopRustdesk,
+ copyToClipboard,
+ loadRtspConfig,
+ saveRtspConfig,
+ startRtsp,
+ stopRtsp,
+ loadVncConfig,
+ saveVncConfig,
+ startVnc,
+ stopVnc,
+ }
+}
diff --git a/web/src/composables/useConfigPopover.ts b/web/src/composables/useConfigPopover.ts
deleted file mode 100644
index bbfc3c0a..00000000
--- a/web/src/composables/useConfigPopover.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-
-import { ref, watch, type Ref } from 'vue'
-
-export interface UseConfigPopoverOptions {
- /** Reactive open state from props */
- open: Ref
- /** Load device list callback */
- loadDevices?: () => Promise
- /** Initialize from current config callback */
- initializeFromCurrent?: () => void
-}
-
-export function useConfigPopover(options: UseConfigPopoverOptions) {
- const applying = ref(false)
- const loadingDevices = ref(false)
-
- watch(options.open, async (isOpen) => {
- if (isOpen) {
- options.initializeFromCurrent?.()
- if (options.loadDevices) {
- loadingDevices.value = true
- try {
- await options.loadDevices()
- } finally {
- loadingDevices.value = false
- }
- }
- }
- })
-
- async function applyConfig(applyFn: () => Promise) {
- applying.value = true
- try {
- await applyFn()
- } catch (e) {
- console.info('[ConfigPopover] Apply failed:', e)
- } finally {
- applying.value = false
- }
- }
-
- // Refresh devices
- async function refreshDevices() {
- if (!options.loadDevices) return
- loadingDevices.value = true
- try {
- await options.loadDevices()
- } finally {
- loadingDevices.value = false
- }
- }
-
- return {
- applying,
- loadingDevices,
-
- applyConfig,
- refreshDevices,
- }
-}
diff --git a/web/src/composables/useConsoleEvents.ts b/web/src/composables/useConsoleEvents.ts
index 6685c250..37804a4e 100644
--- a/web/src/composables/useConsoleEvents.ts
+++ b/web/src/composables/useConsoleEvents.ts
@@ -1,6 +1,6 @@
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
-import { useSystemStore } from '@/stores/system'
+import { useSystemStore, type DeviceInfoEvent } from '@/stores/system'
import type { StreamDeviceLostEventData, StreamStateChangedEventData } from '@/types/websocket'
import { useWebSocket } from '@/composables/useWebSocket'
import { isAudioStreamDeviceLostPayload } from '@/lib/streamSignal'
@@ -17,14 +17,14 @@ export interface ConsoleEventHandlers {
onStreamDeviceLost?: (data: StreamDeviceLostEventData) => void
onStreamReconnecting?: (data: { device: string; attempt: number }) => void
onStreamRecovered?: (data: { device: string }) => void
- onDeviceInfo?: (data: any) => void
+ onDeviceInfo?: (data: DeviceInfoEvent) => void
}
export function useConsoleEvents(handlers: ConsoleEventHandlers) {
const { t } = useI18n()
const systemStore = useSystemStore()
const { on, off, connect } = useWebSocket()
- const noop = () => {}
+ let subscribed = false
function handleStreamDeviceLost(data: StreamDeviceLostEventData) {
const audioLost = isAudioStreamDeviceLostPayload(data)
@@ -41,53 +41,44 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
handlers.onStreamDeviceLost?.(data)
}
- function handleStreamReconnecting(data: { device: string; attempt: number }) {
- handlers.onStreamReconnecting?.(data)
- }
-
- function handleStreamRecovered(_data: { device: string }) {
+ function handleStreamRecovered(data: { device: string }) {
if (systemStore.stream) {
systemStore.stream.online = true
}
- handlers.onStreamRecovered?.(_data)
+ handlers.onStreamRecovered?.(data)
}
- function handleStreamStateChangedForward(data: StreamStateChangedEventData) {
- handlers.onStreamStateChanged?.(data)
- }
+ // Keep registration and cleanup together so every subscription is removed.
+ const subscriptions = [
+ ['stream.config_changing', handlers.onStreamConfigChanging],
+ ['stream.config_applied', handlers.onStreamConfigApplied],
+ ['stream.stats_update', handlers.onStreamStatsUpdate],
+ ['stream.mode_changed', handlers.onStreamModeChanged],
+ ['stream.mode_switching', handlers.onStreamModeSwitching],
+ ['stream.mode_ready', handlers.onStreamModeReady],
+ ['stream.webrtc_ready', handlers.onWebRTCReady],
+ ['stream.state_changed', handlers.onStreamStateChanged],
+ ['stream.device_lost', handleStreamDeviceLost],
+ ['stream.reconnecting', handlers.onStreamReconnecting],
+ ['stream.recovered', handleStreamRecovered],
+ ['system.device_info', handlers.onDeviceInfo],
+ ] as const
function subscribe() {
- on('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
- on('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
- on('stream.stats_update', handlers.onStreamStatsUpdate ?? noop)
- on('stream.mode_changed', handlers.onStreamModeChanged ?? noop)
- on('stream.mode_switching', handlers.onStreamModeSwitching ?? noop)
- on('stream.mode_ready', handlers.onStreamModeReady ?? noop)
- on('stream.webrtc_ready', handlers.onWebRTCReady ?? noop)
- on('stream.state_changed', handleStreamStateChangedForward)
- on('stream.device_lost', handleStreamDeviceLost)
- on('stream.reconnecting', handleStreamReconnecting)
- on('stream.recovered', handleStreamRecovered)
-
- on('system.device_info', handlers.onDeviceInfo ?? noop)
-
+ if (subscribed) return
+ subscribed = true
+ for (const [event, handler] of subscriptions) {
+ if (handler) on(event, handler)
+ }
connect()
}
function unsubscribe() {
- off('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
- off('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
- off('stream.stats_update', handlers.onStreamStatsUpdate ?? noop)
- off('stream.mode_changed', handlers.onStreamModeChanged ?? noop)
- off('stream.mode_switching', handlers.onStreamModeSwitching ?? noop)
- off('stream.mode_ready', handlers.onStreamModeReady ?? noop)
- off('stream.webrtc_ready', handlers.onWebRTCReady ?? noop)
- off('stream.state_changed', handleStreamStateChangedForward)
- off('stream.device_lost', handleStreamDeviceLost)
- off('stream.reconnecting', handleStreamReconnecting)
- off('stream.recovered', handleStreamRecovered)
-
- off('system.device_info', handlers.onDeviceInfo ?? noop)
+ if (!subscribed) return
+ subscribed = false
+ for (const [event, handler] of subscriptions) {
+ if (handler) off(event, handler)
+ }
}
return {
diff --git a/web/src/composables/useUnifiedAudio.ts b/web/src/composables/useUnifiedAudio.ts
index b9b33012..898d4883 100644
--- a/web/src/composables/useUnifiedAudio.ts
+++ b/web/src/composables/useUnifiedAudio.ts
@@ -1,20 +1,11 @@
// Manages audio playback across different video modes (MJPEG/WebSocket and H264/WebRTC)
// Provides a single interface for volume control and audio source switching
-import { ref, watch, type Ref } from 'vue'
+import { ref, watch } from 'vue'
import { getAudioPlayer } from './useAudioPlayer'
export type AudioMode = 'ws' | 'webrtc'
-export interface UnifiedAudioState {
- audioMode: Ref
- volume: Ref
- muted: Ref
- connected: Ref
- playing: Ref
- error: Ref
-}
-
export function useUnifiedAudio() {
const audioMode = ref('ws')
const volume = ref(0) // 0-1, default muted (browser autoplay policy)
diff --git a/web/src/composables/useVideoStream.ts b/web/src/composables/useVideoStream.ts
deleted file mode 100644
index 6c36933f..00000000
--- a/web/src/composables/useVideoStream.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-// Legacy MJPEG-only streaming composable.
-// Deprecated: Console now uses useVideoSession for all switching/connection logic.
-
-import { ref, computed, type Ref } from 'vue'
-import { useSystemStore } from '@/stores/system'
-import { streamApi } from '@/api'
-import { generateUUID } from '@/lib/utils'
-
-export type VideoMode = 'mjpeg'
-
-export interface VideoStreamState {
- mode: Ref
- loading: Ref
- error: Ref
- errorMessage: Ref
- restarting: Ref
- fps: Ref
- mjpegUrl: Ref
- clientId: string
-}
-
-const BASE_RETRY_DELAY = 2000
-const GRACE_PERIOD = 2000
-const MAX_CONSECUTIVE_ERRORS = 2
-
-/** @deprecated Use useVideoSession + ConsoleView instead. */
-export function useVideoStream() {
- const systemStore = useSystemStore()
-
- const videoMode = ref('mjpeg')
- const videoLoading = ref(true)
- const videoError = ref(false)
- const videoErrorMessage = ref('')
- const videoRestarting = ref(false)
- const backendFps = ref(0)
- const mjpegTimestamp = ref(0)
- const clientId = generateUUID()
- const clientsStats = ref>({})
-
- let retryTimeoutId: number | null = null
- let retryCount = 0
- let gracePeriodTimeoutId: number | null = null
- let consecutiveErrors = 0
- let isRefreshingVideo = false
- let initialDeviceInfoReceived = false
-
- const mjpegUrl = computed(() => {
- if (videoMode.value !== 'mjpeg') return ''
- if (mjpegTimestamp.value === 0) return ''
- return `${streamApi.getMjpegUrl(clientId)}&t=${mjpegTimestamp.value}`
- })
-
- function refreshVideo() {
- backendFps.value = 0
- videoError.value = false
- videoErrorMessage.value = ''
- isRefreshingVideo = true
- videoLoading.value = true
- mjpegTimestamp.value = Date.now()
-
- setTimeout(() => {
- isRefreshingVideo = false
- if (videoLoading.value) {
- videoLoading.value = false
- }
- }, 1500)
- }
-
- function handleVideoLoad() {
- if (videoMode.value === 'mjpeg') {
- systemStore.setStreamOnline(true)
- }
-
- if (!videoLoading.value) return
-
- clearRetryTimers()
- videoLoading.value = false
- videoError.value = false
- videoErrorMessage.value = ''
- videoRestarting.value = false
- retryCount = 0
- consecutiveErrors = 0
- }
-
- function handleVideoError() {
- if (videoMode.value !== 'mjpeg') return
- if (isRefreshingVideo) return
-
- consecutiveErrors += 1
-
- if (consecutiveErrors > MAX_CONSECUTIVE_ERRORS && gracePeriodTimeoutId !== null) {
- clearTimeout(gracePeriodTimeoutId)
- gracePeriodTimeoutId = null
- videoRestarting.value = false
- }
-
- if (videoRestarting.value || gracePeriodTimeoutId !== null) return
-
- if (retryTimeoutId !== null) {
- clearTimeout(retryTimeoutId)
- retryTimeoutId = null
- }
-
- videoLoading.value = true
- retryCount += 1
- const delay = BASE_RETRY_DELAY * Math.pow(1.5, Math.min(retryCount - 1, 5))
-
- retryTimeoutId = window.setTimeout(() => {
- retryTimeoutId = null
- refreshVideo()
- }, delay)
- }
-
- function clearRetryTimers() {
- if (retryTimeoutId !== null) {
- clearTimeout(retryTimeoutId)
- retryTimeoutId = null
- }
- if (gracePeriodTimeoutId !== null) {
- clearTimeout(gracePeriodTimeoutId)
- gracePeriodTimeoutId = null
- }
- }
-
- function handleStreamConfigChanging() {
- clearRetryTimers()
- videoRestarting.value = true
- videoLoading.value = true
- videoError.value = false
- retryCount = 0
- consecutiveErrors = 0
- backendFps.value = 0
- }
-
- function handleStreamConfigApplied() {
- consecutiveErrors = 0
-
- gracePeriodTimeoutId = window.setTimeout(() => {
- gracePeriodTimeoutId = null
- consecutiveErrors = 0
- }, GRACE_PERIOD)
-
- videoRestarting.value = false
- refreshVideo()
- }
-
- function handleStreamStatsUpdate(data: { clients?: number; clients_stat?: Record }) {
- if (typeof data.clients === 'number') {
- systemStore.updateStreamClients(data.clients)
- }
-
- if (data.clients_stat) {
- clientsStats.value = data.clients_stat as any
- const myStats = data.clients_stat[clientId]
- if (myStats) {
- backendFps.value = myStats.fps || 0
- } else {
- const fpsList = Object.values(data.clients_stat)
- .map((s) => s?.fps || 0)
- .filter(f => f > 0)
- backendFps.value = fpsList.length > 0 ? Math.min(...fpsList) : 0
- }
- } else {
- backendFps.value = 0
- }
- }
-
- function handleDeviceInfo(data: any) {
- systemStore.updateFromDeviceInfo(data)
-
- if (data.video?.config_changing) return
-
- if (!initialDeviceInfoReceived) {
- initialDeviceInfoReceived = true
- if (data.video?.stream_mode === 'mjpeg') {
- setTimeout(() => refreshVideo(), 100)
- }
- }
- }
-
- function handleModeChange(mode: VideoMode) {
- if (mode !== 'mjpeg') return
- if (mode === videoMode.value) return
- videoMode.value = mode
- localStorage.setItem('videoMode', mode)
- refreshVideo()
- }
-
- function cleanup() {
- clearRetryTimers()
- }
-
- const state: VideoStreamState = {
- mode: videoMode,
- loading: videoLoading,
- error: videoError,
- errorMessage: videoErrorMessage,
- restarting: videoRestarting,
- fps: backendFps,
- mjpegUrl,
- clientId,
- }
-
- return {
- state,
- clientsStats,
- refreshVideo,
- handleVideoLoad,
- handleVideoError,
- handleStreamConfigChanging,
- handleStreamConfigApplied,
- handleStreamStatsUpdate,
- handleDeviceInfo,
- handleModeChange,
- cleanup,
- }
-}
diff --git a/web/src/lib/configResource.ts b/web/src/lib/configResource.ts
new file mode 100644
index 00000000..c6b7ca81
--- /dev/null
+++ b/web/src/lib/configResource.ts
@@ -0,0 +1,54 @@
+import { ref, type Ref } from 'vue'
+
+/** Shared request state for a service with several independently cached endpoints. */
+export function createRequestState() {
+ const loading = ref(false)
+ const error = ref(null)
+ let pending = 0
+
+ async function run(request: () => Promise): Promise {
+ pending++
+ loading.value = true
+ error.value = null
+ try {
+ return await request()
+ } catch (cause) {
+ error.value = cause instanceof Error ? cause.message
+ : typeof cause === 'string' ? cause : 'Unknown error'
+ throw cause
+ } finally {
+ loading.value = --pending > 0
+ }
+ }
+
+ return { loading, error, run }
+}
+
+/** Cache successful reads and coalesce concurrent refreshes of the same endpoint. */
+export function createConfigResource(
+ fetcher: () => Promise,
+ state = createRequestState(),
+ onLoaded?: (response: T) => void,
+) {
+ const data = ref(null) as Ref
+ let inFlight: Promise | null = null
+
+ function refresh(): Promise {
+ if (inFlight) return inFlight
+ inFlight = state.run(async () => {
+ const response = await fetcher()
+ data.value = response
+ onLoaded?.(response)
+ return response
+ }).finally(() => {
+ inFlight = null
+ })
+ return inFlight
+ }
+
+ function ensure(): Promise {
+ return data.value === null ? refresh() : Promise.resolve(data.value)
+ }
+
+ return { data, loading: state.loading, error: state.error, refresh, ensure }
+}
diff --git a/web/src/lib/url.ts b/web/src/lib/url.ts
new file mode 100644
index 00000000..5df9b8ce
--- /dev/null
+++ b/web/src/lib/url.ts
@@ -0,0 +1,4 @@
+export function formatHostForUrl(hostname: string): string {
+ if (!hostname) return '127.0.0.1'
+ return hostname.includes(':') && !hostname.startsWith('[') ? `[${hostname}]` : hostname
+}
diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts
index 1fe04f59..b1e7a3db 100644
--- a/web/src/stores/auth.ts
+++ b/web/src/stores/auth.ts
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
-import { ref, computed } from 'vue'
+import { ref } from 'vue'
import { authApi, systemApi, type AuthLoginResponse } from '@/api'
export const useAuthStore = defineStore('auth', () => {
@@ -11,8 +11,6 @@ export const useAuthStore = defineStore('auth', () => {
const error = ref(null)
let pendingUsername: string | null = null
- const isLoggedIn = computed(() => isAuthenticated.value && user.value !== null)
-
async function checkSetupStatus() {
try {
const status = await systemApi.setupStatus()
@@ -136,7 +134,6 @@ export const useAuthStore = defineStore('auth', () => {
needsSetup,
loading,
error,
- isLoggedIn,
checkSetupStatus,
checkAuth,
beginLogin,
diff --git a/web/src/stores/config.ts b/web/src/stores/config.ts
index 81a0d6c7..349f792a 100644
--- a/web/src/stores/config.ts
+++ b/web/src/stores/config.ts
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
-import { ref } from 'vue'
+import { createConfigResource, createRequestState } from '@/lib/configResource'
import {
authConfigApi,
atxConfigApi,
@@ -15,488 +15,88 @@ import {
webConfigApi,
} from '@/api'
import type {
- AtxConfig,
AtxConfigUpdate,
- AudioConfig,
AudioConfigUpdate,
- AuthConfig,
AuthConfigUpdate,
- HidConfig,
HidConfigUpdate,
- MsdConfig,
MsdConfigUpdate,
OtgConfigResponse,
OtgConfigUpdate,
- StreamConfigResponse,
StreamConfigUpdate,
- VideoConfig,
VideoConfigUpdate,
} from '@/types/generated'
import type {
- RtspConfigResponse as ApiRtspConfigResponse,
RtspConfigUpdate as ApiRtspConfigUpdate,
- RtspStatusResponse as ApiRtspStatusResponse,
- RustDeskConfigResponse as ApiRustDeskConfigResponse,
RustDeskConfigUpdate as ApiRustDeskConfigUpdate,
- RustDeskStatusResponse as ApiRustDeskStatusResponse,
- RustDeskPasswordResponse as ApiRustDeskPasswordResponse,
- VncConfigResponse as ApiVncConfigResponse,
VncConfigUpdate as ApiVncConfigUpdate,
- VncStatusResponse as ApiVncStatusResponse,
- WebConfig,
WebConfigUpdate,
} from '@/api'
-function normalizeErrorMessage(error: unknown): string {
- if (error instanceof Error) return error.message
- if (typeof error === 'string') return error
- return 'Unknown error'
-}
-
export const useConfigStore = defineStore('config', () => {
- const auth = ref(null)
- const video = ref(null)
- const audio = ref(null)
- const hid = ref(null)
- const msd = ref(null)
- const stream = ref(null)
- const web = ref(null)
- const atx = ref(null)
- const rtspConfig = ref(null)
- const rtspStatus = ref(null)
- const vncConfig = ref(null)
- const vncStatus = ref(null)
- const rustdeskConfig = ref(null)
- const rustdeskStatus = ref(null)
- const rustdeskPassword = ref(null)
+ const {
+ data: auth, loading: authLoading, error: authError,
+ refresh: refreshAuth, ensure: ensureAuth,
+ } = createConfigResource(authConfigApi.get)
+ const {
+ data: video, loading: videoLoading, error: videoError,
+ refresh: refreshVideo, ensure: ensureVideo,
+ } = createConfigResource(videoConfigApi.get)
+ const {
+ data: audio, loading: audioLoading, error: audioError,
+ refresh: refreshAudio, ensure: ensureAudio,
+ } = createConfigResource(audioConfigApi.get)
+ const {
+ data: hid, loading: hidLoading, error: hidError,
+ refresh: refreshHid, ensure: ensureHid,
+ } = createConfigResource(hidConfigApi.get)
+ const {
+ data: msd, loading: msdLoading, error: msdError,
+ refresh: refreshMsd, ensure: ensureMsd,
+ } = createConfigResource(msdConfigApi.get)
+ const {
+ data: stream, loading: streamLoading, error: streamError,
+ refresh: refreshStream, ensure: ensureStream,
+ } = createConfigResource(streamConfigApi.get)
+ const {
+ data: web, loading: webLoading, error: webError,
+ refresh: refreshWeb, ensure: ensureWeb,
+ } = createConfigResource(webConfigApi.get)
+ const {
+ data: atx, loading: atxLoading, error: atxError,
+ refresh: refreshAtx, ensure: ensureAtx,
+ } = createConfigResource(atxConfigApi.get)
- const authLoading = ref(false)
- const videoLoading = ref(false)
- const audioLoading = ref(false)
- const hidLoading = ref(false)
- const msdLoading = ref(false)
- const streamLoading = ref(false)
- const webLoading = ref(false)
- const atxLoading = ref(false)
- const rtspLoading = ref(false)
- const vncLoading = ref(false)
- const rustdeskLoading = ref(false)
+ const rtspRequests = createRequestState()
+ const { loading: rtspLoading, error: rtspError } = rtspRequests
+ const { data: rtspConfig, refresh: refreshRtspConfig, ensure: ensureRtspConfig } =
+ createConfigResource(rtspConfigApi.get, rtspRequests)
+ const { data: rtspStatus, refresh: refreshRtspStatus } = createConfigResource(
+ rtspConfigApi.getStatus, rtspRequests, (response) => {
+ rtspConfig.value = response.config
+ },
+ )
- const authError = ref(null)
- const videoError = ref(null)
- const audioError = ref(null)
- const hidError = ref(null)
- const msdError = ref(null)
- const streamError = ref(null)
- const webError = ref(null)
- const atxError = ref(null)
- const rtspError = ref(null)
- const vncError = ref(null)
- const rustdeskError = ref(null)
+ const vncRequests = createRequestState()
+ const { loading: vncLoading, error: vncError } = vncRequests
+ const { data: vncConfig, refresh: refreshVncConfig, ensure: ensureVncConfig } =
+ createConfigResource(vncConfigApi.get, vncRequests)
+ const { data: vncStatus, refresh: refreshVncStatus } = createConfigResource(
+ vncConfigApi.getStatus, vncRequests, (response) => {
+ vncConfig.value = response.config
+ },
+ )
- let authPromise: Promise | null = null
- let videoPromise: Promise | null = null
- let audioPromise: Promise | null = null
- let hidPromise: Promise | null = null
- let msdPromise: Promise | null = null
- let streamPromise: Promise | null = null
- let webPromise: Promise | null = null
- let atxPromise: Promise | null = null
- let rtspPromise: Promise | null = null
- let rtspStatusPromise: Promise | null = null
- let vncPromise: Promise | null = null
- let vncStatusPromise: Promise | null = null
- let rustdeskPromise: Promise | null = null
- let rustdeskStatusPromise: Promise | null = null
- let rustdeskPasswordPromise: Promise | null = null
-
- async function refreshAuth() {
- if (authLoading.value && authPromise) return authPromise
- authLoading.value = true
- authError.value = null
- const request = authConfigApi.get()
- .then((response) => {
- auth.value = response
- return response
- })
- .catch((error) => {
- authError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- authLoading.value = false
- authPromise = null
- })
-
- authPromise = request
- return request
- }
-
- async function refreshVideo() {
- if (videoLoading.value && videoPromise) return videoPromise
- videoLoading.value = true
- videoError.value = null
- const request = videoConfigApi.get()
- .then((response) => {
- video.value = response
- return response
- })
- .catch((error) => {
- videoError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- videoLoading.value = false
- videoPromise = null
- })
-
- videoPromise = request
- return request
- }
-
- async function refreshAudio() {
- if (audioLoading.value && audioPromise) return audioPromise
- audioLoading.value = true
- audioError.value = null
- const request = audioConfigApi.get()
- .then((response) => {
- audio.value = response
- return response
- })
- .catch((error) => {
- audioError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- audioLoading.value = false
- audioPromise = null
- })
-
- audioPromise = request
- return request
- }
-
- async function refreshHid() {
- if (hidLoading.value && hidPromise) return hidPromise
- hidLoading.value = true
- hidError.value = null
- const request = hidConfigApi.get()
- .then((response) => {
- hid.value = response
- return response
- })
- .catch((error) => {
- hidError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- hidLoading.value = false
- hidPromise = null
- })
-
- hidPromise = request
- return request
- }
-
- async function refreshMsd() {
- if (msdLoading.value && msdPromise) return msdPromise
- msdLoading.value = true
- msdError.value = null
- const request = msdConfigApi.get()
- .then((response) => {
- msd.value = response
- return response
- })
- .catch((error) => {
- msdError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- msdLoading.value = false
- msdPromise = null
- })
-
- msdPromise = request
- return request
- }
-
- async function refreshStream() {
- if (streamLoading.value && streamPromise) return streamPromise
- streamLoading.value = true
- streamError.value = null
- const request = streamConfigApi.get()
- .then((response) => {
- stream.value = response
- return response
- })
- .catch((error) => {
- streamError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- streamLoading.value = false
- streamPromise = null
- })
-
- streamPromise = request
- return request
- }
-
- async function refreshWeb() {
- if (webLoading.value && webPromise) return webPromise
- webLoading.value = true
- webError.value = null
- const request = webConfigApi.get()
- .then((response) => {
- web.value = response
- return response
- })
- .catch((error) => {
- webError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- webLoading.value = false
- webPromise = null
- })
-
- webPromise = request
- return request
- }
-
- async function refreshAtx() {
- if (atxLoading.value && atxPromise) return atxPromise
- atxLoading.value = true
- atxError.value = null
- const request = atxConfigApi.get()
- .then((response) => {
- atx.value = response
- return response
- })
- .catch((error) => {
- atxError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- atxLoading.value = false
- atxPromise = null
- })
-
- atxPromise = request
- return request
- }
-
- async function refreshRtspConfig() {
- if (rtspLoading.value && rtspPromise) return rtspPromise
- rtspLoading.value = true
- rtspError.value = null
- const request = rtspConfigApi.get()
- .then((response) => {
- rtspConfig.value = response
- return response
- })
- .catch((error) => {
- rtspError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- rtspLoading.value = false
- rtspPromise = null
- })
-
- rtspPromise = request
- return request
- }
-
- async function refreshRtspStatus() {
- if (rtspLoading.value && rtspStatusPromise) return rtspStatusPromise
- rtspLoading.value = true
- rtspError.value = null
- const request = rtspConfigApi.getStatus()
- .then((response) => {
- rtspStatus.value = response
- rtspConfig.value = response.config
- return response
- })
- .catch((error) => {
- rtspError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- rtspLoading.value = false
- rtspStatusPromise = null
- })
-
- rtspStatusPromise = request
- return request
- }
-
- async function refreshVncConfig() {
- if (vncLoading.value && vncPromise) return vncPromise
- vncLoading.value = true
- vncError.value = null
- const request = vncConfigApi.get()
- .then((response) => {
- vncConfig.value = response
- return response
- })
- .catch((error) => {
- vncError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- vncLoading.value = false
- vncPromise = null
- })
-
- vncPromise = request
- return request
- }
-
- async function refreshVncStatus() {
- if (vncLoading.value && vncStatusPromise) return vncStatusPromise
- vncLoading.value = true
- vncError.value = null
- const request = vncConfigApi.getStatus()
- .then((response) => {
- vncStatus.value = response
- vncConfig.value = response.config
- return response
- })
- .catch((error) => {
- vncError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- vncLoading.value = false
- vncStatusPromise = null
- })
-
- vncStatusPromise = request
- return request
- }
-
- async function refreshRustdeskConfig() {
- if (rustdeskLoading.value && rustdeskPromise) return rustdeskPromise
- rustdeskLoading.value = true
- rustdeskError.value = null
- const request = rustdeskConfigApi.get()
- .then((response) => {
- rustdeskConfig.value = response
- return response
- })
- .catch((error) => {
- rustdeskError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- rustdeskLoading.value = false
- rustdeskPromise = null
- })
-
- rustdeskPromise = request
- return request
- }
-
- async function refreshRustdeskStatus() {
- if (rustdeskLoading.value && rustdeskStatusPromise) return rustdeskStatusPromise
- rustdeskLoading.value = true
- rustdeskError.value = null
- const request = rustdeskConfigApi.getStatus()
- .then((response) => {
- rustdeskStatus.value = response
- rustdeskConfig.value = response.config
- return response
- })
- .catch((error) => {
- rustdeskError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- rustdeskLoading.value = false
- rustdeskStatusPromise = null
- })
-
- rustdeskStatusPromise = request
- return request
- }
-
- async function refreshRustdeskPassword() {
- if (rustdeskLoading.value && rustdeskPasswordPromise) return rustdeskPasswordPromise
- rustdeskLoading.value = true
- rustdeskError.value = null
- const request = rustdeskConfigApi.getPassword()
- .then((response) => {
- rustdeskPassword.value = response
- return response
- })
- .catch((error) => {
- rustdeskError.value = normalizeErrorMessage(error)
- throw error
- })
- .finally(() => {
- rustdeskLoading.value = false
- rustdeskPasswordPromise = null
- })
-
- rustdeskPasswordPromise = request
- return request
- }
-
- function ensureAuth() {
- if (auth.value) return Promise.resolve(auth.value)
- return refreshAuth()
- }
-
- function ensureVideo() {
- if (video.value) return Promise.resolve(video.value)
- return refreshVideo()
- }
-
- function ensureAudio() {
- if (audio.value) return Promise.resolve(audio.value)
- return refreshAudio()
- }
-
- function ensureHid() {
- if (hid.value) return Promise.resolve(hid.value)
- return refreshHid()
- }
-
- function ensureMsd() {
- if (msd.value) return Promise.resolve(msd.value)
- return refreshMsd()
- }
-
- function ensureStream() {
- if (stream.value) return Promise.resolve(stream.value)
- return refreshStream()
- }
-
- function ensureWeb() {
- if (web.value) return Promise.resolve(web.value)
- return refreshWeb()
- }
-
- function ensureAtx() {
- if (atx.value) return Promise.resolve(atx.value)
- return refreshAtx()
- }
-
- function ensureRtspConfig() {
- if (rtspConfig.value) return Promise.resolve(rtspConfig.value)
- return refreshRtspConfig()
- }
-
- function ensureVncConfig() {
- if (vncConfig.value) return Promise.resolve(vncConfig.value)
- return refreshVncConfig()
- }
-
- function ensureRustdeskConfig() {
- if (rustdeskConfig.value) return Promise.resolve(rustdeskConfig.value)
- return refreshRustdeskConfig()
- }
+ const rustdeskRequests = createRequestState()
+ const { loading: rustdeskLoading, error: rustdeskError } = rustdeskRequests
+ const { data: rustdeskConfig, refresh: refreshRustdeskConfig, ensure: ensureRustdeskConfig } =
+ createConfigResource(rustdeskConfigApi.get, rustdeskRequests)
+ const { data: rustdeskStatus, refresh: refreshRustdeskStatus } = createConfigResource(
+ rustdeskConfigApi.getStatus, rustdeskRequests, (response) => {
+ rustdeskConfig.value = response.config
+ },
+ )
+ const { data: rustdeskPassword, refresh: refreshRustdeskPassword } =
+ createConfigResource(rustdeskConfigApi.getPassword, rustdeskRequests)
async function updateAuth(update: AuthConfigUpdate) {
const response = await authConfigApi.update(update)
diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts
deleted file mode 100644
index 638a5468..00000000
--- a/web/src/stores/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { useAuthStore } from './auth'
-export { useConfigStore } from './config'
-export { useSystemStore } from './system'
diff --git a/web/src/types/websocket.ts b/web/src/types/websocket.ts
index 9ccc3f4d..54b57df1 100644
--- a/web/src/types/websocket.ts
+++ b/web/src/types/websocket.ts
@@ -1,31 +1,3 @@
-import { ref, type Ref } from 'vue'
-
-/** WebSocket connection state */
-export interface WsConnectionState {
- connected: Ref
- reconnectAttempts: Ref
- networkError: Ref
- networkErrorMessage: Ref
-}
-
-/** Create a new WebSocket connection state */
-export function createWsConnectionState(): WsConnectionState {
- return {
- connected: ref(false),
- reconnectAttempts: ref(0),
- networkError: ref(false),
- networkErrorMessage: ref(null),
- }
-}
-
-/** Reset connection state to initial values */
-export function resetWsConnectionState(state: WsConnectionState) {
- state.connected.value = false
- state.reconnectAttempts.value = 0
- state.networkError.value = false
- state.networkErrorMessage.value = null
-}
-
/** Build WebSocket URL from current location */
export function buildWsUrl(path: string): string {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
@@ -73,11 +45,3 @@ export interface StreamStateChangedEventData {
reason?: StreamSignalReason | null
next_retry_ms?: number | null
}
-
-/** WebSocket ready states */
-export const WS_STATE = {
- CONNECTING: WebSocket.CONNECTING,
- OPEN: WebSocket.OPEN,
- CLOSING: WebSocket.CLOSING,
- CLOSED: WebSocket.CLOSED,
-} as const
diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue
index 63e6f88d..15faa6a6 100644
--- a/web/src/views/ConsoleView.vue
+++ b/web/src/views/ConsoleView.vue
@@ -3036,9 +3036,6 @@ function handleToggleMouseMode() {
accumulatedDelta = { x: 0, y: 0 }
lastMousePosition.value = { x: 0, y: 0 }
mousePosition.value = { x: 0, y: 0 }
-
- if (mouseMode.value === 'relative') {
- }
}
async function refreshUacAvailability() {
diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue
index b1a0633d..287452fa 100644
--- a/web/src/views/SettingsView.vue
+++ b/web/src/views/SettingsView.vue
@@ -27,22 +27,12 @@ import {
atxConfigApi,
extensionsApi,
redfishConfigApi,
- rtspConfigApi,
- rustdeskConfigApi,
systemApi,
updateApi,
usbApi,
- vncConfigApi,
watchdogConfigApi,
type EncoderBackendInfo,
type AuthConfig,
- type RustDeskConfigResponse,
- type RustDeskStatusResponse,
- type RustDeskPasswordResponse,
- type RtspStatusResponse,
- type RtspConfigUpdate,
- type VncConfigUpdate,
- type VncStatusResponse,
type WebConfig,
type UpdateOverviewResponse,
type UpdateStatusResponse,
@@ -68,6 +58,8 @@ import type {
import { EasytierConfigMode, FrpProxyType, FrpcConfigMode } from '@/types/generated'
import { toConfigFps } from '@/lib/fps'
import { useClipboard } from '@/composables/useClipboard'
+import { useRemoteAccessSettings } from '@/composables/settings/useRemoteAccessSettings'
+import { formatHostForUrl } from '@/lib/url'
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
import { useTheme } from '@/composables/useTheme'
import { useConsoleLayout, type ConsoleLayout } from '@/composables/useConsoleLayout'
@@ -200,7 +192,6 @@ const onlineUpgradeSupported = computed(() => systemStore.platform?.mode === 'li
const activeSection = ref('appearance')
const loading = ref(false)
-const saved = ref(false)
const saveError = ref('')
const SETTINGS_SECTION_IDS = [
'appearance',
@@ -571,80 +562,41 @@ const frpcValidationMessage = computed(() => {
return ''
})
-const rustdeskConfig = ref(null)
-const rustdeskStatus = ref(null)
-const rustdeskPassword = ref(null)
-const rustdeskLoading = ref(false)
-const rustdeskCopied = ref<'id' | 'password' | null>(null)
const { copy: clipboardCopy } = useClipboard()
-const rustdeskLocalConfig = ref({
- enabled: false,
- mode: 'id' as 'id' | 'direct_ip',
- codec: 'h264' as 'h264' | 'h265',
- direct_access_port: 21118,
- rendezvous_server: '',
- relay_server: '',
- relay_key: '',
-})
-
-const rustdeskValidationMessage = computed(() => {
- if (
- rustdeskLocalConfig.value.mode === 'id' &&
- !rustdeskLocalConfig.value.rendezvous_server?.trim()
- ) {
- return t('extensions.rustdesk.rendezvousServerRequired')
- }
- if (
- rustdeskLocalConfig.value.mode === 'direct_ip' &&
- (rustdeskLocalConfig.value.direct_access_port < 1 ||
- rustdeskLocalConfig.value.direct_access_port > 65535)
- ) {
- return t('extensions.rustdesk.directAccessPortInvalid')
- }
- return ''
-})
-
-const rtspStatus = ref(null)
-const rtspLoading = ref(false)
-const rtspLocalConfig = ref({
- enabled: false,
- bind: '0.0.0.0',
- port: 8554,
- path: 'live',
- allow_one_client: true,
- codec: 'h264',
- username: '',
- password: '',
-})
-
-const vncStatus = ref(null)
-const vncLoading = ref(false)
-const vncLocalConfig = ref({
- enabled: false,
- bind: '0.0.0.0',
- port: 5900,
- encoding: 'tight_jpeg',
- allow_one_client: true,
- password: '',
-})
-
-function formatHostForUrl(hostname: string): string {
- if (!hostname) return '127.0.0.1'
- return hostname.includes(':') && !hostname.startsWith('[') ? `[${hostname}]` : hostname
-}
-
-const rtspStreamUrl = computed(() => {
- const host = formatHostForUrl(window.location.hostname || '127.0.0.1')
- const path = (rtspLocalConfig.value.path || 'live').trim().replace(/^\/+|\/+$/g, '') || 'live'
- const port = Number(rtspLocalConfig.value.port) || 8554
- return `rtsp://${host}:${port}/${path}`
-})
-
-const vncStreamUrl = computed(() => {
- const host = formatHostForUrl(window.location.hostname || '127.0.0.1')
- const port = Number(vncLocalConfig.value.port) || 5900
- return `${host}:${port}`
-})
+const {
+ rustdeskConfig,
+ rustdeskStatus,
+ rustdeskPassword,
+ rustdeskLoading,
+ rustdeskCopied,
+ rustdeskLocalConfig,
+ rustdeskValidationMessage,
+ rtspStatus,
+ rtspLoading,
+ rtspLocalConfig,
+ vncStatus,
+ vncLoading,
+ vncLocalConfig,
+ rtspStreamUrl,
+ vncStreamUrl,
+ serviceSaving,
+ loadRustdeskConfig,
+ loadRustdeskPassword,
+ saveRustdeskConfig,
+ regenerateRustdeskId,
+ regenerateRustdeskPassword,
+ startRustdesk,
+ stopRustdesk,
+ copyToClipboard,
+ loadRtspConfig,
+ saveRtspConfig,
+ startRtsp,
+ stopRtsp,
+ loadVncConfig,
+ saveVncConfig,
+ startVnc,
+ stopVnc,
+} = useRemoteAccessSettings({ loading, showValidationError })
const webServerConfig = ref({
http_port: 8080,
@@ -1512,7 +1464,6 @@ async function changePassword() {
}
}
-const serviceSaving = ref<'rtsp' | 'vnc' | 'rustdesk' | null>(null)
const videoSaving = ref<'capture' | 'encoder' | 'connection' | null>(null)
async function saveVideoCard(card: 'capture' | 'encoder' | 'connection') {
if (videoSaving.value) return
@@ -1555,7 +1506,6 @@ async function saveHidSettings(scope: 'features' | 'identity' = 'features') {
if (loading.value) return
hidSaveScope.value = scope
loading.value = true
- saved.value = false
saveError.value = ''
try {
@@ -1630,8 +1580,6 @@ async function saveHidSettings(scope: 'features' | 'identity' = 'features') {
if (activeSection.value !== 'hid') {
await loadSectionData(activeSection.value)
}
- saved.value = true
- setTimeout(() => (saved.value = false), 2000)
} catch (error) {
saveError.value = error instanceof Error ? error.message : t('api.operationFailedDesc')
} finally {
@@ -2126,51 +2074,6 @@ watch(
},
)
-function applyRustdeskStatus(status: RustDeskStatusResponse) {
- const config = status.config
- rustdeskConfig.value = config
- rustdeskStatus.value = status
- rustdeskLocalConfig.value = {
- enabled: config.enabled,
- mode: config.mode || 'id',
- codec: config.codec || 'h264',
- direct_access_port: config.direct_access_port || 21118,
- rendezvous_server: config.rendezvous_server,
- relay_server: config.relay_server || '',
- relay_key: config.relay_key || '',
- }
-}
-
-async function loadRustdeskConfig() {
- rustdeskLoading.value = true
- try {
- const status = await configStore.refreshRustdeskStatus()
- applyRustdeskStatus(status)
- } catch {
- } finally {
- rustdeskLoading.value = false
- }
-}
-
-async function loadRustdeskPassword() {
- try {
- rustdeskPassword.value = await configStore.refreshRustdeskPassword()
- } catch {}
-}
-
-function normalizeRustdeskServer(value: string, defaultPort: number): string {
- const trimmed = value.trim()
- if (!trimmed) return ''
- if (trimmed.includes(':')) return trimmed
- return `${trimmed}:${defaultPort}`
-}
-
-/** Strip line breaks from pasted keys. */
-function normalizeRustdeskRelayKey(value: string): string {
- const cleaned = value.replace(/\r?\n/g, '').trim()
- return cleaned
-}
-
function showValidationError(message: string): boolean {
toast.error(t('api.operationFailed'), {
description: message,
@@ -2192,25 +2095,6 @@ function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
return !message || showValidationError(message)
}
-function validateRustdeskConfig(): boolean {
- return !rustdeskValidationMessage.value || showValidationError(rustdeskValidationMessage.value)
-}
-
-function validateVncConfig(enabled = vncLocalConfig.value.enabled): boolean {
- const password = (vncLocalConfig.value.password || '').trim()
- if (enabled && !vncStatus.value?.config.has_password && !password) {
- return showValidationError(t('extensions.vnc.passwordRequired'))
- }
- if (password.length > 8) {
- return showValidationError(t('extensions.vnc.passwordMaxLength'))
- }
- return true
-}
-
-function normalizeRtspPath(path: string): string {
- return path.trim().replace(/^\/+|\/+$/g, '') || 'live'
-}
-
function normalizeBindAddresses(addresses: string[]): string[] {
return addresses.map((addr) => addr.trim()).filter(Boolean)
}
@@ -2542,252 +2426,6 @@ function updateStatusBadgeText(): string {
)
}
-function rustdeskUpdatePayload(enabled = rustdeskLocalConfig.value.enabled) {
- return {
- enabled,
- mode: rustdeskLocalConfig.value.mode,
- codec: rustdeskLocalConfig.value.codec,
- direct_access_port: rustdeskLocalConfig.value.direct_access_port,
- rendezvous_server: normalizeRustdeskServer(rustdeskLocalConfig.value.rendezvous_server, 21116),
- relay_server: normalizeRustdeskServer(rustdeskLocalConfig.value.relay_server, 21117),
- relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key),
- }
-}
-
-async function saveRustdeskConfig() {
- if (
- (rustdeskLocalConfig.value.enabled || rustdeskLocalConfig.value.mode === 'direct_ip') &&
- !validateRustdeskConfig()
- )
- return
-
- serviceSaving.value = 'rustdesk'
- loading.value = true
- saved.value = false
- try {
- await configStore.updateRustdesk(rustdeskUpdatePayload())
- await loadRustdeskConfig()
- toast.success(t('settingsUi.saveDone'))
- } catch {
- } finally {
- loading.value = false
- serviceSaving.value = null
- }
-}
-
-async function regenerateRustdeskId() {
- if (!confirm(t('extensions.rustdesk.confirmRegenerateId'))) return
- rustdeskLoading.value = true
- try {
- await configStore.regenerateRustdeskId()
- await loadRustdeskConfig()
- await loadRustdeskPassword()
- } catch {
- } finally {
- rustdeskLoading.value = false
- }
-}
-
-async function regenerateRustdeskPassword() {
- if (!confirm(t('extensions.rustdesk.confirmRegeneratePassword'))) return
- rustdeskLoading.value = true
- try {
- await configStore.regenerateRustdeskPassword()
- await loadRustdeskConfig()
- await loadRustdeskPassword()
- } catch {
- } finally {
- rustdeskLoading.value = false
- }
-}
-
-async function startRustdesk() {
- if (!validateRustdeskConfig()) return
-
- rustdeskLoading.value = true
- try {
- await configStore.updateRustdesk(rustdeskUpdatePayload())
- const status = await rustdeskConfigApi.start()
- applyRustdeskStatus(status)
- } catch {
- } finally {
- rustdeskLoading.value = false
- }
-}
-
-async function stopRustdesk() {
- rustdeskLoading.value = true
- try {
- const status = await rustdeskConfigApi.stop()
- applyRustdeskStatus(status)
- } catch {
- } finally {
- rustdeskLoading.value = false
- }
-}
-
-async function copyToClipboard(text: string, type: 'id' | 'password') {
- const success = await clipboardCopy(text)
- if (success) {
- rustdeskCopied.value = type
- setTimeout(() => (rustdeskCopied.value = null), 2000)
- }
-}
-
-function applyRtspStatus(status: RtspStatusResponse) {
- rtspStatus.value = status
- rtspLocalConfig.value = {
- enabled: status.config.enabled,
- bind: status.config.bind,
- port: status.config.port,
- path: status.config.path,
- allow_one_client: status.config.allow_one_client,
- codec: status.config.codec,
- username: status.config.username || '',
- password: status.config.password || '',
- }
-}
-
-async function loadRtspConfig() {
- rtspLoading.value = true
- try {
- const status = await configStore.refreshRtspStatus()
- applyRtspStatus(status)
- } catch {
- } finally {
- rtspLoading.value = false
- }
-}
-
-function rtspUpdatePayload(enabled = !!rtspLocalConfig.value.enabled): RtspConfigUpdate {
- return {
- enabled,
- bind: rtspLocalConfig.value.bind?.trim() || '0.0.0.0',
- port: Number(rtspLocalConfig.value.port) || 8554,
- path: normalizeRtspPath(rtspLocalConfig.value.path || 'live'),
- allow_one_client: !!rtspLocalConfig.value.allow_one_client,
- codec: rtspLocalConfig.value.codec || 'h264',
- username: (rtspLocalConfig.value.username || '').trim(),
- password: (rtspLocalConfig.value.password || '').trim(),
- }
-}
-
-async function saveRtspConfig() {
- serviceSaving.value = 'rtsp'
- loading.value = true
- saved.value = false
- try {
- await configStore.updateRtsp(rtspUpdatePayload())
- await loadRtspConfig()
- toast.success(t('settingsUi.saveDone'))
- } catch {
- } finally {
- loading.value = false
- serviceSaving.value = null
- }
-}
-
-async function startRtsp() {
- rtspLoading.value = true
- try {
- await configStore.updateRtsp(rtspUpdatePayload())
- const status = await rtspConfigApi.start()
- applyRtspStatus(status)
- } catch {
- } finally {
- rtspLoading.value = false
- }
-}
-
-async function stopRtsp() {
- rtspLoading.value = true
- try {
- const status = await rtspConfigApi.stop()
- applyRtspStatus(status)
- } catch {
- } finally {
- rtspLoading.value = false
- }
-}
-
-function applyVncStatus(status: VncStatusResponse) {
- vncStatus.value = status
- vncLocalConfig.value = {
- enabled: status.config.enabled,
- bind: status.config.bind,
- port: status.config.port,
- encoding: status.config.encoding,
- allow_one_client: status.config.allow_one_client,
- password: '',
- }
-}
-
-async function loadVncConfig() {
- vncLoading.value = true
- try {
- const status = await configStore.refreshVncStatus()
- applyVncStatus(status)
- } catch {
- } finally {
- vncLoading.value = false
- }
-}
-
-function vncUpdatePayload(enabled = !!vncLocalConfig.value.enabled): VncConfigUpdate {
- const update: VncConfigUpdate = {
- enabled,
- bind: vncLocalConfig.value.bind?.trim() || '0.0.0.0',
- port: Number(vncLocalConfig.value.port) || 5900,
- encoding: vncLocalConfig.value.encoding || 'tight_jpeg',
- allow_one_client: !!vncLocalConfig.value.allow_one_client,
- }
- const password = (vncLocalConfig.value.password || '').trim()
- if (password) update.password = password
- return update
-}
-
-async function saveVncConfig() {
- if (!validateVncConfig()) return
-
- serviceSaving.value = 'vnc'
- loading.value = true
- saved.value = false
- try {
- await configStore.updateVnc(vncUpdatePayload())
- await loadVncConfig()
- toast.success(t('settingsUi.saveDone'))
- } catch {
- } finally {
- loading.value = false
- serviceSaving.value = null
- }
-}
-
-async function startVnc() {
- if (!validateVncConfig(true)) return
-
- vncLoading.value = true
- try {
- await configStore.updateVnc(vncUpdatePayload())
- const status = await vncConfigApi.start()
- applyVncStatus(status)
- } catch {
- } finally {
- vncLoading.value = false
- }
-}
-
-async function stopVnc() {
- vncLoading.value = true
- try {
- const status = await vncConfigApi.stop()
- applyVncStatus(status)
- } catch {
- } finally {
- vncLoading.value = false
- }
-}
-
onMounted(async () => {
const initialSection = normalizeSettingsSection(route.query.tab)
if (initialSection) {
@@ -5759,7 +5397,6 @@ watch(isWindows, () => {
:aria-label="t('extensions.start') + ' ' + t('extensions.rtsp.title')"
:disabled="
rtspLoading ||
- rtspStatus?.service_status === 'starting' ||
rtspStatus?.service_status === 'running' ||
rtspStatus?.service_status === 'starting'
"
@@ -5927,7 +5564,6 @@ watch(isWindows, () => {
:aria-label="t('extensions.start') + ' ' + t('extensions.vnc.title')"
:disabled="
vncLoading ||
- vncStatus?.service_status === 'starting' ||
vncStatus?.service_status === 'running' ||
vncStatus?.service_status === 'starting'
"