mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
refactor: 删除部分多余的代码和注释
This commit is contained in:
@@ -20,7 +20,6 @@ import { generateUUID } from '@/lib/utils'
|
||||
import { formatFpsValue } from '@/lib/fps'
|
||||
import type { VideoMode } from '@/components/VideoConfigPopover.vue'
|
||||
|
||||
// Components
|
||||
import StatusCard, { type StatusDetail } from '@/components/StatusCard.vue'
|
||||
import ActionBar from '@/components/ActionBar.vue'
|
||||
import InfoBar from '@/components/InfoBar.vue'
|
||||
@@ -84,10 +83,8 @@ const consoleEvents = useConsoleEvents({
|
||||
onDeviceInfo: handleDeviceInfo,
|
||||
})
|
||||
|
||||
// Video mode state
|
||||
const videoMode = ref<VideoMode>('mjpeg')
|
||||
|
||||
// Video state
|
||||
const videoRef = ref<HTMLImageElement | null>(null)
|
||||
const webrtcVideoRef = ref<HTMLVideoElement | null>(null)
|
||||
const videoContainerRef = ref<HTMLDivElement | null>(null)
|
||||
@@ -104,7 +101,6 @@ const streamSignalState = ref<StreamSignalState>('ok')
|
||||
const streamSignalReason = ref<string | null>(null)
|
||||
const streamNextRetryMs = ref<number | null>(null)
|
||||
|
||||
// Video aspect ratio (dynamically updated from actual video dimensions)
|
||||
// Using string format "width/height" to let browser handle the ratio calculation
|
||||
const videoAspectRatio = ref<string | null>(null)
|
||||
|
||||
@@ -123,7 +119,6 @@ const clientsStats = ref<Record<string, ClientStat>>({})
|
||||
// This allows us to identify our own stats in the clients_stat map
|
||||
const myClientId = generateUUID()
|
||||
|
||||
// HID state
|
||||
const mouseMode = ref<'absolute' | 'relative'>('absolute')
|
||||
const pressedKeys = ref<CanonicalKey[]>([])
|
||||
const keyboardLed = computed(() => ({
|
||||
@@ -140,7 +135,6 @@ const isPointerLocked = ref(false) // Track pointer lock state
|
||||
/** Local overlay crosshair position (px, relative to video container); HID uses mousePosition separately */
|
||||
const localCrosshairPos = ref<{ x: number; y: number } | null>(null)
|
||||
|
||||
// Mouse move throttling (60 Hz = ~16.67ms interval)
|
||||
const DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS = 16
|
||||
let mouseMoveSendIntervalMs = DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS
|
||||
let mouseFlushTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -148,7 +142,6 @@ let lastMouseMoveSendTime = 0
|
||||
let pendingMouseMove: { type: 'move' | 'move_abs'; x: number; y: number } | null = null
|
||||
let accumulatedDelta = { x: 0, y: 0 } // For relative mode: accumulate deltas between sends
|
||||
|
||||
// Cursor visibility (from localStorage, updated via storage event)
|
||||
const cursorVisible = ref(localStorage.getItem('hidShowCursor') !== 'false')
|
||||
let interactionListenersBound = false
|
||||
const isConsoleActive = ref(false)
|
||||
@@ -162,7 +155,6 @@ function syncMouseModeFromConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual keyboard state
|
||||
const virtualKeyboardVisible = ref(false)
|
||||
const virtualKeyboardAttached = ref(true)
|
||||
const statsSheetOpen = ref(false)
|
||||
@@ -173,18 +165,15 @@ const virtualKeyboardConsumerEnabled = computed(() => {
|
||||
return hid.otg_functions?.consumer !== false
|
||||
})
|
||||
|
||||
// Change password dialog state
|
||||
const changePasswordDialogOpen = ref(false)
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const changingPassword = ref(false)
|
||||
|
||||
// ttyd (web terminal) state
|
||||
const ttydStatus = ref<{ available: boolean; running: boolean } | null>(null)
|
||||
const showTerminalDialog = ref(false)
|
||||
|
||||
// Theme
|
||||
const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||
|
||||
// Status computed (Device status removed - now only Video, Audio, HID, MSD)
|
||||
@@ -207,9 +196,7 @@ const videoStatus = computed<'connected' | 'connecting' | 'disconnected' | 'erro
|
||||
return 'disconnected'
|
||||
})
|
||||
|
||||
// Convert resolution to short format (e.g., 720p, 1080p, 2K, 4K)
|
||||
function getResolutionShortName(width: number, height: number): string {
|
||||
// Common resolution mappings based on height
|
||||
if (height === 2160 || (height === 2160 && width === 4096)) return '4K'
|
||||
if (height === 1440) return '2K'
|
||||
if (height === 1080) return '1080p'
|
||||
@@ -218,11 +205,9 @@ function getResolutionShortName(width: number, height: number): string {
|
||||
if (height === 600) return '600p'
|
||||
if (height === 1024 && width === 1280) return '1024p'
|
||||
if (height === 960) return '960p'
|
||||
// Fallback: use height + 'p'
|
||||
return `${height}p`
|
||||
}
|
||||
|
||||
// Quick info for status card trigger
|
||||
const videoQuickInfo = computed(() => {
|
||||
const stream = systemStore.stream
|
||||
if (!stream?.resolution) return ''
|
||||
@@ -235,12 +220,10 @@ const videoDetails = computed<StatusDetail[]>(() => {
|
||||
if (!stream) return []
|
||||
const receivedFps = backendFps.value
|
||||
|
||||
// Input (capture) format → output (delivery) mode
|
||||
const inputFmt = stream.format || 'MJPEG'
|
||||
const outputFmt = videoMode.value === 'mjpeg' ? 'MJPEG' : `${videoMode.value.toUpperCase()} (WebRTC)`
|
||||
const formatDisplay = inputFmt === outputFmt ? inputFmt : `${inputFmt} → ${outputFmt}`
|
||||
|
||||
// Target / actual FPS combined
|
||||
const fpsDisplay = `${formatFpsValue(stream.targetFps ?? 0)} / ${formatFpsValue(receivedFps)}`
|
||||
const fpsStatus: StatusDetail['status'] = receivedFps > 5 ? 'ok' : receivedFps > 0 ? 'warning' : undefined
|
||||
|
||||
@@ -269,7 +252,6 @@ const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'
|
||||
}
|
||||
|
||||
// MJPEG mode or WebRTC fallback: check WebSocket HID status
|
||||
// If HID WebSocket has network error, show connecting (yellow)
|
||||
if (hidWs.networkError.value) return 'connecting'
|
||||
|
||||
// If HID WebSocket is not connected (disconnected without error), show disconnected
|
||||
@@ -278,17 +260,14 @@ const hidStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'
|
||||
// If HID backend is unavailable (business error), show disconnected (gray)
|
||||
if (hidWs.hidUnavailable.value) return 'disconnected'
|
||||
|
||||
// Normal status based on system state
|
||||
if (hid?.available && hid.online) return 'connected'
|
||||
if (hid?.available && hid.initialized) return 'connecting'
|
||||
return 'disconnected'
|
||||
})
|
||||
|
||||
// Quick info for HID status card trigger
|
||||
const hidQuickInfo = computed(() => {
|
||||
const hid = systemStore.hid
|
||||
if (!hid?.available) return ''
|
||||
// Show current mode, not hardware capability
|
||||
return mouseMode.value === 'absolute' ? t('statusCard.absolute') : t('statusCard.relative')
|
||||
})
|
||||
|
||||
@@ -387,7 +366,6 @@ const hidDetails = computed<StatusDetail[]>(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Channel (merged with availability / connection state)
|
||||
let channelValue: string
|
||||
let channelStatus: StatusDetail['status']
|
||||
if (videoMode.value !== 'mjpeg') {
|
||||
@@ -422,7 +400,6 @@ const hidDetails = computed<StatusDetail[]>(() => {
|
||||
return details
|
||||
})
|
||||
|
||||
// Audio status computed
|
||||
const audioStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
|
||||
const audio = systemStore.audio
|
||||
if (!audio?.available) return 'disconnected'
|
||||
@@ -431,7 +408,6 @@ const audioStatus = computed<'connected' | 'connecting' | 'disconnected' | 'erro
|
||||
return 'disconnected'
|
||||
})
|
||||
|
||||
// Helper function to translate audio quality
|
||||
function translateAudioQuality(quality: string | undefined): string {
|
||||
if (!quality) return t('common.unknown')
|
||||
const qualityLower = quality.toLowerCase()
|
||||
@@ -463,7 +439,6 @@ const audioDetails = computed<StatusDetail[]>(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// MSD status computed
|
||||
const msdStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
|
||||
const msd = systemStore.msd
|
||||
if (!msd?.available) return 'disconnected'
|
||||
@@ -568,7 +543,6 @@ const hidHoverAlign = computed<'start' | 'end'>(() => {
|
||||
return showMsdStatusCard.value ? 'start' : 'end'
|
||||
})
|
||||
|
||||
// Video handling
|
||||
let retryTimeoutId: number | null = null
|
||||
let retryCount = 0
|
||||
let gracePeriodTimeoutId: number | null = null
|
||||
@@ -618,10 +592,8 @@ async function captureFrameOverlay() {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
// Use JPEG to keep memory reasonable
|
||||
frameOverlayUrl.value = canvas.toDataURL('image/jpeg', 0.7)
|
||||
} catch {
|
||||
// Best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,7 +701,6 @@ function handleVideoLoad() {
|
||||
gracePeriodTimeoutId = null
|
||||
}
|
||||
|
||||
// Reset all error states
|
||||
videoLoading.value = false
|
||||
videoError.value = false
|
||||
videoErrorMessage.value = ''
|
||||
@@ -738,7 +709,6 @@ function handleVideoLoad() {
|
||||
consecutiveErrors = 0
|
||||
clearFrameOverlay()
|
||||
|
||||
// Auto-focus video container for immediate keyboard input
|
||||
const container = videoContainerRef.value
|
||||
if (container && typeof container.focus === 'function') {
|
||||
container.focus()
|
||||
@@ -787,13 +757,11 @@ function handleVideoError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear any pending retries to avoid duplicate attempts
|
||||
if (retryTimeoutId !== null) {
|
||||
clearTimeout(retryTimeoutId)
|
||||
retryTimeoutId = null
|
||||
}
|
||||
|
||||
// Show loading state immediately
|
||||
videoLoading.value = true
|
||||
mjpegFrameReceived.value = false
|
||||
|
||||
@@ -821,7 +789,6 @@ function handleStreamDeviceLost(data: { device: string; reason: string }) {
|
||||
}
|
||||
|
||||
function scheduleWebRTCRecovery() {
|
||||
// Clear any previous timer
|
||||
if (webrtcRecoveryTimerId !== null) {
|
||||
clearTimeout(webrtcRecoveryTimerId)
|
||||
webrtcRecoveryTimerId = null
|
||||
@@ -862,7 +829,6 @@ function scheduleWebRTCRecovery() {
|
||||
videoErrorMessage.value = ''
|
||||
webrtcRecoveryAttempts = 0
|
||||
} else {
|
||||
// Retry
|
||||
scheduleWebRTCRecovery()
|
||||
}
|
||||
} catch {
|
||||
@@ -883,21 +849,17 @@ function handleStreamRecovered(_data: { device: string }) {
|
||||
// Cancel any pending recovery timer – backend is back
|
||||
cancelWebRTCRecovery()
|
||||
|
||||
// Reset video error state
|
||||
videoError.value = false
|
||||
videoErrorMessage.value = ''
|
||||
// Refresh video stream
|
||||
refreshVideo()
|
||||
}
|
||||
|
||||
async function handleAudioStateChanged(data: { streaming: boolean; device: string | null }) {
|
||||
if (!data.streaming) {
|
||||
// Audio stopped, disconnect
|
||||
unifiedAudio.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Audio started streaming
|
||||
if (videoMode.value !== 'mjpeg' && webrtc.isConnected.value) {
|
||||
// WebRTC mode: check if we have an audio track
|
||||
if (!webrtc.audioTrack.value) {
|
||||
@@ -907,9 +869,7 @@ async function handleAudioStateChanged(data: { streaming: boolean; device: strin
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
await connectWebRTCSerial('audio track refresh')
|
||||
// After reconnect, the new session will have audio track
|
||||
// and the watch on audioTrack will add it to MediaStream
|
||||
} else {
|
||||
// We have audio track, ensure it's in MediaStream
|
||||
const currentStream = webrtcVideoRef.value?.srcObject as MediaStream | null
|
||||
if (currentStream && currentStream.getAudioTracks().length === 0) {
|
||||
currentStream.addTrack(webrtc.audioTrack.value)
|
||||
@@ -934,7 +894,6 @@ function handleStreamConfigChanging(data: any) {
|
||||
gracePeriodTimeoutId = null
|
||||
}
|
||||
|
||||
// Reset all counters and states
|
||||
videoRestarting.value = true
|
||||
pendingWebRTCReadyGate = true
|
||||
videoLoading.value = true
|
||||
@@ -952,7 +911,6 @@ function handleStreamConfigChanging(data: any) {
|
||||
}
|
||||
|
||||
async function handleStreamConfigApplied(data: any) {
|
||||
// Reset consecutive error counter for new config
|
||||
consecutiveErrors = 0
|
||||
|
||||
// Start grace period to ignore transient errors
|
||||
@@ -961,7 +919,6 @@ async function handleStreamConfigApplied(data: any) {
|
||||
consecutiveErrors = 0 // Also reset when grace period ends
|
||||
}, GRACE_PERIOD)
|
||||
|
||||
// Refresh video based on current mode
|
||||
videoRestarting.value = true
|
||||
|
||||
// 如果正在进行模式切换,不需要在这里处理(WebRTCReady 事件会处理)
|
||||
@@ -1003,7 +960,6 @@ function handleStreamModeReady(data: { transition_id: string; mode: string }) {
|
||||
}
|
||||
|
||||
function handleStreamModeSwitching(data: { transition_id: string; to_mode: string; from_mode: string }) {
|
||||
// External mode switches: keep UI responsive and avoid black flash
|
||||
if (!isModeSwitching.value) {
|
||||
videoRestarting.value = true
|
||||
videoLoading.value = true
|
||||
@@ -1230,13 +1186,11 @@ function handleDeviceInfo(data: any) {
|
||||
})
|
||||
}
|
||||
|
||||
// Skip mode sync if video config is being changed
|
||||
// This prevents false-positive mode changes during config switching
|
||||
if (data.video?.config_changing) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sync video mode from server's stream_mode
|
||||
if (data.video?.stream_mode) {
|
||||
const serverMode = normalizeServerMode(data.video.stream_mode)
|
||||
if (!serverMode) return
|
||||
@@ -1256,7 +1210,6 @@ function handleDeviceInfo(data: any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stream mode change event from server (WebSocket broadcast)
|
||||
function handleStreamModeChanged(data: { mode: string; previous_mode: string }) {
|
||||
const newMode = normalizeServerMode(data.mode)
|
||||
if (!newMode) return
|
||||
@@ -1267,7 +1220,6 @@ function handleStreamModeChanged(data: { mode: string; previous_mode: string })
|
||||
return
|
||||
}
|
||||
|
||||
// Show toast notification only if this is an external mode change
|
||||
toast.info(t('console.streamModeChanged'), {
|
||||
description: t('console.streamModeChangedDesc', { mode: data.mode.toUpperCase() }),
|
||||
duration: 5000,
|
||||
@@ -1300,7 +1252,6 @@ function refreshVideo() {
|
||||
mjpegTimestamp.value = Date.now()
|
||||
|
||||
// For MJPEG streams, the 'load' event fires when first frame arrives
|
||||
// But on reconnection it may not fire again, so use a timeout as fallback
|
||||
setTimeout(() => {
|
||||
isRefreshingVideo = false
|
||||
// Clear loading state after timeout - if stream failed, error handler will show error
|
||||
@@ -1508,7 +1459,6 @@ async function switchToMJPEG() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to switch to MJPEG mode:', e)
|
||||
// Continue anyway - the mode might already be correct
|
||||
}
|
||||
|
||||
// Step 2: Disconnect WebRTC if connected or session still exists
|
||||
@@ -1542,7 +1492,6 @@ function syncToServerMode(mode: VideoMode) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle video mode change
|
||||
async function handleVideoModeChange(mode: VideoMode) {
|
||||
// 防止重复切换和竞态条件
|
||||
if (mode === videoMode.value) return
|
||||
@@ -1592,10 +1541,8 @@ watch(() => webrtc.videoTrack.value, async (track) => {
|
||||
// Watch for WebRTC audio track changes - update MediaStream when audio arrives
|
||||
watch(() => webrtc.audioTrack.value, async (track) => {
|
||||
if (track && webrtcVideoRef.value && videoMode.value !== 'mjpeg') {
|
||||
// Audio track arrived, update the MediaStream to include it
|
||||
const currentStream = webrtcVideoRef.value.srcObject as MediaStream | null
|
||||
if (currentStream && currentStream.getAudioTracks().length === 0) {
|
||||
// Add audio track to existing stream
|
||||
currentStream.addTrack(track)
|
||||
}
|
||||
}
|
||||
@@ -1607,7 +1554,6 @@ watch(webrtcVideoRef, (el) => {
|
||||
}, { immediate: true })
|
||||
|
||||
// Watch for WebRTC stats to update FPS display
|
||||
// Watch the ref directly with deep: true to detect property changes
|
||||
watch(webrtc.stats, (stats) => {
|
||||
if (videoMode.value !== 'mjpeg' && stats.framesPerSecond > 0) {
|
||||
backendFps.value = Math.round(stats.framesPerSecond)
|
||||
@@ -1626,7 +1572,6 @@ let webrtcReconnectFailures = 0
|
||||
watch(() => webrtc.state.value, (newState, oldState) => {
|
||||
console.log('[WebRTC] State changed:', oldState, '->', newState)
|
||||
|
||||
// Clear any pending reconnect
|
||||
if (webrtcReconnectTimeout) {
|
||||
clearTimeout(webrtcReconnectTimeout)
|
||||
webrtcReconnectTimeout = null
|
||||
@@ -1644,7 +1589,6 @@ watch(() => webrtc.state.value, (newState, oldState) => {
|
||||
})
|
||||
}
|
||||
} else if (newState === 'disconnected' || newState === 'failed') {
|
||||
// Don't immediately set offline - wait for potential reconnect
|
||||
// The device_info event will eventually sync the correct state
|
||||
}
|
||||
}
|
||||
@@ -1653,7 +1597,6 @@ watch(() => webrtc.state.value, (newState, oldState) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-reconnect when disconnected (but was previously connected)
|
||||
if (newState === 'disconnected' && oldState === 'connected' && videoMode.value !== 'mjpeg') {
|
||||
webrtcReconnectTimeout = setTimeout(async () => {
|
||||
if (videoMode.value !== 'mjpeg' && webrtc.state.value === 'disconnected') {
|
||||
@@ -1676,8 +1619,6 @@ watch(() => webrtc.state.value, (newState, oldState) => {
|
||||
}
|
||||
|
||||
// Handle direct 'failed' state (ICE or DTLS failure)
|
||||
// Allow one automatic retry before marking as failed, consistent with
|
||||
// the disconnected->reconnect path that allows 2 failures.
|
||||
if (newState === 'failed' && videoMode.value !== 'mjpeg') {
|
||||
webrtcReconnectFailures += 1
|
||||
if (webrtcReconnectFailures >= 2) {
|
||||
@@ -1706,20 +1647,17 @@ async function toggleFullscreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Theme toggle
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value
|
||||
document.documentElement.classList.toggle('dark', isDark.value)
|
||||
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// Change password function
|
||||
async function handleChangePassword() {
|
||||
if (!newPassword.value || !confirmPassword.value) {
|
||||
toast.error(t('auth.passwordRequired'))
|
||||
@@ -1741,20 +1679,17 @@ async function handleChangePassword() {
|
||||
await authApi.changePassword(currentPassword.value, newPassword.value)
|
||||
toast.success(t('auth.passwordChanged'))
|
||||
|
||||
// Reset form and close dialog
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
changePasswordDialogOpen.value = false
|
||||
} catch (e) {
|
||||
// Error toast is shown by API layer
|
||||
console.info('[ChangePassword] Failed:', e)
|
||||
} finally {
|
||||
changingPassword.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ttyd (web terminal) functions
|
||||
function openTerminal() {
|
||||
if (!ttydStatus.value?.running) return
|
||||
showTerminalDialog.value = true
|
||||
@@ -1764,13 +1699,11 @@ function openTerminalInNewTab() {
|
||||
window.open('/api/terminal/', '_blank')
|
||||
}
|
||||
|
||||
// ATX actions
|
||||
async function handlePowerShort() {
|
||||
try {
|
||||
await atxApi.power('short')
|
||||
await systemStore.fetchAtxState()
|
||||
} catch {
|
||||
// ATX action failed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1779,7 +1712,6 @@ async function handlePowerLong() {
|
||||
await atxApi.power('long')
|
||||
await systemStore.fetchAtxState()
|
||||
} catch {
|
||||
// ATX action failed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1788,7 +1720,6 @@ async function handleReset() {
|
||||
await atxApi.power('reset')
|
||||
await systemStore.fetchAtxState()
|
||||
} catch {
|
||||
// ATX action failed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1801,14 +1732,10 @@ async function handleWol(mac: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// HID error handling - silently handle all HID errors
|
||||
function handleHidError(_error: any, _operation: string) {
|
||||
// All HID errors are silently ignored
|
||||
}
|
||||
|
||||
// HID channel selection: use WebRTC DataChannel when available, fallback to WebSocket
|
||||
function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: number) {
|
||||
// In WebRTC mode with DataChannel ready, use DataChannel for lower latency
|
||||
if (videoMode.value !== 'mjpeg' && webrtc.dataChannelReady.value) {
|
||||
const event: HidKeyboardEvent = {
|
||||
type: type === 'down' ? 'keydown' : 'keyup',
|
||||
@@ -1817,14 +1744,11 @@ function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: nu
|
||||
}
|
||||
const sent = webrtc.sendKeyboard(event)
|
||||
if (sent) return
|
||||
// Fallback to WebSocket if DataChannel send failed
|
||||
}
|
||||
// Use WebSocket as fallback or for MJPEG mode
|
||||
hidApi.keyboard(type, key, modifier).catch(err => handleHidError(err, `keyboard ${type}`))
|
||||
}
|
||||
|
||||
function sendMouseEvent(data: { type: 'move' | 'move_abs' | 'down' | 'up' | 'scroll'; x?: number; y?: number; button?: 'left' | 'right' | 'middle'; scroll?: number }) {
|
||||
// In WebRTC mode with DataChannel ready, use DataChannel for lower latency
|
||||
if (videoMode.value !== 'mjpeg' && webrtc.dataChannelReady.value) {
|
||||
const event: HidMouseEvent = {
|
||||
type: data.type === 'move_abs' ? 'moveabs' : data.type,
|
||||
@@ -1835,50 +1759,37 @@ function sendMouseEvent(data: { type: 'move' | 'move_abs' | 'down' | 'up' | 'scr
|
||||
}
|
||||
const sent = webrtc.sendMouse(event)
|
||||
if (sent) return
|
||||
// Fallback to WebSocket if DataChannel send failed
|
||||
}
|
||||
// Use WebSocket as fallback or for MJPEG mode
|
||||
hidApi.mouse(data).catch(err => handleHidError(err, `mouse ${data.type}`))
|
||||
}
|
||||
|
||||
// Check if a key should be blocked (prevented from default behavior)
|
||||
function shouldBlockKey(e: KeyboardEvent): boolean {
|
||||
// In fullscreen mode, block all keys for maximum capture
|
||||
if (isFullscreen.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Don't block critical browser shortcuts in non-fullscreen mode
|
||||
const key = e.key.toUpperCase()
|
||||
|
||||
// Don't block Ctrl+W (close tab), Ctrl+T (new tab), Ctrl+N (new window)
|
||||
if (e.ctrlKey && ['W', 'T', 'N'].includes(key)) return false
|
||||
|
||||
// Don't block F11 (browser fullscreen toggle)
|
||||
if (key === 'F11') return false
|
||||
|
||||
// Don't block Alt+Tab (already can't capture it anyway)
|
||||
if (e.altKey && key === 'TAB') return false
|
||||
|
||||
// Block everything else
|
||||
return true
|
||||
}
|
||||
|
||||
// Keyboard/Mouse event handling
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const container = videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
// Check focus in non-fullscreen mode
|
||||
if (!isFullscreen.value && !container.contains(document.activeElement)) return
|
||||
|
||||
// Try to block the key if appropriate
|
||||
if (shouldBlockKey(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// Show hint for Meta key in non-fullscreen mode
|
||||
if (!isFullscreen.value && (e.metaKey || e.key === 'Meta')) {
|
||||
toast.info(t('console.metaKeyHint'), {
|
||||
description: t('console.metaKeyHintDesc'),
|
||||
@@ -1905,10 +1816,8 @@ function handleKeyUp(e: KeyboardEvent) {
|
||||
const container = videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
// Check focus in non-fullscreen mode
|
||||
if (!isFullscreen.value && !container.contains(document.activeElement)) return
|
||||
|
||||
// Try to block the key if appropriate
|
||||
if (shouldBlockKey(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -2049,20 +1958,16 @@ function handleMouseMove(e: MouseEvent) {
|
||||
pendingMouseMove = { type: 'move_abs', x, y }
|
||||
requestMouseMoveFlush()
|
||||
} else {
|
||||
// Relative mode: use movementX/Y when pointer is locked
|
||||
if (isPointerLocked.value) {
|
||||
const dx = e.movementX
|
||||
const dy = e.movementY
|
||||
|
||||
// Only accumulate if there's actual movement
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
// Accumulate deltas for throttled sending
|
||||
accumulatedDelta.x += dx
|
||||
accumulatedDelta.y += dy
|
||||
requestMouseMoveFlush()
|
||||
}
|
||||
|
||||
// Update display position (accumulated delta for display only)
|
||||
mousePosition.value = {
|
||||
x: mousePosition.value.x + dx,
|
||||
y: mousePosition.value.y + dy,
|
||||
@@ -2086,13 +1991,11 @@ function flushMouseMoveOnce(): boolean {
|
||||
|
||||
if (accumulatedDelta.x === 0 && accumulatedDelta.y === 0) return false
|
||||
|
||||
// Clamp to i8 range (-127 to 127)
|
||||
const clampedDx = Math.max(-127, Math.min(127, accumulatedDelta.x))
|
||||
const clampedDy = Math.max(-127, Math.min(127, accumulatedDelta.y))
|
||||
|
||||
sendMouseEvent({ type: 'move', x: clampedDx, y: clampedDy })
|
||||
|
||||
// Subtract sent amount (keep remainder for next send if clamped)
|
||||
accumulatedDelta.x -= clampedDx
|
||||
accumulatedDelta.y -= clampedDy
|
||||
return true
|
||||
@@ -2147,13 +2050,11 @@ function requestMouseMoveFlush() {
|
||||
scheduleMouseMoveFlush()
|
||||
}
|
||||
|
||||
// Track pressed mouse button for window-level mouseup handling
|
||||
const pressedMouseButton = ref<'left' | 'right' | 'middle' | null>(null)
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
// Auto-focus the video container to enable keyboard input
|
||||
const container = videoContainerRef.value
|
||||
if (container && document.activeElement !== container) {
|
||||
if (typeof container.focus === 'function') {
|
||||
@@ -2161,7 +2062,6 @@ function handleMouseDown(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// In relative mode, request pointer lock on first click
|
||||
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
||||
requestPointerLock()
|
||||
return
|
||||
@@ -2186,7 +2086,6 @@ function handleMouseUp(e: MouseEvent) {
|
||||
handleMouseUpInternal(e.button)
|
||||
}
|
||||
|
||||
// Window-level mouseup handler (catches releases outside the container)
|
||||
function handleWindowMouseUp(e: MouseEvent) {
|
||||
if (pressedMouseButton.value !== null) {
|
||||
handleMouseUpInternal(e.button)
|
||||
@@ -2201,7 +2100,6 @@ function handleMouseUpInternal(rawButton: number) {
|
||||
|
||||
const button = rawButton === 0 ? 'left' : rawButton === 2 ? 'right' : 'middle'
|
||||
|
||||
// Only send if this button was actually pressed
|
||||
if (pressedMouseButton.value !== button) {
|
||||
return
|
||||
}
|
||||
@@ -2220,7 +2118,6 @@ function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Pointer Lock API for relative mouse mode
|
||||
function requestPointerLock() {
|
||||
const container = videoContainerRef.value
|
||||
if (!container) return
|
||||
@@ -2243,7 +2140,6 @@ function handlePointerLockChange() {
|
||||
isPointerLocked.value = document.pointerLockElement === container
|
||||
|
||||
if (isPointerLocked.value) {
|
||||
// Reset mouse position display when locked
|
||||
mousePosition.value = { x: 0, y: 0 }
|
||||
if (cursorVisible.value && container) {
|
||||
const r = container.getBoundingClientRect()
|
||||
@@ -2269,7 +2165,6 @@ function handleFullscreenChange() {
|
||||
function handleBlur() {
|
||||
pressedKeys.value = []
|
||||
activeModifierMask.value = 0
|
||||
// Release any pressed mouse button when window loses focus
|
||||
if (pressedMouseButton.value !== null) {
|
||||
const button = pressedMouseButton.value
|
||||
pressedMouseButton.value = null
|
||||
@@ -2277,7 +2172,6 @@ function handleBlur() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cursor visibility change from HidConfigPopover
|
||||
function handleCursorVisibilityChange(e: Event) {
|
||||
const customEvent = e as CustomEvent<{ visible: boolean }>
|
||||
cursorVisible.value = customEvent.detail.visible
|
||||
@@ -2365,7 +2259,6 @@ async function activateConsoleView() {
|
||||
void systemStore.fetchAllStates()
|
||||
void configStore.refreshHid().then(() => syncMouseModeFromConfig()).catch(() => {})
|
||||
|
||||
// Ensure HID WebSocket is connected when console becomes active
|
||||
if (!hidWs.connected.value) {
|
||||
hidWs.connect().catch(() => {})
|
||||
}
|
||||
@@ -2395,28 +2288,22 @@ function deactivateConsoleView() {
|
||||
unregisterInteractionListeners()
|
||||
}
|
||||
|
||||
// ActionBar handlers
|
||||
// (MSD and Settings are now handled by ActionBar component directly)
|
||||
|
||||
function handleToggleVirtualKeyboard() {
|
||||
virtualKeyboardVisible.value = !virtualKeyboardVisible.value
|
||||
}
|
||||
|
||||
// Virtual keyboard key event handlers
|
||||
function handleVirtualKeyDown(key: CanonicalKey) {
|
||||
// Add to pressedKeys for InfoBar display
|
||||
if (!pressedKeys.value.includes(key)) {
|
||||
pressedKeys.value = [...pressedKeys.value, key]
|
||||
}
|
||||
}
|
||||
|
||||
function handleVirtualKeyUp(key: CanonicalKey) {
|
||||
// Remove from pressedKeys
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== key)
|
||||
}
|
||||
|
||||
function handleToggleMouseMode() {
|
||||
// Exit pointer lock when switching away from relative mode
|
||||
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
||||
exitPointerLock()
|
||||
}
|
||||
@@ -2424,7 +2311,6 @@ function handleToggleMouseMode() {
|
||||
mouseMode.value = mouseMode.value === 'absolute' ? 'relative' : 'absolute'
|
||||
pendingMouseMove = null
|
||||
accumulatedDelta = { x: 0, y: 0 }
|
||||
// Reset position when switching modes
|
||||
lastMousePosition.value = { x: 0, y: 0 }
|
||||
mousePosition.value = { x: 0, y: 0 }
|
||||
|
||||
@@ -2436,16 +2322,13 @@ function handleToggleMouseMode() {
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
// 1. 先订阅 WebSocket 事件,再连接(内部会 connect)
|
||||
consoleEvents.subscribe()
|
||||
|
||||
// 3. Watch WebSocket connection states and sync to store
|
||||
watch([wsConnected, wsNetworkError], ([connected, netError], [_prevConnected, prevNetError]) => {
|
||||
systemStore.updateWsConnection(connected, netError)
|
||||
|
||||
// Auto-refresh video when network recovers (wsNetworkError: true -> false)
|
||||
if (prevNetError === true && netError === false && connected === true) {
|
||||
refreshVideo()
|
||||
}
|
||||
@@ -2476,7 +2359,6 @@ onMounted(async () => {
|
||||
|
||||
// Note: Video mode is now synced from server via device_info event
|
||||
// The handleDeviceInfo function will automatically switch to the server's mode
|
||||
// localStorage preference is only used when server mode matches
|
||||
try {
|
||||
const modeResp = await streamApi.getMode()
|
||||
const serverMode = normalizeServerMode(modeResp?.mode)
|
||||
@@ -2504,13 +2386,11 @@ onUnmounted(() => {
|
||||
initialModeRestoreDone = false
|
||||
initialModeRestoreInProgress = false
|
||||
|
||||
// Clear mouse flush timer
|
||||
if (mouseFlushTimer !== null) {
|
||||
clearTimeout(mouseFlushTimer)
|
||||
mouseFlushTimer = null
|
||||
}
|
||||
|
||||
// Clear all timers
|
||||
if (retryTimeoutId !== null) {
|
||||
clearTimeout(retryTimeoutId)
|
||||
retryTimeoutId = null
|
||||
@@ -2522,7 +2402,6 @@ onUnmounted(() => {
|
||||
cancelWebRTCRecovery()
|
||||
videoSession.clearWaiters()
|
||||
|
||||
// Reset counters
|
||||
retryCount = 0
|
||||
|
||||
consoleEvents.unsubscribe()
|
||||
@@ -2533,7 +2412,6 @@ onUnmounted(() => {
|
||||
void webrtc.disconnect()
|
||||
}
|
||||
|
||||
// Exit pointer lock if active
|
||||
exitPointerLock()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -106,7 +106,6 @@ const systemStore = useSystemStore()
|
||||
const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Settings state
|
||||
const activeSection = ref('appearance')
|
||||
const mobileMenuOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -127,7 +126,6 @@ const SETTINGS_SECTION_IDS = new Set([
|
||||
'about',
|
||||
])
|
||||
|
||||
// Navigation structure
|
||||
const navGroups = computed(() => [
|
||||
{
|
||||
title: t('settings.system'),
|
||||
@@ -175,10 +173,8 @@ function normalizeSettingsSection(value: unknown): string | null {
|
||||
return SETTINGS_SECTION_IDS.has(value) ? value : null
|
||||
}
|
||||
|
||||
// Theme
|
||||
const theme = ref<'light' | 'dark' | 'system'>('system')
|
||||
|
||||
// Account settings
|
||||
const usernameInput = ref('')
|
||||
const usernamePassword = ref('')
|
||||
const usernameSaving = ref(false)
|
||||
@@ -192,7 +188,6 @@ const passwordSaved = ref(false)
|
||||
const passwordError = ref('')
|
||||
const showPasswords = ref(false)
|
||||
|
||||
// Auth config state
|
||||
const authConfig = ref<AuthConfig>({
|
||||
session_timeout_secs: 3600 * 24,
|
||||
single_user_allow_multiple_sessions: false,
|
||||
@@ -201,7 +196,6 @@ const authConfig = ref<AuthConfig>({
|
||||
})
|
||||
const authConfigLoading = ref(false)
|
||||
|
||||
// Extensions management
|
||||
const extensions = ref<ExtensionsStatus | null>(null)
|
||||
const extensionsLoading = ref(false)
|
||||
const extensionLogs = ref<Record<string, string[]>>({
|
||||
@@ -215,17 +209,14 @@ const showLogs = ref<Record<string, boolean>>({
|
||||
easytier: false,
|
||||
})
|
||||
|
||||
// Terminal dialog
|
||||
const showTerminalDialog = ref(false)
|
||||
|
||||
// Extension config (local edit state)
|
||||
const extConfig = ref({
|
||||
ttyd: { enabled: false, shell: '/bin/bash' },
|
||||
gostc: { enabled: false, addr: '', key: '', tls: true },
|
||||
easytier: { enabled: false, network_name: '', network_secret: '', peer_urls: [] as string[], virtual_ip: '' },
|
||||
})
|
||||
|
||||
// RustDesk config state
|
||||
const rustdeskConfig = ref<RustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(null)
|
||||
@@ -239,7 +230,6 @@ const rustdeskLocalConfig = ref({
|
||||
relay_key: '',
|
||||
})
|
||||
|
||||
// RTSP config state
|
||||
const rtspStatus = ref<RtspStatusResponse | null>(null)
|
||||
const rtspLoading = ref(false)
|
||||
const rtspLocalConfig = ref<RtspConfigUpdate & { password?: string }>({
|
||||
@@ -267,7 +257,6 @@ const rtspStreamUrl = computed(() => {
|
||||
return `rtsp://${host}:${port}/${path}`
|
||||
})
|
||||
|
||||
// Web server config state
|
||||
const webServerConfig = ref<WebConfig>({
|
||||
http_port: 8080,
|
||||
https_port: 8443,
|
||||
@@ -277,14 +266,12 @@ const webServerConfig = ref<WebConfig>({
|
||||
has_custom_cert: false,
|
||||
})
|
||||
const webServerLoading = ref(false)
|
||||
// SSL certificate state
|
||||
const sslCertPem = ref('')
|
||||
const sslKeyPem = ref('')
|
||||
const certSaving = ref(false)
|
||||
const certClearing = ref(false)
|
||||
const showRestartDialog = ref(false)
|
||||
const restarting = ref(false)
|
||||
// Auto-restart flow (no dialog needed for web-config saves)
|
||||
const autoRestarting = ref(false)
|
||||
const autoRestartFailed = ref(false)
|
||||
// For HTTPS targets: can't poll (self-signed cert), show manual link instead
|
||||
@@ -339,7 +326,6 @@ const previewAccessUrl = computed(() => {
|
||||
return `${scheme}://${host}:${port}`
|
||||
})
|
||||
|
||||
// Config
|
||||
interface DeviceConfig {
|
||||
video: Array<{
|
||||
path: string
|
||||
@@ -389,14 +375,12 @@ const config = ref({
|
||||
msd_enabled: false,
|
||||
msd_dir: '',
|
||||
encoder_backend: 'auto',
|
||||
// STUN/TURN settings
|
||||
stun_server: '',
|
||||
turn_server: '',
|
||||
turn_username: '',
|
||||
turn_password: '',
|
||||
})
|
||||
|
||||
// Tracks whether TURN password is configured on the server
|
||||
const hasTurnPassword = ref(false)
|
||||
|
||||
type OtgSelfCheckLevel = 'info' | 'warn' | 'error'
|
||||
@@ -658,7 +642,6 @@ async function onRunVideoEncoderSelfCheckClick() {
|
||||
await runVideoEncoderSelfCheck()
|
||||
}
|
||||
|
||||
// USB devices state
|
||||
const usbDevices = ref<import('@/api').UsbDeviceInfo[]>([])
|
||||
const usbDevicesLoading = ref(false)
|
||||
const usbDevicesError = ref('')
|
||||
@@ -683,11 +666,9 @@ async function confirmUsbReset() {
|
||||
try {
|
||||
await usbApi.resetDevice(usbResetTarget.value.bus_num, usbResetTarget.value.dev_num)
|
||||
} catch {
|
||||
// Error already shown by request helper toast
|
||||
} finally {
|
||||
usbResetLoading.value = false
|
||||
usbResetTarget.value = null
|
||||
// Refresh the list after a short delay for USB re-enumeration
|
||||
setTimeout(() => fetchUsbDevices(), 1500)
|
||||
}
|
||||
}
|
||||
@@ -782,14 +763,12 @@ const isHidFunctionSelectionValid = computed(() => {
|
||||
return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer)
|
||||
})
|
||||
|
||||
// OTG Descriptor settings
|
||||
const otgVendorIdHex = ref('1d6b')
|
||||
const otgProductIdHex = ref('0104')
|
||||
const otgManufacturer = ref('One-KVM')
|
||||
const otgProduct = ref('One-KVM USB Device')
|
||||
const otgSerialNumber = ref('')
|
||||
|
||||
// Validate hex input
|
||||
const validateHex = (event: Event, _field: string) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
input.value = input.value.replace(/[^0-9a-fA-F]/g, '').toLowerCase()
|
||||
@@ -807,7 +786,6 @@ watch(bindMode, (mode) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ATX config state
|
||||
const atxConfig = ref({
|
||||
enabled: false,
|
||||
power: {
|
||||
@@ -833,7 +811,6 @@ const atxConfig = ref({
|
||||
wol_interface: '',
|
||||
})
|
||||
|
||||
// ATX devices for discovery
|
||||
const atxDevices = ref<AtxDevices>({
|
||||
gpio_chips: [],
|
||||
usb_relays: [],
|
||||
@@ -856,7 +833,6 @@ const isSharedAtxSerialRelay = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// Encoder backend
|
||||
const availableBackends = ref<EncoderBackendInfo[]>([])
|
||||
|
||||
const selectedBackendFormats = computed(() => {
|
||||
@@ -921,7 +897,6 @@ const availableFps = computed(() => {
|
||||
return currentRes ? currentRes.fps : []
|
||||
})
|
||||
|
||||
// Keep the selected format aligned with currently selectable formats.
|
||||
watch(
|
||||
selectableFormats,
|
||||
() => {
|
||||
@@ -938,7 +913,6 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Watch for format change to set default resolution
|
||||
watch(() => config.value.video_format, () => {
|
||||
if (availableResolutions.value.length > 0) {
|
||||
const isValid = availableResolutions.value.some(
|
||||
@@ -955,7 +929,6 @@ watch(() => config.value.video_format, () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for resolution change to set default FPS
|
||||
watch(() => [config.value.video_width, config.value.video_height], () => {
|
||||
const fpsList = availableFps.value
|
||||
if (fpsList.length > 0) {
|
||||
@@ -975,7 +948,6 @@ watch(() => authStore.user, (value) => {
|
||||
})
|
||||
|
||||
|
||||
// Format bytes to human readable string
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
@@ -984,7 +956,6 @@ function formatBytes(bytes: number): string {
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
// Theme handling
|
||||
function setTheme(newTheme: 'light' | 'dark' | 'system') {
|
||||
theme.value = newTheme
|
||||
localStorage.setItem('theme', newTheme)
|
||||
@@ -997,7 +968,6 @@ function setTheme(newTheme: 'light' | 'dark' | 'system') {
|
||||
}
|
||||
}
|
||||
|
||||
// Account updates
|
||||
async function changeUsername() {
|
||||
usernameError.value = ''
|
||||
usernameSaved.value = false
|
||||
@@ -1062,17 +1032,13 @@ async function changePassword() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save config using domain-separated APIs
|
||||
async function saveConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
|
||||
try {
|
||||
// Save only config related to the active section.
|
||||
// Sequential awaits: backend ConfigStore uses read-modify-write; parallel PATCH
|
||||
// requests could overwrite each other's section (last writer wins on full JSON).
|
||||
|
||||
// Video config (including encoder and WebRTC/STUN/TURN settings)
|
||||
if (activeSection.value === 'video') {
|
||||
await configStore.updateVideo({
|
||||
device: config.value.video_device || undefined,
|
||||
@@ -1100,7 +1066,6 @@ async function saveConfig() {
|
||||
ch9329_port: config.value.hid_serial_device || undefined,
|
||||
ch9329_baudrate: config.value.hid_serial_baudrate,
|
||||
}
|
||||
// Add descriptor config for OTG backend
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
hidUpdate.otg_descriptor = {
|
||||
vendor_id: parseInt(otgVendorIdHex.value, 16) || 0x1d6b,
|
||||
@@ -1120,7 +1085,6 @@ async function saveConfig() {
|
||||
})
|
||||
}
|
||||
|
||||
// MSD config
|
||||
if (activeSection.value === 'msd') {
|
||||
await configStore.updateMsd({
|
||||
msd_dir: config.value.msd_dir || undefined,
|
||||
@@ -1137,10 +1101,8 @@ async function saveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load config using domain-separated APIs
|
||||
async function loadConfig() {
|
||||
try {
|
||||
// Load all domain configs in parallel
|
||||
const [video, stream, hid, msd] = await Promise.all([
|
||||
configStore.refreshVideo(),
|
||||
configStore.refreshStream(),
|
||||
@@ -1170,17 +1132,14 @@ async function loadConfig() {
|
||||
msd_enabled: msd.enabled || false,
|
||||
msd_dir: msd.msd_dir || '',
|
||||
encoder_backend: stream.encoder || 'auto',
|
||||
// STUN/TURN settings
|
||||
stun_server: stream.stun_server || '',
|
||||
turn_server: stream.turn_server || '',
|
||||
turn_username: stream.turn_username || '',
|
||||
turn_password: '', // Password is never returned from server; set-only field
|
||||
}
|
||||
|
||||
// Track whether TURN password is configured
|
||||
hasTurnPassword.value = stream.has_turn_password || false
|
||||
|
||||
// Load OTG descriptor config
|
||||
if (hid.otg_descriptor) {
|
||||
otgVendorIdHex.value = hid.otg_descriptor.vendor_id?.toString(16).padStart(4, '0') || '1d6b'
|
||||
otgProductIdHex.value = hid.otg_descriptor.product_id?.toString(16).padStart(4, '0') || '0104'
|
||||
@@ -1211,7 +1170,6 @@ async function loadBackends() {
|
||||
}
|
||||
}
|
||||
|
||||
// Auth config functions
|
||||
async function loadAuthConfig() {
|
||||
authConfigLoading.value = true
|
||||
try {
|
||||
@@ -1236,12 +1194,10 @@ async function saveAuthConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Extension management functions
|
||||
async function loadExtensions() {
|
||||
extensionsLoading.value = true
|
||||
try {
|
||||
extensions.value = await extensionsApi.getAll()
|
||||
// Sync config from server
|
||||
if (extensions.value) {
|
||||
const ttyd = extensions.value.ttyd.config
|
||||
extConfig.value.ttyd = {
|
||||
@@ -1359,7 +1315,6 @@ function removeEasytierPeer(index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// ATX management functions
|
||||
async function loadAtxConfig() {
|
||||
try {
|
||||
const config = await configStore.refreshAtx()
|
||||
@@ -1485,7 +1440,6 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// RustDesk management functions
|
||||
async function loadRustdeskConfig() {
|
||||
rustdeskLoading.value = true
|
||||
try {
|
||||
@@ -1573,7 +1527,6 @@ function removeBindAddress(index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// Web server config functions
|
||||
async function loadWebServerConfig() {
|
||||
try {
|
||||
const config = await configStore.refreshWeb()
|
||||
@@ -1668,7 +1621,6 @@ async function pollUntilReady(targetOrigin: string, maxMs = 30000): Promise<bool
|
||||
clearTimeout(tid)
|
||||
if (res.ok) return true
|
||||
} catch {
|
||||
// server still restarting — keep polling
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -1836,7 +1788,6 @@ async function saveRustdeskConfig() {
|
||||
relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key),
|
||||
})
|
||||
await loadRustdeskConfig()
|
||||
// Clear relay_key input after save (it's a password field)
|
||||
rustdeskLocalConfig.value.relay_key = ''
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
@@ -1878,7 +1829,6 @@ async function regenerateRustdeskPassword() {
|
||||
async function startRustdesk() {
|
||||
rustdeskLoading.value = true
|
||||
try {
|
||||
// Enable and save config to start the service
|
||||
await configStore.updateRustdesk({ enabled: true })
|
||||
rustdeskLocalConfig.value.enabled = true
|
||||
await loadRustdeskConfig()
|
||||
@@ -1892,7 +1842,6 @@ async function startRustdesk() {
|
||||
async function stopRustdesk() {
|
||||
rustdeskLoading.value = true
|
||||
try {
|
||||
// Disable and save config to stop the service
|
||||
await configStore.updateRustdesk({ enabled: false })
|
||||
rustdeskLocalConfig.value.enabled = false
|
||||
await loadRustdeskConfig()
|
||||
@@ -1919,7 +1868,6 @@ function getRustdeskServiceStatusText(status: string | undefined): string {
|
||||
case 'stopped': return t('extensions.stopped')
|
||||
case 'not_initialized': return t('extensions.rustdesk.notInitialized')
|
||||
default:
|
||||
// Handle "error: xxx" format
|
||||
if (status.startsWith('error:')) return t('extensions.failed')
|
||||
return status
|
||||
}
|
||||
@@ -1933,7 +1881,6 @@ function getRustdeskRendezvousStatusText(status: string | null | undefined): str
|
||||
case 'connecting': return t('extensions.rustdesk.connecting')
|
||||
case 'disconnected': return t('extensions.rustdesk.disconnected')
|
||||
default:
|
||||
// Handle "error: xxx" format
|
||||
if (status.startsWith('error:')) return t('extensions.failed')
|
||||
return status
|
||||
}
|
||||
@@ -1950,7 +1897,6 @@ function getRustdeskStatusClass(status: string | null | undefined): string {
|
||||
case 'not_initialized':
|
||||
case 'disconnected': return 'bg-gray-400'
|
||||
default:
|
||||
// Handle "error: xxx" format
|
||||
if (status?.startsWith('error:')) return 'bg-red-500'
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
@@ -2058,9 +2004,7 @@ function getRtspStatusClass(status: string | undefined): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
// Load theme preference
|
||||
const storedTheme = localStorage.getItem('theme') as 'light' | 'dark' | 'system' | null
|
||||
if (storedTheme) {
|
||||
theme.value = storedTheme
|
||||
|
||||
@@ -42,20 +42,17 @@ const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Steps: 1 = Account, 2 = Audio/Video, 3 = HID, 4 = Extensions
|
||||
const step = ref(1)
|
||||
const totalSteps = 4
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const slideDirection = ref<'forward' | 'backward'>('forward')
|
||||
|
||||
// Account settings
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
// Form validation states
|
||||
const usernameError = ref('')
|
||||
const passwordError = ref('')
|
||||
const confirmPasswordError = ref('')
|
||||
@@ -63,17 +60,14 @@ const usernameTouched = ref(false)
|
||||
const passwordTouched = ref(false)
|
||||
const confirmPasswordTouched = ref(false)
|
||||
|
||||
// Video settings
|
||||
const videoDevice = ref('')
|
||||
const videoFormat = ref('')
|
||||
const videoResolution = ref('')
|
||||
const videoFps = ref<number | null>(null)
|
||||
|
||||
// Audio settings
|
||||
const audioDevice = ref('')
|
||||
const audioEnabled = ref(true)
|
||||
|
||||
// HID settings
|
||||
const hidBackend = ref('ch9329')
|
||||
const ch9329Port = ref('')
|
||||
const ch9329Baudrate = ref(9600)
|
||||
@@ -83,7 +77,6 @@ const otgMsdEnabled = ref(true)
|
||||
const otgEndpointBudget = ref<'five' | 'six' | 'unlimited'>('six')
|
||||
const otgKeyboardLeds = ref(true)
|
||||
|
||||
// Extension settings
|
||||
const ttydEnabled = ref(false)
|
||||
const ttydAvailable = ref(false)
|
||||
|
||||
@@ -137,7 +130,6 @@ const devices = ref<DeviceInfo>({
|
||||
},
|
||||
})
|
||||
|
||||
// Password strength calculation
|
||||
const passwordStrength = computed(() => {
|
||||
const pwd = password.value
|
||||
if (!pwd) return 0
|
||||
@@ -183,7 +175,6 @@ async function refreshDeviceList() {
|
||||
ttydAvailable.value = result.extensions.ttyd_available
|
||||
}
|
||||
} catch {
|
||||
// keep current list
|
||||
} finally {
|
||||
refreshingDevices.value = false
|
||||
}
|
||||
@@ -195,13 +186,11 @@ const availableFormats = computed(() => {
|
||||
return device?.formats || []
|
||||
})
|
||||
|
||||
// Computed: available resolutions for selected format
|
||||
const availableResolutions = computed(() => {
|
||||
const format = availableFormats.value.find((f) => f.format === videoFormat.value)
|
||||
return format?.resolutions || []
|
||||
})
|
||||
|
||||
// Computed: available FPS for selected resolution
|
||||
const availableFps = computed(() => {
|
||||
const [width, height] = (videoResolution.value || '').split('x').map(Number)
|
||||
const resolution = availableResolutions.value.find(
|
||||
@@ -252,10 +241,8 @@ function applyOtgDefaults() {
|
||||
otgKeyboardLeds.value = otgEndpointBudget.value !== 'five'
|
||||
}
|
||||
|
||||
// Common baud rates for CH9329
|
||||
const baudRates = [9600, 19200, 38400, 57600, 115200]
|
||||
|
||||
// Step labels for the indicator
|
||||
const stepLabels = computed(() => [
|
||||
t('setup.stepAccount'),
|
||||
t('setup.stepAudioVideo'),
|
||||
@@ -263,7 +250,6 @@ const stepLabels = computed(() => [
|
||||
t('setup.stepExtensions'),
|
||||
])
|
||||
|
||||
// Real-time validation functions
|
||||
function validateUsername() {
|
||||
usernameTouched.value = true
|
||||
if (username.value.length === 0) {
|
||||
@@ -284,7 +270,6 @@ function validatePassword() {
|
||||
} else {
|
||||
passwordError.value = ''
|
||||
}
|
||||
// Also validate confirm password if it was touched
|
||||
if (confirmPasswordTouched.value) {
|
||||
validateConfirmPassword()
|
||||
}
|
||||
@@ -335,12 +320,10 @@ watch(videoDevice, (newDevice) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Watch format change to auto-select best resolution
|
||||
watch(videoFormat, () => {
|
||||
videoResolution.value = ''
|
||||
videoFps.value = null
|
||||
if (availableResolutions.value.length > 0) {
|
||||
// Prefer 1080p if available, otherwise highest resolution
|
||||
const r1080 = availableResolutions.value.find((r) => r.width === 1920 && r.height === 1080)
|
||||
const r720 = availableResolutions.value.find((r) => r.width === 1280 && r.height === 720)
|
||||
const best = r1080 || r720 || availableResolutions.value[0]
|
||||
@@ -350,11 +333,9 @@ watch(videoFormat, () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Watch resolution change to auto-select FPS
|
||||
watch(videoResolution, () => {
|
||||
videoFps.value = null
|
||||
if (availableFps.value.length > 0) {
|
||||
// Prefer 30fps if available
|
||||
videoFps.value = availableFps.value.includes(30) ? 30 : availableFps.value[0] || null
|
||||
}
|
||||
})
|
||||
@@ -389,7 +370,6 @@ onMounted(async () => {
|
||||
ch9329Port.value = result.serial[0].path
|
||||
}
|
||||
|
||||
// Auto-select first UDC for OTG
|
||||
if (result.udc.length > 0 && result.udc[0]) {
|
||||
otgUdc.value = result.udc[0].name
|
||||
}
|
||||
@@ -407,7 +387,6 @@ onMounted(async () => {
|
||||
ttydAvailable.value = result.extensions.ttyd_available
|
||||
}
|
||||
} catch {
|
||||
// Use defaults
|
||||
}
|
||||
|
||||
// Load encoder backends
|
||||
@@ -415,10 +394,8 @@ onMounted(async () => {
|
||||
const codecsResult = await streamApi.getCodecs()
|
||||
availableBackends.value = codecsResult.backends || []
|
||||
} catch {
|
||||
// Use defaults
|
||||
}
|
||||
|
||||
// Add keyboard navigation
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
@@ -427,7 +404,6 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Don't interfere with input fields
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return
|
||||
}
|
||||
@@ -446,7 +422,6 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
function validateStep1(): boolean {
|
||||
// Trigger validation for all fields
|
||||
validateUsername()
|
||||
validatePassword()
|
||||
validateConfirmPassword()
|
||||
@@ -521,7 +496,6 @@ async function handleSetup() {
|
||||
|
||||
loading.value = true
|
||||
|
||||
// Parse resolution
|
||||
const [width, height] = (videoResolution.value || '').split('x').map(Number)
|
||||
|
||||
const setupData: Parameters<typeof authStore.setup>[0] = {
|
||||
@@ -529,7 +503,6 @@ async function handleSetup() {
|
||||
password: password.value,
|
||||
}
|
||||
|
||||
// Video settings
|
||||
if (videoDevice.value) {
|
||||
setupData.video_device = videoDevice.value
|
||||
}
|
||||
@@ -544,7 +517,6 @@ async function handleSetup() {
|
||||
setupData.video_fps = toConfigFps(videoFps.value)
|
||||
}
|
||||
|
||||
// HID settings
|
||||
setupData.hid_backend = hidBackend.value
|
||||
if (hidBackend.value === 'ch9329') {
|
||||
setupData.hid_ch9329_port = ch9329Port.value
|
||||
@@ -563,18 +535,15 @@ async function handleSetup() {
|
||||
setupData.encoder_backend = encoderBackend.value
|
||||
}
|
||||
|
||||
// Audio settings
|
||||
if (audioDevice.value && audioDevice.value !== '__none__') {
|
||||
setupData.audio_device = audioDevice.value
|
||||
}
|
||||
|
||||
// Extension settings
|
||||
setupData.ttyd_enabled = ttydEnabled.value
|
||||
|
||||
const success = await authStore.setup(setupData)
|
||||
|
||||
if (success) {
|
||||
// Auto login after setup
|
||||
await authStore.login(username.value, password.value)
|
||||
router.push('/')
|
||||
} else {
|
||||
@@ -584,7 +553,6 @@ async function handleSetup() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// Step icon component helper
|
||||
const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user