mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +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 Card } from "./Card.vue"
|
||||||
export { default as CardAction } from "./CardAction.vue"
|
|
||||||
export { default as CardContent } from "./CardContent.vue"
|
export { default as CardContent } from "./CardContent.vue"
|
||||||
export { default as CardDescription } from "./CardDescription.vue"
|
export { default as CardDescription } from "./CardDescription.vue"
|
||||||
export { default as CardFooter } from "./CardFooter.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 DialogFooter } from "./DialogFooter.vue"
|
||||||
export { default as DialogHeader } from "./DialogHeader.vue"
|
export { default as DialogHeader } from "./DialogHeader.vue"
|
||||||
export { default as DialogOverlay } from "./DialogOverlay.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 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 DropdownMenu } from "./DropdownMenu.vue"
|
||||||
|
|
||||||
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
|
|
||||||
export { default as DropdownMenuContent } from "./DropdownMenuContent.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 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 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 { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
|
||||||
export { DropdownMenuPortal } from "reka-ui"
|
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 Select } from "./Select.vue"
|
||||||
export { default as SelectContent } from "./SelectContent.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 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 SelectScrollDownButton } from "./SelectScrollDownButton.vue"
|
||||||
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.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 SelectTrigger } from "./SelectTrigger.vue"
|
||||||
export { default as SelectValue } from "./SelectValue.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 type { VariantProps } from "class-variance-authority"
|
||||||
import { cva } from "class-variance-authority"
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
export { default as Toggle } from "./Toggle.vue"
|
|
||||||
|
|
||||||
export const toggleVariants = cva(
|
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",
|
"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 { useI18n } from 'vue-i18n'
|
||||||
import { toast } from 'vue-sonner'
|
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 type { StreamDeviceLostEventData, StreamStateChangedEventData } from '@/types/websocket'
|
||||||
import { useWebSocket } from '@/composables/useWebSocket'
|
import { useWebSocket } from '@/composables/useWebSocket'
|
||||||
import { isAudioStreamDeviceLostPayload } from '@/lib/streamSignal'
|
import { isAudioStreamDeviceLostPayload } from '@/lib/streamSignal'
|
||||||
@@ -17,14 +17,14 @@ export interface ConsoleEventHandlers {
|
|||||||
onStreamDeviceLost?: (data: StreamDeviceLostEventData) => void
|
onStreamDeviceLost?: (data: StreamDeviceLostEventData) => void
|
||||||
onStreamReconnecting?: (data: { device: string; attempt: number }) => void
|
onStreamReconnecting?: (data: { device: string; attempt: number }) => void
|
||||||
onStreamRecovered?: (data: { device: string }) => void
|
onStreamRecovered?: (data: { device: string }) => void
|
||||||
onDeviceInfo?: (data: any) => void
|
onDeviceInfo?: (data: DeviceInfoEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useConsoleEvents(handlers: ConsoleEventHandlers) {
|
export function useConsoleEvents(handlers: ConsoleEventHandlers) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const systemStore = useSystemStore()
|
const systemStore = useSystemStore()
|
||||||
const { on, off, connect } = useWebSocket()
|
const { on, off, connect } = useWebSocket()
|
||||||
const noop = () => {}
|
let subscribed = false
|
||||||
|
|
||||||
function handleStreamDeviceLost(data: StreamDeviceLostEventData) {
|
function handleStreamDeviceLost(data: StreamDeviceLostEventData) {
|
||||||
const audioLost = isAudioStreamDeviceLostPayload(data)
|
const audioLost = isAudioStreamDeviceLostPayload(data)
|
||||||
@@ -41,53 +41,44 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
|
|||||||
handlers.onStreamDeviceLost?.(data)
|
handlers.onStreamDeviceLost?.(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStreamReconnecting(data: { device: string; attempt: number }) {
|
function handleStreamRecovered(data: { device: string }) {
|
||||||
handlers.onStreamReconnecting?.(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStreamRecovered(_data: { device: string }) {
|
|
||||||
if (systemStore.stream) {
|
if (systemStore.stream) {
|
||||||
systemStore.stream.online = true
|
systemStore.stream.online = true
|
||||||
}
|
}
|
||||||
handlers.onStreamRecovered?.(_data)
|
handlers.onStreamRecovered?.(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStreamStateChangedForward(data: StreamStateChangedEventData) {
|
// Keep registration and cleanup together so every subscription is removed.
|
||||||
handlers.onStreamStateChanged?.(data)
|
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() {
|
function subscribe() {
|
||||||
on('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
|
if (subscribed) return
|
||||||
on('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
|
subscribed = true
|
||||||
on('stream.stats_update', handlers.onStreamStatsUpdate ?? noop)
|
for (const [event, handler] of subscriptions) {
|
||||||
on('stream.mode_changed', handlers.onStreamModeChanged ?? noop)
|
if (handler) on(event, handler)
|
||||||
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)
|
|
||||||
|
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribe() {
|
function unsubscribe() {
|
||||||
off('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
|
if (!subscribed) return
|
||||||
off('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
|
subscribed = false
|
||||||
off('stream.stats_update', handlers.onStreamStatsUpdate ?? noop)
|
for (const [event, handler] of subscriptions) {
|
||||||
off('stream.mode_changed', handlers.onStreamModeChanged ?? noop)
|
if (handler) off(event, handler)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
// Manages audio playback across different video modes (MJPEG/WebSocket and H264/WebRTC)
|
// Manages audio playback across different video modes (MJPEG/WebSocket and H264/WebRTC)
|
||||||
// Provides a single interface for volume control and audio source switching
|
// 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'
|
import { getAudioPlayer } from './useAudioPlayer'
|
||||||
|
|
||||||
export type AudioMode = 'ws' | 'webrtc'
|
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() {
|
export function useUnifiedAudio() {
|
||||||
const audioMode = ref<AudioMode>('ws')
|
const audioMode = ref<AudioMode>('ws')
|
||||||
const volume = ref(0) // 0-1, default muted (browser autoplay policy)
|
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 { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { authApi, systemApi, type AuthLoginResponse } from '@/api'
|
import { authApi, systemApi, type AuthLoginResponse } from '@/api'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
@@ -11,8 +11,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
let pendingUsername: string | null = null
|
let pendingUsername: string | null = null
|
||||||
|
|
||||||
const isLoggedIn = computed(() => isAuthenticated.value && user.value !== null)
|
|
||||||
|
|
||||||
async function checkSetupStatus() {
|
async function checkSetupStatus() {
|
||||||
try {
|
try {
|
||||||
const status = await systemApi.setupStatus()
|
const status = await systemApi.setupStatus()
|
||||||
@@ -136,7 +134,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
needsSetup,
|
needsSetup,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
isLoggedIn,
|
|
||||||
checkSetupStatus,
|
checkSetupStatus,
|
||||||
checkAuth,
|
checkAuth,
|
||||||
beginLogin,
|
beginLogin,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { createConfigResource, createRequestState } from '@/lib/configResource'
|
||||||
import {
|
import {
|
||||||
authConfigApi,
|
authConfigApi,
|
||||||
atxConfigApi,
|
atxConfigApi,
|
||||||
@@ -15,488 +15,88 @@ import {
|
|||||||
webConfigApi,
|
webConfigApi,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
import type {
|
import type {
|
||||||
AtxConfig,
|
|
||||||
AtxConfigUpdate,
|
AtxConfigUpdate,
|
||||||
AudioConfig,
|
|
||||||
AudioConfigUpdate,
|
AudioConfigUpdate,
|
||||||
AuthConfig,
|
|
||||||
AuthConfigUpdate,
|
AuthConfigUpdate,
|
||||||
HidConfig,
|
|
||||||
HidConfigUpdate,
|
HidConfigUpdate,
|
||||||
MsdConfig,
|
|
||||||
MsdConfigUpdate,
|
MsdConfigUpdate,
|
||||||
OtgConfigResponse,
|
OtgConfigResponse,
|
||||||
OtgConfigUpdate,
|
OtgConfigUpdate,
|
||||||
StreamConfigResponse,
|
|
||||||
StreamConfigUpdate,
|
StreamConfigUpdate,
|
||||||
VideoConfig,
|
|
||||||
VideoConfigUpdate,
|
VideoConfigUpdate,
|
||||||
} from '@/types/generated'
|
} from '@/types/generated'
|
||||||
import type {
|
import type {
|
||||||
RtspConfigResponse as ApiRtspConfigResponse,
|
|
||||||
RtspConfigUpdate as ApiRtspConfigUpdate,
|
RtspConfigUpdate as ApiRtspConfigUpdate,
|
||||||
RtspStatusResponse as ApiRtspStatusResponse,
|
|
||||||
RustDeskConfigResponse as ApiRustDeskConfigResponse,
|
|
||||||
RustDeskConfigUpdate as ApiRustDeskConfigUpdate,
|
RustDeskConfigUpdate as ApiRustDeskConfigUpdate,
|
||||||
RustDeskStatusResponse as ApiRustDeskStatusResponse,
|
|
||||||
RustDeskPasswordResponse as ApiRustDeskPasswordResponse,
|
|
||||||
VncConfigResponse as ApiVncConfigResponse,
|
|
||||||
VncConfigUpdate as ApiVncConfigUpdate,
|
VncConfigUpdate as ApiVncConfigUpdate,
|
||||||
VncStatusResponse as ApiVncStatusResponse,
|
|
||||||
WebConfig,
|
|
||||||
WebConfigUpdate,
|
WebConfigUpdate,
|
||||||
} from '@/api'
|
} 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', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const auth = ref<AuthConfig | null>(null)
|
const {
|
||||||
const video = ref<VideoConfig | null>(null)
|
data: auth, loading: authLoading, error: authError,
|
||||||
const audio = ref<AudioConfig | null>(null)
|
refresh: refreshAuth, ensure: ensureAuth,
|
||||||
const hid = ref<HidConfig | null>(null)
|
} = createConfigResource(authConfigApi.get)
|
||||||
const msd = ref<MsdConfig | null>(null)
|
const {
|
||||||
const stream = ref<StreamConfigResponse | null>(null)
|
data: video, loading: videoLoading, error: videoError,
|
||||||
const web = ref<WebConfig | null>(null)
|
refresh: refreshVideo, ensure: ensureVideo,
|
||||||
const atx = ref<AtxConfig | null>(null)
|
} = createConfigResource(videoConfigApi.get)
|
||||||
const rtspConfig = ref<ApiRtspConfigResponse | null>(null)
|
const {
|
||||||
const rtspStatus = ref<ApiRtspStatusResponse | null>(null)
|
data: audio, loading: audioLoading, error: audioError,
|
||||||
const vncConfig = ref<ApiVncConfigResponse | null>(null)
|
refresh: refreshAudio, ensure: ensureAudio,
|
||||||
const vncStatus = ref<ApiVncStatusResponse | null>(null)
|
} = createConfigResource(audioConfigApi.get)
|
||||||
const rustdeskConfig = ref<ApiRustDeskConfigResponse | null>(null)
|
const {
|
||||||
const rustdeskStatus = ref<ApiRustDeskStatusResponse | null>(null)
|
data: hid, loading: hidLoading, error: hidError,
|
||||||
const rustdeskPassword = ref<ApiRustDeskPasswordResponse | null>(null)
|
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 rtspRequests = createRequestState()
|
||||||
const videoLoading = ref(false)
|
const { loading: rtspLoading, error: rtspError } = rtspRequests
|
||||||
const audioLoading = ref(false)
|
const { data: rtspConfig, refresh: refreshRtspConfig, ensure: ensureRtspConfig } =
|
||||||
const hidLoading = ref(false)
|
createConfigResource(rtspConfigApi.get, rtspRequests)
|
||||||
const msdLoading = ref(false)
|
const { data: rtspStatus, refresh: refreshRtspStatus } = createConfigResource(
|
||||||
const streamLoading = ref(false)
|
rtspConfigApi.getStatus, rtspRequests, (response) => {
|
||||||
const webLoading = ref(false)
|
rtspConfig.value = response.config
|
||||||
const atxLoading = ref(false)
|
},
|
||||||
const rtspLoading = ref(false)
|
)
|
||||||
const vncLoading = ref(false)
|
|
||||||
const rustdeskLoading = ref(false)
|
|
||||||
|
|
||||||
const authError = ref<string | null>(null)
|
const vncRequests = createRequestState()
|
||||||
const videoError = ref<string | null>(null)
|
const { loading: vncLoading, error: vncError } = vncRequests
|
||||||
const audioError = ref<string | null>(null)
|
const { data: vncConfig, refresh: refreshVncConfig, ensure: ensureVncConfig } =
|
||||||
const hidError = ref<string | null>(null)
|
createConfigResource(vncConfigApi.get, vncRequests)
|
||||||
const msdError = ref<string | null>(null)
|
const { data: vncStatus, refresh: refreshVncStatus } = createConfigResource(
|
||||||
const streamError = ref<string | null>(null)
|
vncConfigApi.getStatus, vncRequests, (response) => {
|
||||||
const webError = ref<string | null>(null)
|
vncConfig.value = response.config
|
||||||
const atxError = ref<string | null>(null)
|
},
|
||||||
const rtspError = ref<string | null>(null)
|
)
|
||||||
const vncError = ref<string | null>(null)
|
|
||||||
const rustdeskError = ref<string | null>(null)
|
|
||||||
|
|
||||||
let authPromise: Promise<AuthConfig> | null = null
|
const rustdeskRequests = createRequestState()
|
||||||
let videoPromise: Promise<VideoConfig> | null = null
|
const { loading: rustdeskLoading, error: rustdeskError } = rustdeskRequests
|
||||||
let audioPromise: Promise<AudioConfig> | null = null
|
const { data: rustdeskConfig, refresh: refreshRustdeskConfig, ensure: ensureRustdeskConfig } =
|
||||||
let hidPromise: Promise<HidConfig> | null = null
|
createConfigResource(rustdeskConfigApi.get, rustdeskRequests)
|
||||||
let msdPromise: Promise<MsdConfig> | null = null
|
const { data: rustdeskStatus, refresh: refreshRustdeskStatus } = createConfigResource(
|
||||||
let streamPromise: Promise<StreamConfigResponse> | null = null
|
rustdeskConfigApi.getStatus, rustdeskRequests, (response) => {
|
||||||
let webPromise: Promise<WebConfig> | null = null
|
rustdeskConfig.value = response.config
|
||||||
let atxPromise: Promise<AtxConfig> | null = null
|
},
|
||||||
let rtspPromise: Promise<ApiRtspConfigResponse> | null = null
|
)
|
||||||
let rtspStatusPromise: Promise<ApiRtspStatusResponse> | null = null
|
const { data: rustdeskPassword, refresh: refreshRustdeskPassword } =
|
||||||
let vncPromise: Promise<ApiVncConfigResponse> | null = null
|
createConfigResource(rustdeskConfigApi.getPassword, rustdeskRequests)
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateAuth(update: AuthConfigUpdate) {
|
async function updateAuth(update: AuthConfigUpdate) {
|
||||||
const response = await authConfigApi.update(update)
|
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 */
|
/** Build WebSocket URL from current location */
|
||||||
export function buildWsUrl(path: string): string {
|
export function buildWsUrl(path: string): string {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
@@ -73,11 +45,3 @@ export interface StreamStateChangedEventData {
|
|||||||
reason?: StreamSignalReason | null
|
reason?: StreamSignalReason | null
|
||||||
next_retry_ms?: number | 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 }
|
accumulatedDelta = { x: 0, y: 0 }
|
||||||
lastMousePosition.value = { x: 0, y: 0 }
|
lastMousePosition.value = { x: 0, y: 0 }
|
||||||
mousePosition.value = { x: 0, y: 0 }
|
mousePosition.value = { x: 0, y: 0 }
|
||||||
|
|
||||||
if (mouseMode.value === 'relative') {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshUacAvailability() {
|
async function refreshUacAvailability() {
|
||||||
|
|||||||
@@ -27,22 +27,12 @@ import {
|
|||||||
atxConfigApi,
|
atxConfigApi,
|
||||||
extensionsApi,
|
extensionsApi,
|
||||||
redfishConfigApi,
|
redfishConfigApi,
|
||||||
rtspConfigApi,
|
|
||||||
rustdeskConfigApi,
|
|
||||||
systemApi,
|
systemApi,
|
||||||
updateApi,
|
updateApi,
|
||||||
usbApi,
|
usbApi,
|
||||||
vncConfigApi,
|
|
||||||
watchdogConfigApi,
|
watchdogConfigApi,
|
||||||
type EncoderBackendInfo,
|
type EncoderBackendInfo,
|
||||||
type AuthConfig,
|
type AuthConfig,
|
||||||
type RustDeskConfigResponse,
|
|
||||||
type RustDeskStatusResponse,
|
|
||||||
type RustDeskPasswordResponse,
|
|
||||||
type RtspStatusResponse,
|
|
||||||
type RtspConfigUpdate,
|
|
||||||
type VncConfigUpdate,
|
|
||||||
type VncStatusResponse,
|
|
||||||
type WebConfig,
|
type WebConfig,
|
||||||
type UpdateOverviewResponse,
|
type UpdateOverviewResponse,
|
||||||
type UpdateStatusResponse,
|
type UpdateStatusResponse,
|
||||||
@@ -68,6 +58,8 @@ import type {
|
|||||||
import { EasytierConfigMode, FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
import { EasytierConfigMode, FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
||||||
import { toConfigFps } from '@/lib/fps'
|
import { toConfigFps } from '@/lib/fps'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
import { useRemoteAccessSettings } from '@/composables/settings/useRemoteAccessSettings'
|
||||||
|
import { formatHostForUrl } from '@/lib/url'
|
||||||
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
|
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useConsoleLayout, type ConsoleLayout } from '@/composables/useConsoleLayout'
|
import { useConsoleLayout, type ConsoleLayout } from '@/composables/useConsoleLayout'
|
||||||
@@ -200,7 +192,6 @@ const onlineUpgradeSupported = computed(() => systemStore.platform?.mode === 'li
|
|||||||
|
|
||||||
const activeSection = ref<SettingsSectionId>('appearance')
|
const activeSection = ref<SettingsSectionId>('appearance')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saved = ref(false)
|
|
||||||
const saveError = ref('')
|
const saveError = ref('')
|
||||||
const SETTINGS_SECTION_IDS = [
|
const SETTINGS_SECTION_IDS = [
|
||||||
'appearance',
|
'appearance',
|
||||||
@@ -571,80 +562,41 @@ const frpcValidationMessage = computed(() => {
|
|||||||
return ''
|
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 { copy: clipboardCopy } = useClipboard()
|
||||||
const rustdeskLocalConfig = ref({
|
const {
|
||||||
enabled: false,
|
rustdeskConfig,
|
||||||
mode: 'id' as 'id' | 'direct_ip',
|
rustdeskStatus,
|
||||||
codec: 'h264' as 'h264' | 'h265',
|
rustdeskPassword,
|
||||||
direct_access_port: 21118,
|
rustdeskLoading,
|
||||||
rendezvous_server: '',
|
rustdeskCopied,
|
||||||
relay_server: '',
|
rustdeskLocalConfig,
|
||||||
relay_key: '',
|
rustdeskValidationMessage,
|
||||||
})
|
rtspStatus,
|
||||||
|
rtspLoading,
|
||||||
const rustdeskValidationMessage = computed(() => {
|
rtspLocalConfig,
|
||||||
if (
|
vncStatus,
|
||||||
rustdeskLocalConfig.value.mode === 'id' &&
|
vncLoading,
|
||||||
!rustdeskLocalConfig.value.rendezvous_server?.trim()
|
vncLocalConfig,
|
||||||
) {
|
rtspStreamUrl,
|
||||||
return t('extensions.rustdesk.rendezvousServerRequired')
|
vncStreamUrl,
|
||||||
}
|
serviceSaving,
|
||||||
if (
|
loadRustdeskConfig,
|
||||||
rustdeskLocalConfig.value.mode === 'direct_ip' &&
|
loadRustdeskPassword,
|
||||||
(rustdeskLocalConfig.value.direct_access_port < 1 ||
|
saveRustdeskConfig,
|
||||||
rustdeskLocalConfig.value.direct_access_port > 65535)
|
regenerateRustdeskId,
|
||||||
) {
|
regenerateRustdeskPassword,
|
||||||
return t('extensions.rustdesk.directAccessPortInvalid')
|
startRustdesk,
|
||||||
}
|
stopRustdesk,
|
||||||
return ''
|
copyToClipboard,
|
||||||
})
|
loadRtspConfig,
|
||||||
|
saveRtspConfig,
|
||||||
const rtspStatus = ref<RtspStatusResponse | null>(null)
|
startRtsp,
|
||||||
const rtspLoading = ref(false)
|
stopRtsp,
|
||||||
const rtspLocalConfig = ref<RtspConfigUpdate & { password?: string }>({
|
loadVncConfig,
|
||||||
enabled: false,
|
saveVncConfig,
|
||||||
bind: '0.0.0.0',
|
startVnc,
|
||||||
port: 8554,
|
stopVnc,
|
||||||
path: 'live',
|
} = useRemoteAccessSettings({ loading, showValidationError })
|
||||||
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 webServerConfig = ref<WebConfig>({
|
const webServerConfig = ref<WebConfig>({
|
||||||
http_port: 8080,
|
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)
|
const videoSaving = ref<'capture' | 'encoder' | 'connection' | null>(null)
|
||||||
async function saveVideoCard(card: 'capture' | 'encoder' | 'connection') {
|
async function saveVideoCard(card: 'capture' | 'encoder' | 'connection') {
|
||||||
if (videoSaving.value) return
|
if (videoSaving.value) return
|
||||||
@@ -1555,7 +1506,6 @@ async function saveHidSettings(scope: 'features' | 'identity' = 'features') {
|
|||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
hidSaveScope.value = scope
|
hidSaveScope.value = scope
|
||||||
loading.value = true
|
loading.value = true
|
||||||
saved.value = false
|
|
||||||
saveError.value = ''
|
saveError.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1630,8 +1580,6 @@ async function saveHidSettings(scope: 'features' | 'identity' = 'features') {
|
|||||||
if (activeSection.value !== 'hid') {
|
if (activeSection.value !== 'hid') {
|
||||||
await loadSectionData(activeSection.value)
|
await loadSectionData(activeSection.value)
|
||||||
}
|
}
|
||||||
saved.value = true
|
|
||||||
setTimeout(() => (saved.value = false), 2000)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
saveError.value = error instanceof Error ? error.message : t('api.operationFailedDesc')
|
saveError.value = error instanceof Error ? error.message : t('api.operationFailedDesc')
|
||||||
} finally {
|
} 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 {
|
function showValidationError(message: string): boolean {
|
||||||
toast.error(t('api.operationFailed'), {
|
toast.error(t('api.operationFailed'), {
|
||||||
description: message,
|
description: message,
|
||||||
@@ -2192,25 +2095,6 @@ function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
|||||||
return !message || showValidationError(message)
|
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[] {
|
function normalizeBindAddresses(addresses: string[]): string[] {
|
||||||
return addresses.map((addr) => addr.trim()).filter(Boolean)
|
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 () => {
|
onMounted(async () => {
|
||||||
const initialSection = normalizeSettingsSection(route.query.tab)
|
const initialSection = normalizeSettingsSection(route.query.tab)
|
||||||
if (initialSection) {
|
if (initialSection) {
|
||||||
@@ -5759,7 +5397,6 @@ watch(isWindows, () => {
|
|||||||
:aria-label="t('extensions.start') + ' ' + t('extensions.rtsp.title')"
|
:aria-label="t('extensions.start') + ' ' + t('extensions.rtsp.title')"
|
||||||
:disabled="
|
:disabled="
|
||||||
rtspLoading ||
|
rtspLoading ||
|
||||||
rtspStatus?.service_status === 'starting' ||
|
|
||||||
rtspStatus?.service_status === 'running' ||
|
rtspStatus?.service_status === 'running' ||
|
||||||
rtspStatus?.service_status === 'starting'
|
rtspStatus?.service_status === 'starting'
|
||||||
"
|
"
|
||||||
@@ -5927,7 +5564,6 @@ watch(isWindows, () => {
|
|||||||
:aria-label="t('extensions.start') + ' ' + t('extensions.vnc.title')"
|
:aria-label="t('extensions.start') + ' ' + t('extensions.vnc.title')"
|
||||||
:disabled="
|
:disabled="
|
||||||
vncLoading ||
|
vncLoading ||
|
||||||
vncStatus?.service_status === 'starting' ||
|
|
||||||
vncStatus?.service_status === 'running' ||
|
vncStatus?.service_status === 'running' ||
|
||||||
vncStatus?.service_status === 'starting'
|
vncStatus?.service_status === 'starting'
|
||||||
"
|
"
|
||||||
|
|||||||
Reference in New Issue
Block a user