mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 16:30:48 +08:00
refactor(web): 精简前端结构与未使用代码
抽离远程访问设置逻辑,合并配置缓存、并发请求及错误处理,修复共享 loading 状态提前结束。统一控制台事件订阅与清理,移除未使用组件、旧 composable 和冗余状态。 已通过前端生产构建、模拟 API 浏览器检查和 OpenWrt ARMv7 部署验证。
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { AvatarRoot } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarRoot
|
||||
data-slot="avatar"
|
||||
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AvatarRoot>
|
||||
</template>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarFallbackProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AvatarFallback } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarFallback
|
||||
data-slot="avatar-fallback"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AvatarFallback>
|
||||
</template>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarImageProps } from "reka-ui"
|
||||
import { AvatarImage } from "reka-ui"
|
||||
|
||||
const props = defineProps<AvatarImageProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarImage
|
||||
data-slot="avatar-image"
|
||||
v-bind="props"
|
||||
class="aspect-square size-full"
|
||||
>
|
||||
<slot />
|
||||
</AvatarImage>
|
||||
</template>
|
||||
@@ -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"
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-action"
|
||||
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { X } from "lucide-vue-next"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
@pointer-down-outside="(event) => {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
const target = originalEvent.target as HTMLElement;
|
||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTriggerProps } from "reka-ui"
|
||||
import { DialogTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger
|
||||
data-slot="dialog-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
@@ -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"
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { Check } from "lucide-vue-next"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
v-bind="forwarded"
|
||||
:class=" cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Check class="size-4" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuGroupProps } from "reka-ui"
|
||||
import { DropdownMenuGroup } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup
|
||||
data-slot="dropdown-menu-group"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { Circle } from "lucide-vue-next"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Circle class="size-2 fill-current" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>()
|
||||
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useConsoleAppearance } from "@/composables/useConsoleAppearance"
|
||||
const consoleAppearance = useConsoleAppearance()
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
:data-console-layout="consoleAppearance"
|
||||
v-bind="forwarded"
|
||||
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] max-w-(--reka-dropdown-menu-content-available-width) origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { ChevronRight } from "lucide-vue-next"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
useForwardProps,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
v-bind="forwardedProps"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
:class="cn(
|
||||
`relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto size-4" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
@@ -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"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcceptableValue } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { ChevronDownIcon } from "lucide-vue-next"
|
||||
import { reactiveOmit, useVModel } from "@vueuse/core"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
|
||||
{ size: "default" },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: AcceptableValue]
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit, {
|
||||
passive: true,
|
||||
defaultValue: "",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="group/native-select relative w-full has-[select:disabled]:opacity-50"
|
||||
data-slot="native-select-wrapper"
|
||||
>
|
||||
<select
|
||||
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||
v-model="modelValue"
|
||||
data-slot="native-select"
|
||||
:data-size="size"
|
||||
:class="cn(
|
||||
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 data-[size=default]:h-9 data-[size=sm]:h-8 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
<ChevronDownIcon
|
||||
class="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
aria-hidden="true"
|
||||
data-slot="native-select-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<option data-slot="native-select-option" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||
<slot />
|
||||
</option>
|
||||
</template>
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as NativeSelect } from "./NativeSelect.vue"
|
||||
export { default as NativeSelectOption } from "./NativeSelectOption.vue"
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectGroupProps } from "reka-ui"
|
||||
import { SelectGroup } from "reka-ui"
|
||||
|
||||
const props = defineProps<SelectGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup
|
||||
data-slot="select-group"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItemTextProps } from "reka-ui"
|
||||
import { SelectItemText } from "reka-ui"
|
||||
|
||||
const props = defineProps<SelectItemTextProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItemText
|
||||
data-slot="select-item-text"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectLabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { SelectLabel } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
data-slot="select-label"
|
||||
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
@@ -1,19 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectSeparatorProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { SelectSeparator } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
data-slot="select-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -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"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToggleEmits, ToggleProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ToggleVariants } from "."
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Toggle, useForwardPropsEmits } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "."
|
||||
|
||||
const props = withDefaults(defineProps<ToggleProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
variant?: ToggleVariants["variant"]
|
||||
size?: ToggleVariants["size"]
|
||||
}>(), {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits<ToggleEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size", "variant")
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toggle
|
||||
v-slot="slotProps"
|
||||
data-slot="toggle"
|
||||
v-bind="forwarded"
|
||||
:class="cn(toggleVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</Toggle>
|
||||
</template>
|
||||
@@ -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",
|
||||
|
||||
433
web/src/composables/settings/useRemoteAccessSettings.ts
Normal file
433
web/src/composables/settings/useRemoteAccessSettings.ts
Normal file
@@ -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<boolean>
|
||||
showValidationError: (message: string) => boolean
|
||||
}
|
||||
|
||||
export function useRemoteAccessSettings({ loading, showValidationError }: RemoteAccessSettingsOptions) {
|
||||
const { t } = useI18n()
|
||||
const configStore = useConfigStore()
|
||||
const { copy: clipboardCopy } = useClipboard()
|
||||
const serviceSaving = ref<RemoteService | null>(null)
|
||||
let copiedTimer: ReturnType<typeof setTimeout> | undefined
|
||||
onScopeDispose(() => clearTimeout(copiedTimer))
|
||||
|
||||
const rustdeskConfig = ref<RustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(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<RtspStatusResponse | null>(null)
|
||||
const rtspLoading = ref(false)
|
||||
const rtspLocalConfig = ref<RtspConfigUpdate>({
|
||||
enabled: false,
|
||||
bind: '0.0.0.0',
|
||||
port: 8554,
|
||||
path: 'live',
|
||||
allow_one_client: true,
|
||||
codec: 'h264',
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const vncStatus = ref<VncStatusResponse | null>(null)
|
||||
const vncLoading = ref(false)
|
||||
const vncLocalConfig = ref<VncConfigUpdate>({
|
||||
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<unknown>,
|
||||
reload: () => Promise<void>,
|
||||
) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
import { ref, watch, type Ref } from 'vue'
|
||||
|
||||
export interface UseConfigPopoverOptions {
|
||||
/** Reactive open state from props */
|
||||
open: Ref<boolean>
|
||||
/** Load device list callback */
|
||||
loadDevices?: () => Promise<void>
|
||||
/** 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<void>) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<AudioMode>
|
||||
volume: Ref<number>
|
||||
muted: Ref<boolean>
|
||||
connected: Ref<boolean>
|
||||
playing: Ref<boolean>
|
||||
error: Ref<string | null>
|
||||
}
|
||||
|
||||
export function useUnifiedAudio() {
|
||||
const audioMode = ref<AudioMode>('ws')
|
||||
const volume = ref(0) // 0-1, default muted (browser autoplay policy)
|
||||
|
||||
@@ -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<VideoMode>
|
||||
loading: Ref<boolean>
|
||||
error: Ref<boolean>
|
||||
errorMessage: Ref<string>
|
||||
restarting: Ref<boolean>
|
||||
fps: Ref<number>
|
||||
mjpegUrl: Ref<string>
|
||||
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<VideoMode>('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<Record<string, { id: string; fps: number; connected_secs: number }>>({})
|
||||
|
||||
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<string, { fps: number }> }) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
54
web/src/lib/configResource.ts
Normal file
54
web/src/lib/configResource.ts
Normal file
@@ -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<string | null>(null)
|
||||
let pending = 0
|
||||
|
||||
async function run<T>(request: () => Promise<T>): Promise<T> {
|
||||
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<T>(
|
||||
fetcher: () => Promise<T>,
|
||||
state = createRequestState(),
|
||||
onLoaded?: (response: T) => void,
|
||||
) {
|
||||
const data = ref(null) as Ref<T | null>
|
||||
let inFlight: Promise<T> | null = null
|
||||
|
||||
function refresh(): Promise<T> {
|
||||
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<T> {
|
||||
return data.value === null ? refresh() : Promise.resolve(data.value)
|
||||
}
|
||||
|
||||
return { data, loading: state.loading, error: state.error, refresh, ensure }
|
||||
}
|
||||
4
web/src/lib/url.ts
Normal file
4
web/src/lib/url.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function formatHostForUrl(hostname: string): string {
|
||||
if (!hostname) return '127.0.0.1'
|
||||
return hostname.includes(':') && !hostname.startsWith('[') ? `[${hostname}]` : hostname
|
||||
}
|
||||
@@ -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<string | null>(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,
|
||||
|
||||
@@ -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<AuthConfig | null>(null)
|
||||
const video = ref<VideoConfig | null>(null)
|
||||
const audio = ref<AudioConfig | null>(null)
|
||||
const hid = ref<HidConfig | null>(null)
|
||||
const msd = ref<MsdConfig | null>(null)
|
||||
const stream = ref<StreamConfigResponse | null>(null)
|
||||
const web = ref<WebConfig | null>(null)
|
||||
const atx = ref<AtxConfig | null>(null)
|
||||
const rtspConfig = ref<ApiRtspConfigResponse | null>(null)
|
||||
const rtspStatus = ref<ApiRtspStatusResponse | null>(null)
|
||||
const vncConfig = ref<ApiVncConfigResponse | null>(null)
|
||||
const vncStatus = ref<ApiVncStatusResponse | null>(null)
|
||||
const rustdeskConfig = ref<ApiRustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<ApiRustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<ApiRustDeskPasswordResponse | null>(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<string | null>(null)
|
||||
const videoError = ref<string | null>(null)
|
||||
const audioError = ref<string | null>(null)
|
||||
const hidError = ref<string | null>(null)
|
||||
const msdError = ref<string | null>(null)
|
||||
const streamError = ref<string | null>(null)
|
||||
const webError = ref<string | null>(null)
|
||||
const atxError = ref<string | null>(null)
|
||||
const rtspError = ref<string | null>(null)
|
||||
const vncError = ref<string | null>(null)
|
||||
const rustdeskError = ref<string | null>(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<AuthConfig> | null = null
|
||||
let videoPromise: Promise<VideoConfig> | null = null
|
||||
let audioPromise: Promise<AudioConfig> | null = null
|
||||
let hidPromise: Promise<HidConfig> | null = null
|
||||
let msdPromise: Promise<MsdConfig> | null = null
|
||||
let streamPromise: Promise<StreamConfigResponse> | null = null
|
||||
let webPromise: Promise<WebConfig> | null = null
|
||||
let atxPromise: Promise<AtxConfig> | null = null
|
||||
let rtspPromise: Promise<ApiRtspConfigResponse> | null = null
|
||||
let rtspStatusPromise: Promise<ApiRtspStatusResponse> | null = null
|
||||
let vncPromise: Promise<ApiVncConfigResponse> | null = null
|
||||
let vncStatusPromise: Promise<ApiVncStatusResponse> | null = null
|
||||
let rustdeskPromise: Promise<ApiRustDeskConfigResponse> | null = null
|
||||
let rustdeskStatusPromise: Promise<ApiRustDeskStatusResponse> | null = null
|
||||
let rustdeskPasswordPromise: Promise<ApiRustDeskPasswordResponse> | 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)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { useAuthStore } from './auth'
|
||||
export { useConfigStore } from './config'
|
||||
export { useSystemStore } from './system'
|
||||
@@ -1,31 +1,3 @@
|
||||
import { ref, type Ref } from 'vue'
|
||||
|
||||
/** WebSocket connection state */
|
||||
export interface WsConnectionState {
|
||||
connected: Ref<boolean>
|
||||
reconnectAttempts: Ref<number>
|
||||
networkError: Ref<boolean>
|
||||
networkErrorMessage: Ref<string | null>
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<SettingsSectionId>('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<RustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(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<RtspStatusResponse | null>(null)
|
||||
const rtspLoading = ref(false)
|
||||
const rtspLocalConfig = ref<RtspConfigUpdate & { password?: string }>({
|
||||
enabled: false,
|
||||
bind: '0.0.0.0',
|
||||
port: 8554,
|
||||
path: 'live',
|
||||
allow_one_client: true,
|
||||
codec: 'h264',
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const vncStatus = ref<VncStatusResponse | null>(null)
|
||||
const vncLoading = ref(false)
|
||||
const vncLocalConfig = ref<VncConfigUpdate & { password?: string }>({
|
||||
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<WebConfig>({
|
||||
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'
|
||||
"
|
||||
|
||||
Reference in New Issue
Block a user