mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-04-03 02:16:35 +08:00
feat: 适配 RK 原生 HDMI IN 适配采集
This commit is contained in:
@@ -4,6 +4,7 @@ import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,12 +19,10 @@ import {
|
||||
LogOut,
|
||||
Sun,
|
||||
Moon,
|
||||
Languages,
|
||||
Menu,
|
||||
} from 'lucide-vue-next'
|
||||
import { setLanguage } from '@/i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
@@ -40,11 +39,6 @@ function toggleTheme() {
|
||||
localStorage.setItem('theme', isDark ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
function toggleLanguage() {
|
||||
const newLang = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
setLanguage(newLang)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
@@ -93,10 +87,7 @@ async function handleLogout() {
|
||||
</Button>
|
||||
|
||||
<!-- Language Toggle -->
|
||||
<Button variant="ghost" size="icon" :aria-label="t('common.toggleLanguage')" @click="toggleLanguage">
|
||||
<Languages class="h-4 w-4" />
|
||||
<span class="sr-only">{{ t('common.toggleLanguage') }}</span>
|
||||
</Button>
|
||||
<LanguageToggleButton />
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<DropdownMenu>
|
||||
|
||||
50
web/src/components/LanguageToggleButton.vue
Normal file
50
web/src/components/LanguageToggleButton.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { setLanguage } from '@/i18n'
|
||||
import { Languages } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
class?: HTMLAttributes['class']
|
||||
size?: ButtonVariants['size']
|
||||
variant?: ButtonVariants['variant']
|
||||
labelMode?: 'hidden' | 'current' | 'next'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'icon',
|
||||
variant: 'ghost',
|
||||
labelMode: 'hidden',
|
||||
})
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const currentLanguageLabel = computed(() => (locale.value === 'zh-CN' ? '中文' : 'English'))
|
||||
const nextLanguageLabel = computed(() => (locale.value === 'zh-CN' ? 'English' : '中文'))
|
||||
const buttonLabel = computed(() => (
|
||||
props.labelMode === 'current' ? currentLanguageLabel.value : nextLanguageLabel.value
|
||||
))
|
||||
|
||||
function toggleLanguage() {
|
||||
const newLang = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
setLanguage(newLang)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:variant="variant"
|
||||
:size="size"
|
||||
:class="cn(props.labelMode !== 'hidden' && 'gap-2', props.class)"
|
||||
:aria-label="t('common.toggleLanguage')"
|
||||
@click="toggleLanguage"
|
||||
>
|
||||
<Languages class="h-4 w-4" />
|
||||
<span v-if="props.labelMode !== 'hidden'">{{ buttonLabel }}</span>
|
||||
<span class="sr-only">{{ t('common.toggleLanguage') }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/components/ui/sheet'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import type { WebRTCStats } from '@/composables/useWebRTC'
|
||||
import { formatFpsValue } from '@/lib/fps'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -547,7 +548,7 @@ onUnmounted(() => {
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-medium">{{ t('stats.frameRate') }}</h4>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ currentStats.fps }} fps
|
||||
{{ formatFpsValue(currentStats.fps) }} fps
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type StreamConstraintsResponse,
|
||||
} from '@/api'
|
||||
import { getVideoFormatState, isVideoFormatSelectable } from '@/lib/video-format-support'
|
||||
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -549,7 +550,7 @@ async function applyVideoConfig() {
|
||||
format: selectedFormat.value,
|
||||
width,
|
||||
height,
|
||||
fps: selectedFps.value,
|
||||
fps: toConfigFps(selectedFps.value),
|
||||
})
|
||||
|
||||
toast.success(t('config.applied'))
|
||||
@@ -926,7 +927,7 @@ watch(
|
||||
:value="String(fps)"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ fps }} FPS
|
||||
{{ formatFpsLabel(fps) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -268,9 +268,6 @@ export default {
|
||||
// Help tooltips
|
||||
ch9329Help: 'CH9329 is a serial-to-HID chip connected via serial port. Works with most hardware configurations.',
|
||||
otgHelp: 'USB OTG mode emulates HID devices directly through USB Device Controller. Requires hardware OTG support.',
|
||||
otgAdvanced: 'Advanced: OTG Preset',
|
||||
otgProfile: 'Initial HID Preset',
|
||||
otgProfileDesc: 'Choose the initial OTG HID preset. You can change this later in Settings.',
|
||||
otgLowEndpointHint: 'Detected low-endpoint UDC; Consumer Control Keyboard will be disabled automatically.',
|
||||
videoDeviceHelp: 'Select the video capture device for capturing the remote host display. Usually an HDMI capture card.',
|
||||
videoFormatHelp: 'MJPEG has best compatibility. H.264/H.265 uses less bandwidth but requires encoding support.',
|
||||
|
||||
@@ -268,9 +268,6 @@ export default {
|
||||
// Help tooltips
|
||||
ch9329Help: 'CH9329 是一款串口转 HID 芯片,通过串口连接到主机。适用于大多数硬件配置。',
|
||||
otgHelp: 'USB OTG 模式通过 USB 设备控制器直接模拟 HID 设备。需要硬件支持 USB OTG 功能。',
|
||||
otgAdvanced: '高级:OTG 预设',
|
||||
otgProfile: '初始 HID 预设',
|
||||
otgProfileDesc: '选择 OTG HID 的初始预设,后续可在设置中修改。',
|
||||
otgLowEndpointHint: '检测到低端点 UDC,将自动禁用多媒体键盘。',
|
||||
videoDeviceHelp: '选择用于捕获远程主机画面的视频采集设备。通常是 HDMI 采集卡。',
|
||||
videoFormatHelp: 'MJPEG 格式兼容性最好,H.264/H.265 带宽占用更低但需要编码支持。',
|
||||
|
||||
15
web/src/lib/fps.ts
Normal file
15
web/src/lib/fps.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function formatFpsValue(fps: number): string {
|
||||
if (!Number.isFinite(fps)) return '0'
|
||||
|
||||
const rounded = Math.round(fps * 100) / 100
|
||||
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2).replace(/\.?0+$/, '')
|
||||
}
|
||||
|
||||
export function formatFpsLabel(fps: number): string {
|
||||
return `${formatFpsValue(fps)} FPS`
|
||||
}
|
||||
|
||||
export function toConfigFps(fps: number): number {
|
||||
if (!Number.isFinite(fps)) return 30
|
||||
return Math.round(fps)
|
||||
}
|
||||
@@ -8,6 +8,8 @@ const MJPEG_MODE_SUPPORTED_FORMATS = new Set([
|
||||
'YUYV',
|
||||
'YVYU',
|
||||
'NV12',
|
||||
'NV16',
|
||||
'NV24',
|
||||
'RGB24',
|
||||
'BGR24',
|
||||
])
|
||||
@@ -20,6 +22,7 @@ const CONFIG_SUPPORTED_FORMATS = new Set([
|
||||
'NV12',
|
||||
'NV21',
|
||||
'NV16',
|
||||
'NV24',
|
||||
'YUV420',
|
||||
'RGB24',
|
||||
'BGR24',
|
||||
@@ -32,6 +35,7 @@ const WEBRTC_SUPPORTED_FORMATS = new Set([
|
||||
'NV12',
|
||||
'NV21',
|
||||
'NV16',
|
||||
'NV24',
|
||||
'YUV420',
|
||||
'RGB24',
|
||||
'BGR24',
|
||||
@@ -45,14 +49,10 @@ function isCompressedFormat(formatName: string): boolean {
|
||||
return formatName === 'MJPEG' || formatName === 'JPEG'
|
||||
}
|
||||
|
||||
function isRkmppBackend(backendId?: string): boolean {
|
||||
return backendId?.toLowerCase() === 'rkmpp'
|
||||
}
|
||||
|
||||
export function getVideoFormatState(
|
||||
formatName: string,
|
||||
context: VideoFormatSupportContext,
|
||||
encoderBackend = 'auto',
|
||||
_encoderBackend = 'auto',
|
||||
): VideoFormatState {
|
||||
const normalizedFormat = normalizeFormat(formatName)
|
||||
|
||||
@@ -64,12 +64,6 @@ export function getVideoFormatState(
|
||||
if (CONFIG_SUPPORTED_FORMATS.has(normalizedFormat)) {
|
||||
return 'supported'
|
||||
}
|
||||
if (
|
||||
normalizedFormat === 'NV24'
|
||||
&& isRkmppBackend(encoderBackend)
|
||||
) {
|
||||
return 'supported'
|
||||
}
|
||||
return 'unsupported'
|
||||
}
|
||||
|
||||
@@ -77,14 +71,6 @@ export function getVideoFormatState(
|
||||
return isCompressedFormat(normalizedFormat) ? 'not_recommended' : 'supported'
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedFormat === 'NV24'
|
||||
&& isRkmppBackend(encoderBackend)
|
||||
&& (context === 'h264' || context === 'h265')
|
||||
) {
|
||||
return 'supported'
|
||||
}
|
||||
|
||||
return 'unsupported'
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
|
||||
import { keyboardEventToCanonicalKey, updateModifierMaskForKey } from '@/lib/keyboardMappings'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { generateUUID } from '@/lib/utils'
|
||||
import { formatFpsValue } from '@/lib/fps'
|
||||
import type { VideoMode } from '@/components/VideoConfigPopover.vue'
|
||||
|
||||
// Components
|
||||
@@ -25,6 +26,7 @@ import ActionBar from '@/components/ActionBar.vue'
|
||||
import InfoBar from '@/components/InfoBar.vue'
|
||||
import VirtualKeyboard from '@/components/VirtualKeyboard.vue'
|
||||
import StatsSheet from '@/components/StatsSheet.vue'
|
||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import {
|
||||
@@ -50,16 +52,14 @@ import {
|
||||
LogOut,
|
||||
Sun,
|
||||
Moon,
|
||||
Languages,
|
||||
ChevronDown,
|
||||
Terminal,
|
||||
ExternalLink,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
} from 'lucide-vue-next'
|
||||
import { setLanguage } from '@/i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const systemStore = useSystemStore()
|
||||
const configStore = useConfigStore()
|
||||
@@ -212,7 +212,7 @@ const videoQuickInfo = computed(() => {
|
||||
const stream = systemStore.stream
|
||||
if (!stream?.resolution) return ''
|
||||
const resShort = getResolutionShortName(stream.resolution[0], stream.resolution[1])
|
||||
return `${resShort} ${backendFps.value}fps`
|
||||
return `${resShort} ${formatFpsValue(backendFps.value)}fps`
|
||||
})
|
||||
|
||||
const videoDetails = computed<StatusDetail[]>(() => {
|
||||
@@ -227,8 +227,8 @@ const videoDetails = computed<StatusDetail[]>(() => {
|
||||
{ label: t('statusCard.mode'), value: modeDisplay, status: 'ok' },
|
||||
{ label: t('statusCard.format'), value: stream.format || 'MJPEG' },
|
||||
{ label: t('statusCard.resolution'), value: stream.resolution ? `${stream.resolution[0]}x${stream.resolution[1]}` : '-' },
|
||||
{ label: t('statusCard.targetFps'), value: String(stream.targetFps ?? 0) },
|
||||
{ label: t('statusCard.fps'), value: String(receivedFps), status: receivedFps > 5 ? 'ok' : receivedFps > 0 ? 'warning' : undefined },
|
||||
{ label: t('statusCard.targetFps'), value: formatFpsValue(stream.targetFps ?? 0) },
|
||||
{ label: t('statusCard.fps'), value: formatFpsValue(receivedFps), status: receivedFps > 5 ? 'ok' : receivedFps > 0 ? 'warning' : undefined },
|
||||
]
|
||||
|
||||
// Show network error if WebSocket has network issue
|
||||
@@ -875,7 +875,7 @@ async function handleStreamConfigApplied(data: any) {
|
||||
videoRestarting.value = false
|
||||
|
||||
toast.success(t('console.videoRestarted'), {
|
||||
description: `${data.device} - ${data.resolution[0]}x${data.resolution[1]} @ ${data.fps}fps`,
|
||||
description: `${data.device} - ${data.resolution[0]}x${data.resolution[1]} @ ${formatFpsValue(data.fps)}fps`,
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
@@ -1458,12 +1458,6 @@ function toggleTheme() {
|
||||
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
// Language toggle
|
||||
function toggleLanguage() {
|
||||
const newLang = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
setLanguage(newLang)
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
await authStore.logout()
|
||||
@@ -2306,9 +2300,7 @@ onUnmounted(() => {
|
||||
</Button>
|
||||
|
||||
<!-- Language Toggle -->
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" :aria-label="t('common.toggleLanguage')" @click="toggleLanguage">
|
||||
<Languages class="h-4 w-4" />
|
||||
</Button>
|
||||
<LanguageToggleButton class="h-8 w-8 hidden md:flex" />
|
||||
|
||||
<!-- User Menu -->
|
||||
<DropdownMenu>
|
||||
@@ -2324,9 +2316,13 @@ onUnmounted(() => {
|
||||
<Moon v-else class="h-4 w-4 mr-2" />
|
||||
{{ isDark ? t('settings.lightMode') : t('settings.darkMode') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem class="md:hidden" @click="toggleLanguage">
|
||||
<Languages class="h-4 w-4 mr-2" />
|
||||
{{ locale === 'zh-CN' ? 'English' : '中文' }}
|
||||
<DropdownMenuItem as-child class="md:hidden p-0">
|
||||
<LanguageToggleButton
|
||||
label-mode="next"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="w-full justify-start rounded-sm px-2 py-1.5 font-normal shadow-none"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator class="md:hidden" />
|
||||
<DropdownMenuItem @click="changePasswordDialogOpen = true">
|
||||
|
||||
@@ -3,15 +3,11 @@ import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
setLanguage,
|
||||
getCurrentLanguage,
|
||||
type SupportedLocale,
|
||||
} from '@/i18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||
import { Monitor, Lock, Eye, EyeOff, User } from 'lucide-vue-next'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -19,18 +15,12 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const currentLanguage = ref<SupportedLocale>(getCurrentLanguage())
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const showPassword = ref(false)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
function handleLanguageChange(lang: SupportedLocale) {
|
||||
currentLanguage.value = lang
|
||||
setLanguage(lang)
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!username.value) {
|
||||
error.value = t('auth.enterUsername')
|
||||
@@ -60,21 +50,8 @@ async function handleLogin() {
|
||||
<template>
|
||||
<div class="min-h-screen min-h-dvh flex items-center justify-center bg-background p-4">
|
||||
<Card class="relative w-full max-w-sm">
|
||||
<div class="absolute top-4 right-4 flex gap-2">
|
||||
<Button
|
||||
:variant="currentLanguage === 'zh-CN' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="handleLanguageChange('zh-CN')"
|
||||
>
|
||||
中文
|
||||
</Button>
|
||||
<Button
|
||||
:variant="currentLanguage === 'en-US' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="handleLanguageChange('en-US')"
|
||||
>
|
||||
English
|
||||
</Button>
|
||||
<div class="absolute top-4 right-4">
|
||||
<LanguageToggleButton />
|
||||
</div>
|
||||
|
||||
<CardHeader class="space-y-2 pt-10 text-center sm:pt-12">
|
||||
|
||||
@@ -37,10 +37,11 @@ import type {
|
||||
OtgHidProfile,
|
||||
OtgHidFunctions,
|
||||
} from '@/types/generated'
|
||||
import { setLanguage } from '@/i18n'
|
||||
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { getVideoFormatState } from '@/lib/video-format-support'
|
||||
import AppLayout from '@/components/AppLayout.vue'
|
||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -85,7 +86,7 @@ import {
|
||||
Radio,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const { t, te, locale } = useI18n()
|
||||
const { t, te } = useI18n()
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
const configStore = useConfigStore()
|
||||
@@ -902,13 +903,6 @@ function setTheme(newTheme: 'light' | 'dark' | 'system') {
|
||||
}
|
||||
}
|
||||
|
||||
// Language handling
|
||||
function handleLanguageChange(lang: string) {
|
||||
if (lang === 'zh-CN' || lang === 'en-US') {
|
||||
setLanguage(lang)
|
||||
}
|
||||
}
|
||||
|
||||
// Account updates
|
||||
async function changeUsername() {
|
||||
usernameError.value = ''
|
||||
@@ -991,7 +985,7 @@ async function saveConfig() {
|
||||
format: config.value.video_format || undefined,
|
||||
width: config.value.video_width,
|
||||
height: config.value.video_height,
|
||||
fps: config.value.video_fps,
|
||||
fps: toConfigFps(config.value.video_fps),
|
||||
})
|
||||
)
|
||||
// Save Stream/Encoder and STUN/TURN config together
|
||||
@@ -2022,9 +2016,8 @@ watch(() => route.query.tab, (tab) => {
|
||||
<CardDescription>{{ t('settings.languageDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex gap-2">
|
||||
<Button :variant="locale === 'zh-CN' ? 'default' : 'outline'" size="sm" @click="handleLanguageChange('zh-CN')">中文</Button>
|
||||
<Button :variant="locale === 'en-US' ? 'default' : 'outline'" size="sm" @click="handleLanguageChange('en-US')">English</Button>
|
||||
<div class="flex">
|
||||
<LanguageToggleButton variant="outline" size="sm" label-mode="current" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -2132,8 +2125,8 @@ watch(() => route.query.tab, (tab) => {
|
||||
<div class="space-y-2">
|
||||
<Label for="video-fps">{{ t('settings.frameRate') }}</Label>
|
||||
<select id="video-fps" v-model.number="config.video_fps" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!config.video_format">
|
||||
<option v-for="fps in availableFps" :key="fps" :value="fps">{{ fps }} FPS</option>
|
||||
<option v-if="!availableFps.includes(config.video_fps)" :value="config.video_fps">{{ config.video_fps }} FPS</option>
|
||||
<option v-for="fps in availableFps" :key="fps" :value="fps">{{ formatFpsLabel(fps) }}</option>
|
||||
<option v-if="!availableFps.includes(config.video_fps)" :value="config.video_fps">{{ formatFpsLabel(config.video_fps) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,8 @@ import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { configApi, streamApi, type EncoderBackendInfo } from '@/api'
|
||||
import {
|
||||
supportedLanguages,
|
||||
setLanguage,
|
||||
getCurrentLanguage,
|
||||
type SupportedLocale,
|
||||
} from '@/i18n'
|
||||
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
|
||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -21,12 +17,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
@@ -44,7 +34,6 @@ import {
|
||||
Keyboard,
|
||||
Check,
|
||||
HelpCircle,
|
||||
Languages,
|
||||
Puzzle,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
@@ -52,14 +41,6 @@ const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Language switcher
|
||||
const currentLanguage = ref<SupportedLocale>(getCurrentLanguage())
|
||||
|
||||
function switchLanguage(lang: SupportedLocale) {
|
||||
currentLanguage.value = lang
|
||||
setLanguage(lang)
|
||||
}
|
||||
|
||||
// Steps: 1 = Account, 2 = Audio/Video, 3 = HID, 4 = Extensions
|
||||
const step = ref(1)
|
||||
const totalSteps = 4
|
||||
@@ -96,14 +77,10 @@ const hidBackend = ref('ch9329')
|
||||
const ch9329Port = ref('')
|
||||
const ch9329Baudrate = ref(9600)
|
||||
const otgUdc = ref('')
|
||||
const hidOtgProfile = ref('full')
|
||||
const hidOtgProfile = ref('full_no_consumer')
|
||||
const otgMsdEnabled = ref(true)
|
||||
const otgEndpointBudget = ref<'five' | 'six' | 'unlimited'>('six')
|
||||
const otgKeyboardLeds = ref(true)
|
||||
const otgProfileTouched = ref(false)
|
||||
const otgEndpointBudgetTouched = ref(false)
|
||||
const otgKeyboardLedsTouched = ref(false)
|
||||
const showAdvancedOtg = ref(false)
|
||||
|
||||
// Extension settings
|
||||
const ttydEnabled = ref(false)
|
||||
@@ -237,57 +214,17 @@ const otgRequiredEndpoints = computed(() => {
|
||||
return endpoints
|
||||
})
|
||||
|
||||
const otgProfileHasKeyboard = computed(() =>
|
||||
hidOtgProfile.value === 'full'
|
||||
|| hidOtgProfile.value === 'full_no_consumer'
|
||||
|| hidOtgProfile.value === 'legacy_keyboard'
|
||||
)
|
||||
|
||||
const isOtgEndpointBudgetValid = computed(() => {
|
||||
const limit = endpointLimitForBudget(otgEndpointBudget.value)
|
||||
return limit === null || otgRequiredEndpoints.value <= limit
|
||||
})
|
||||
|
||||
const otgEndpointUsageText = computed(() => {
|
||||
const limit = endpointLimitForBudget(otgEndpointBudget.value)
|
||||
if (limit === null) {
|
||||
return t('settings.otgEndpointUsageUnlimited', { used: otgRequiredEndpoints.value })
|
||||
}
|
||||
return t('settings.otgEndpointUsage', { used: otgRequiredEndpoints.value, limit })
|
||||
})
|
||||
|
||||
function applyOtgDefaults() {
|
||||
if (hidBackend.value !== 'otg') return
|
||||
|
||||
const recommendedBudget = defaultOtgEndpointBudgetForUdc(otgUdc.value)
|
||||
if (!otgEndpointBudgetTouched.value) {
|
||||
otgEndpointBudget.value = recommendedBudget
|
||||
}
|
||||
if (!otgProfileTouched.value) {
|
||||
hidOtgProfile.value = 'full_no_consumer'
|
||||
}
|
||||
if (!otgKeyboardLedsTouched.value) {
|
||||
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
|
||||
}
|
||||
}
|
||||
|
||||
function onOtgProfileChange(value: unknown) {
|
||||
hidOtgProfile.value = typeof value === 'string' ? value : 'full'
|
||||
otgProfileTouched.value = true
|
||||
}
|
||||
|
||||
function onOtgEndpointBudgetChange(value: unknown) {
|
||||
otgEndpointBudget.value =
|
||||
value === 'five' || value === 'six' || value === 'unlimited' ? value : 'six'
|
||||
otgEndpointBudgetTouched.value = true
|
||||
if (!otgKeyboardLedsTouched.value) {
|
||||
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
|
||||
}
|
||||
}
|
||||
|
||||
function onOtgKeyboardLedsChange(value: boolean) {
|
||||
otgKeyboardLeds.value = value
|
||||
otgKeyboardLedsTouched.value = true
|
||||
otgEndpointBudget.value = defaultOtgEndpointBudgetForUdc(otgUdc.value)
|
||||
hidOtgProfile.value = 'full_no_consumer'
|
||||
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
|
||||
}
|
||||
|
||||
// Common baud rates for CH9329
|
||||
@@ -412,12 +349,6 @@ watch(otgUdc, () => {
|
||||
applyOtgDefaults()
|
||||
})
|
||||
|
||||
watch(showAdvancedOtg, (open) => {
|
||||
if (open) {
|
||||
applyOtgDefaults()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result = await configApi.listDevices()
|
||||
@@ -585,7 +516,7 @@ async function handleSetup() {
|
||||
setupData.video_height = height
|
||||
}
|
||||
if (videoFps.value) {
|
||||
setupData.video_fps = videoFps.value
|
||||
setupData.video_fps = toConfigFps(videoFps.value)
|
||||
}
|
||||
|
||||
// HID settings
|
||||
@@ -637,27 +568,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
<Card class="w-full max-w-lg relative">
|
||||
<!-- Language Switcher -->
|
||||
<div class="absolute top-4 right-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="gap-2">
|
||||
<Languages class="w-4 h-4" />
|
||||
<span class="text-sm">
|
||||
{{ supportedLanguages.find((l) => l.code === currentLanguage)?.name }}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
v-for="lang in supportedLanguages"
|
||||
:key="lang.code"
|
||||
:class="{ 'bg-accent': lang.code === currentLanguage }"
|
||||
@click="switchLanguage(lang.code)"
|
||||
>
|
||||
<span class="mr-2">{{ lang.flag }}</span>
|
||||
{{ lang.name }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<LanguageToggleButton />
|
||||
</div>
|
||||
|
||||
<CardHeader class="text-center space-y-2 pt-10 sm:pt-12">
|
||||
@@ -879,7 +790,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="fps in availableFps" :key="fps" :value="fps">
|
||||
{{ fps }} FPS
|
||||
{{ formatFpsLabel(fps) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -1039,78 +950,6 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
{{ t('setup.noUdcDevices') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 border rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between p-3 text-left hover:bg-muted/50 rounded-lg transition-colors"
|
||||
:aria-label="t('setup.advancedOtg')"
|
||||
@click="showAdvancedOtg = !showAdvancedOtg"
|
||||
>
|
||||
<span class="text-sm font-medium">
|
||||
{{ t('setup.otgAdvanced') }} ({{ t('common.optional') }})
|
||||
</span>
|
||||
<ChevronRight
|
||||
class="h-4 w-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': showAdvancedOtg }"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="showAdvancedOtg" class="px-3 pb-3 space-y-3">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('setup.otgProfileDesc') }}
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<Label for="otgProfile">{{ t('setup.otgProfile') }}</Label>
|
||||
<Select :model-value="hidOtgProfile" @update:modelValue="onOtgProfileChange">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="full">{{ t('settings.otgProfileFull') }}</SelectItem>
|
||||
<SelectItem value="full_no_consumer">{{ t('settings.otgProfileFullNoConsumer') }}</SelectItem>
|
||||
<SelectItem value="legacy_keyboard">{{ t('settings.otgProfileLegacyKeyboard') }}</SelectItem>
|
||||
<SelectItem value="legacy_mouse_relative">{{ t('settings.otgProfileLegacyMouseRelative') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="otgEndpointBudget">{{ t('settings.otgEndpointBudget') }}</Label>
|
||||
<Select :model-value="otgEndpointBudget" @update:modelValue="onOtgEndpointBudgetChange">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="five">5</SelectItem>
|
||||
<SelectItem value="six">6</SelectItem>
|
||||
<SelectItem value="unlimited">{{ t('settings.otgEndpointBudgetUnlimited') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ otgEndpointUsageText }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
|
||||
<div>
|
||||
<Label>{{ t('settings.otgKeyboardLeds') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.otgKeyboardLedsDesc') }}</p>
|
||||
</div>
|
||||
<Switch :model-value="otgKeyboardLeds" :disabled="!otgProfileHasKeyboard" @update:model-value="onOtgKeyboardLedsChange" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
|
||||
<div>
|
||||
<Label>{{ t('settings.otgFunctionMsd') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMsdDesc') }}</p>
|
||||
</div>
|
||||
<Switch v-model="otgMsdEnabled" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('settings.otgEndpointBudgetHint') }}
|
||||
</p>
|
||||
<p v-if="!isOtgEndpointBudgetValid" class="text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.otgEndpointExceeded', { used: otgRequiredEndpoints, limit: otgEndpointBudget === 'unlimited' ? t('settings.otgEndpointBudgetUnlimited') : otgEndpointBudget === 'five' ? '5' : '6' }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user