refactor: 删除部分多余的代码和注释

This commit is contained in:
mofeng-git
2026-05-01 17:31:04 +08:00
parent 74035f8e12
commit d8e7de74a6
165 changed files with 2960 additions and 9917 deletions

View File

@@ -51,7 +51,6 @@ const { t, locale } = useI18n()
const router = useRouter()
const systemStore = useSystemStore()
// Overflow menu state
const overflowMenuOpen = ref(false)
const hidBackend = computed(() => (systemStore.hid?.backend ?? '').toLowerCase())
@@ -79,7 +78,6 @@ const emit = defineEmits<{
(e: 'openTerminal'): void
}>()
// Desktop toolbar popover/dialog state
const pasteOpen = ref(false)
const atxOpen = ref(false)
const videoPopoverOpen = ref(false)
@@ -88,7 +86,6 @@ const audioPopoverOpen = ref(false)
const msdDialogOpen = ref(false)
const extensionOpen = ref(false)
// Mobile Sheet state
const mobileAtxOpen = ref(false)
const mobilePasteOpen = ref(false)
const mobileAtxOpenTime = ref(0)
@@ -117,7 +114,6 @@ const openMobilePaste = () => openFromOverflow(() => {
mobilePasteOpenTime.value = Date.now()
})
// ── Adaptive overflow: measure real width, show as many items as fit ──
const barRef = ref<HTMLElement | null>(null)
const measureRef = ref<HTMLElement | null>(null)
@@ -146,11 +142,9 @@ const ITEM_SPECS: ItemSpec[] = [
{ id: 'settings', side: 'right' },
]
// Measured widths from DOM (icon-only and with-label)
const measuredWidths = ref<Map<CollapsibleItem, { icon: number; label: number }>>(new Map())
const measurementReady = ref(false)
// Measure button widths from hidden measurement container
const measureButtonWidths = async () => {
await nextTick()
if (!measureRef.value) return
@@ -162,7 +156,6 @@ const measureButtonWidths = async () => {
const labelEl = measureRef.value.querySelector(`[data-measure="${spec.id}-label"]`) as HTMLElement
if (iconEl && labelEl) {
// Add small buffer (8px) for gaps and rounding errors
newWidths.set(spec.id, {
icon: Math.ceil(iconEl.offsetWidth) + 8,
label: Math.ceil(labelEl.offsetWidth) + 8,
@@ -191,17 +184,13 @@ onUnmounted(() => {
resizeObserver?.disconnect()
})
// Re-measure when locale changes (different text widths)
watch(locale, () => {
measurementReady.value = false
measureButtonWidths()
})
// Fixed-width budget for always-visible items (right side):
// keyboard + fullscreen + potential overflow button + gaps
const RIGHT_FIXED_PX = 120
// First 3 items (video/audio/hid) are always visible
const collapsibleItems = computed(() => {
const items = ITEM_SPECS.slice(3).filter(item => {
if (item.id === 'msd' && !showMsd.value) return false
@@ -210,27 +199,22 @@ const collapsibleItems = computed(() => {
return items
})
// Determine which collapsible items are visible (icon-only or with label)
const visibleSet = computed(() => {
if (!measurementReady.value) {
// Fallback to hardcoded estimates during initial render
return new Map<CollapsibleItem, 'icon' | 'label'>()
}
const available = barWidth.value - RIGHT_FIXED_PX
// Measure actual width of always-visible items (video/audio/hid)
let used = 0
if (barRef.value) {
const leftContainer = barRef.value.querySelector('.left-buttons') as HTMLElement
if (leftContainer) {
// Get width of first 3 children (video/audio/hid)
const children = Array.from(leftContainer.children).slice(0, 3) as HTMLElement[]
used = children.reduce((sum, el) => sum + el.offsetWidth, 0)
}
}
// If measurement failed, use estimate
if (used === 0) used = 330
const result = new Map<CollapsibleItem, 'icon' | 'label'>()

View File

@@ -33,14 +33,12 @@ const { t } = useI18n()
const activeTab = ref('atx')
// ATX state
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
let powerStateTimer: number | null = null
// Decouple action data from dialog visibility to prevent race conditions
const pendingAction = ref<'short' | 'long' | 'reset' | null>(null)
const confirmDialogOpen = ref(false)
// WOL state
const wolMacAddress = ref('')
const wolHistory = ref<string[]>([])
const wolSending = ref(false)
@@ -95,10 +93,8 @@ const confirmDescription = computed(() => {
default: return ''
}
})
// MAC address validation
const isValidMac = computed(() => {
const mac = wolMacAddress.value.trim()
// Support formats: AA:BB:CC:DD:EE:FF or AA-BB-CC-DD-EE-FF or AABBCCDDEEFF
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{12})$/
return macRegex.test(mac)
})
@@ -107,7 +103,6 @@ function sendWol() {
if (!isValidMac.value) return
wolSending.value = true
// Normalize MAC address
let mac = wolMacAddress.value.trim().toUpperCase()
if (mac.length === 12) {
mac = mac.match(/.{2}/g)!.join(':')
@@ -117,7 +112,6 @@ function sendWol() {
emit('wol', mac)
// Optimistic update, then sync from server after request likely completes
wolHistory.value = [mac, ...wolHistory.value.filter(item => item !== mac)].slice(0, 5)
setTimeout(() => {
loadWolHistory().catch(() => {})

View File

@@ -42,10 +42,8 @@ const configStore = useConfigStore()
const systemStore = useSystemStore()
const unifiedAudio = getUnifiedAudio()
// === Playback Control (immediate effect) ===
const localVolume = ref([unifiedAudio.volume.value * 100])
// Volume change - immediate effect, also triggers connection if needed
async function handleVolumeChange(value: number[] | undefined) {
if (!value || value.length === 0 || value[0] === undefined) return
@@ -53,7 +51,6 @@ async function handleVolumeChange(value: number[] | undefined) {
unifiedAudio.setVolume(newVolume)
localVolume.value = value
// If backend is streaming but audio not connected, connect now (user gesture)
if (newVolume > 0 && systemStore.audio?.streaming && !unifiedAudio.connected.value) {
console.log('[Audio] User adjusted volume, connecting unified audio')
try {
@@ -64,17 +61,14 @@ async function handleVolumeChange(value: number[] | undefined) {
}
}
// === Device Settings (requires apply) ===
const devices = ref<AudioDevice[]>([])
const loadingDevices = ref(false)
const applying = ref(false)
// Config values
const audioEnabled = ref(false)
const selectedDevice = ref('')
const selectedQuality = ref<'voice' | 'balanced' | 'high'>('balanced')
// Load device list
async function loadDevices() {
loadingDevices.value = true
try {
@@ -87,7 +81,6 @@ async function loadDevices() {
}
}
// Initialize from current config
function initializeFromCurrent() {
const audio = configStore.audio
if (audio) {
@@ -96,46 +89,36 @@ function initializeFromCurrent() {
selectedQuality.value = (audio.quality as 'voice' | 'balanced' | 'high') || 'balanced'
}
// Sync playback control state
localVolume.value = [unifiedAudio.volume.value * 100]
}
// Apply device configuration
async function applyConfig() {
applying.value = true
try {
// Update config
await configStore.updateAudio({
enabled: audioEnabled.value,
device: selectedDevice.value,
quality: selectedQuality.value,
})
// If enabled and device is selected, try to start audio stream
if (audioEnabled.value && selectedDevice.value) {
try {
// Restore default volume BEFORE starting audio
// This ensures handleAudioStateChanged sees the correct volume
if (localVolume.value[0] === 0) {
localVolume.value = [100]
unifiedAudio.setVolume(1)
}
await audioApi.start()
// ConsoleView will react when system.device_info reflects streaming=true.
} catch (startError) {
// Audio start failed - config was saved but streaming not started
console.info('[AudioConfig] Audio start failed:', startError)
}
} else if (!audioEnabled.value) {
// Reset volume to 0 when disabling audio
localVolume.value = [0]
unifiedAudio.setVolume(0)
try {
await audioApi.stop()
} catch {
// Ignore stop errors
}
unifiedAudio.disconnect()
}
@@ -148,7 +131,6 @@ async function applyConfig() {
}
}
// Watch popover open state
watch(() => props.open, (isOpen) => {
if (!isOpen) return

View File

@@ -54,7 +54,6 @@ function loadMouseMoveSendIntervalFromStorage(): number {
)
}
// Mouse Settings (real-time)
const mouseThrottle = ref<number>(
loadMouseMoveSendIntervalFromStorage()
)
@@ -62,9 +61,7 @@ const showCursor = ref<boolean>(
localStorage.getItem('hidShowCursor') !== 'false' // default true
)
// Watch showCursor changes and sync to localStorage + notify ConsoleView
watch(showCursor, (newValue, oldValue) => {
// Only sync if value actually changed (avoid triggering on initialization)
if (newValue !== oldValue) {
localStorage.setItem('hidShowCursor', newValue ? 'true' : 'false')
window.dispatchEvent(new CustomEvent('hidCursorVisibilityChanged', {
@@ -78,7 +75,6 @@ const hidBackend = ref<HidBackend>(HidBackend.None)
const devicePath = ref<string>('')
const baudrate = ref<number>(9600)
// UI state
const applying = ref(false)
const loadingDevices = ref(false)
@@ -86,7 +82,6 @@ const loadingDevices = ref(false)
const serialDevices = ref<Array<{ path: string; name: string }>>([])
const udcDevices = ref<Array<{ name: string }>>([])
// Button display text - simplified to just show label
const buttonText = computed(() => t('actionbar.hidConfig'))
// Available device paths based on backend type
@@ -117,9 +112,7 @@ async function loadDevices() {
}
}
// Initialize from current config
function initializeFromCurrent() {
// Re-sync real-time settings from localStorage
mouseThrottle.value = loadMouseMoveSendIntervalFromStorage()
const storedCursor = localStorage.getItem('hidShowCursor') !== 'false'
@@ -140,7 +133,6 @@ function initializeFromCurrent() {
}
}
// Toggle mouse mode (real-time)
function toggleMouseMode() {
const newMode = props.mouseMode === 'absolute' ? 'relative' : 'absolute'
emit('update:mouseMode', newMode)
@@ -154,14 +146,11 @@ function toggleMouseMode() {
})
}
// Update mouse throttle (real-time)
function handleThrottleChange(value: number[] | undefined) {
if (!value || value.length === 0 || value[0] === undefined) return
const throttleValue = clampMouseMoveSendIntervalMs(value[0])
mouseThrottle.value = throttleValue
// Save to localStorage
localStorage.setItem('hidMouseThrottle', String(throttleValue))
// Notify ConsoleView (storage event doesn't fire in same tab)
window.dispatchEvent(new CustomEvent('hidMouseSendIntervalChanged', {
detail: { intervalMs: throttleValue },
}))
@@ -191,7 +180,6 @@ function handleDevicePathChange(path: unknown) {
devicePath.value = path
}
// Handle baudrate change
function handleBaudrateChange(rate: unknown) {
if (typeof rate !== 'string') return
baudrate.value = Number(rate)
@@ -219,13 +207,11 @@ async function applyHidConfig() {
// HID state will be updated via WebSocket device_info event
} catch (e) {
console.info('[HidConfig] Failed to apply config:', e)
// Error toast already shown by API layer
} finally {
applying.value = false
}
}
// Watch open state
watch(() => props.open, (isOpen) => {
if (!isOpen) return

View File

@@ -17,7 +17,6 @@ const props = defineProps<{
const { t } = useI18n()
// Key name mapping for friendly display
const keyNameMap: Record<string, string> = {
MetaLeft: 'Win', MetaRight: 'Win',
ControlLeft: 'Ctrl', ControlRight: 'Ctrl',

View File

@@ -60,29 +60,23 @@ const { t } = useI18n()
const systemStore = useSystemStore()
const { on, off } = useWebSocket()
// Tab state
const activeTab = ref('images')
// Image state
const images = ref<MsdImage[]>([])
const loadingImages = ref(false)
const uploadProgress = ref(0)
const uploading = ref(false)
// Mount options (using ToggleGroup)
const mountMode = ref<'cdrom' | 'flash'>('flash')
const accessMode = ref<'readonly' | 'readwrite'>('readonly')
// Computed properties for API compatibility
const cdromMode = computed(() => mountMode.value === 'cdrom')
const readOnly = computed(() => accessMode.value === 'readonly')
// Operation state flags
const connecting = ref(false)
const disconnecting = ref(false)
const deleting = ref(false)
// Drive state
const driveFiles = ref<DriveFile[]>([])
const currentPath = ref('/')
const loadingDrive = ref(false)
@@ -91,13 +85,11 @@ const driveInitialized = ref(false)
const uploadingFile = ref(false)
const fileUploadProgress = ref(0)
// Inner dialog state
const showDeleteDialog = ref(false)
const deleteTarget = ref<{ type: 'image' | 'file'; id: string; name: string } | null>(null)
const showNewFolderDialog = ref(false)
const newFolderName = ref('')
// Drive init dialog state
const showDriveInitDialog = ref(false)
const showDeleteDriveDialog = ref(false)
const selectedDriveSize = ref(256) // Default 256MB
@@ -105,7 +97,6 @@ const customDriveSize = ref<number | undefined>(undefined)
const initializingDrive = ref(false)
const deletingDrive = ref(false)
// URL download state
const showUrlDialog = ref(false)
const downloadUrl = ref('')
const downloadFilename = ref('')
@@ -119,14 +110,11 @@ const downloadProgress = ref<{
status: string
} | null>(null)
// Constants
const TWO_POINT_TWO_GB = 2.2 * 1024 * 1024 * 1024
// Computed
const msdConnected = computed(() => systemStore.msd?.connected ?? false)
const msdMode = computed(() => systemStore.msd?.mode ?? 'none')
// Get currently connected image name
const connectedImageName = computed(() => {
if (!msdConnected.value) return null
if (msdMode.value === 'drive') return t('msd.drive')
@@ -136,7 +124,6 @@ const connectedImageName = computed(() => {
return image?.name ?? null
})
// Check if any operation is in progress
const operationInProgress = computed(() => {
return connecting.value ||
disconnecting.value ||
@@ -147,7 +134,6 @@ const operationInProgress = computed(() => {
deletingDrive.value
})
// Check if image is large (>2.2GB)
function isLargeFile(image: MsdImage): boolean {
return image.size > TWO_POINT_TWO_GB
}
@@ -163,7 +149,6 @@ const breadcrumbs = computed(() => {
return crumbs
})
// Load data when dialog opens
watch(() => props.open, async (isOpen) => {
if (isOpen) {
await loadData()
@@ -179,7 +164,6 @@ async function loadData() {
}
}
// Image functions
async function loadImages() {
loadingImages.value = true
try {
@@ -294,7 +278,6 @@ async function executeDelete() {
}
}
// Drive functions
async function loadDriveInfo() {
try {
driveInfo.value = await msdApi.driveInfo()
@@ -304,7 +287,6 @@ async function loadDriveInfo() {
}
}
// Drive size options - computed for i18n support
const driveSizeOptions = computed(() => [
{ value: 64, label: '64 MB' },
{ value: 128, label: '128 MB' },
@@ -316,17 +298,14 @@ const driveSizeOptions = computed(() => [
{ value: 8192, label: '8 GB' },
])
// Computed final drive size
const finalDriveSize = computed(() => {
return customDriveSize.value || selectedDriveSize.value
})
// Open drive init dialog
function initializeDrive() {
showDriveInitDialog.value = true
}
// Create drive with selected size
async function createDrive() {
initializingDrive.value = true
try {
@@ -342,7 +321,6 @@ async function createDrive() {
}
}
// Delete virtual drive
async function deleteDrive() {
deletingDrive.value = true
try {
@@ -422,7 +400,6 @@ async function createFolder() {
}
}
// URL download functions
async function startUrlDownload() {
if (!downloadUrl.value.trim()) return

View File

@@ -53,10 +53,8 @@ const emit = defineEmits<{
const { t } = useI18n()
const systemStore = useSystemStore()
// Tab state
const activeTab = ref('images')
// Image state
const images = ref<MsdImage[]>([])
const loadingImages = ref(false)
const uploadProgress = ref(0)
@@ -64,7 +62,6 @@ const uploading = ref(false)
const cdromMode = ref(true)
const readOnly = ref(true)
// Drive state
const driveFiles = ref<DriveFile[]>([])
const currentPath = ref('/')
const loadingDrive = ref(false)
@@ -73,13 +70,11 @@ const driveInitialized = ref(false)
const uploadingFile = ref(false)
const fileUploadProgress = ref(0)
// Dialog state
const showDeleteDialog = ref(false)
const deleteTarget = ref<{ type: 'image' | 'file'; id: string; name: string } | null>(null)
const showNewFolderDialog = ref(false)
const newFolderName = ref('')
// Computed
const msdConnected = computed(() => systemStore.msd?.connected ?? false)
const msdMode = computed(() => systemStore.msd?.mode ?? 'none')
@@ -94,7 +89,6 @@ const breadcrumbs = computed(() => {
return crumbs
})
// Load data when sheet opens
watch(() => props.open, async (isOpen) => {
if (isOpen) {
await loadData()
@@ -110,7 +104,6 @@ async function loadData() {
}
}
// Image functions
async function loadImages() {
loadingImages.value = true
try {
@@ -186,7 +179,6 @@ async function executeDelete() {
}
}
// Drive functions
async function loadDriveInfo() {
try {
driveInfo.value = await msdApi.driveInfo()

View File

@@ -23,11 +23,8 @@ const currentChar = ref(0)
const totalChars = ref(0)
const abortController = ref<AbortController | null>(null)
// Typing speed in milliseconds between characters
// Configurable delay to prevent target system from missing keystrokes
const typingDelay = ref(10)
// Text analysis for warning display
const textAnalysis = computed(() => {
if (!text.value) return null
return analyzeText(text.value)
@@ -38,14 +35,12 @@ const hasUntypableChars = computed(() => {
})
onMounted(() => {
// Auto focus the textarea
setTimeout(() => {
textareaRef.value?.focus()
}, 100)
})
onUnmounted(() => {
// Cancel any ongoing paste operation when component is unmounted
cancelPaste()
})
@@ -65,7 +60,6 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
const mapping = charToKey(char)
if (!mapping) {
// Skip untypable characters
return true
}
@@ -73,10 +67,8 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
const modifier = shift ? 0x02 : 0
try {
// Send keydown
await hidApi.keyboard('down', key, modifier)
// Small delay between down and up to ensure key is registered
await sleep(5)
if (signal.aborted) {
@@ -85,20 +77,16 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
return false
}
// Send keyup
await hidApi.keyboard('up', key, modifier)
// Additional small delay after keyup to ensure it's processed
await sleep(2)
return true
} catch (error) {
console.error('[Paste] Failed to type character:', char, error)
// Try to release the key even on error
try {
await hidApi.keyboard('up', key, modifier)
} catch {
// Ignore cleanup errors
}
return false
}
@@ -133,20 +121,17 @@ async function handlePaste() {
currentChar.value = charIndex
progress.value = Math.round((charIndex / totalLength) * 100)
// Handle CRLF: skip \r if followed by \n
if (char === '\r' && charIndex < totalLength && chars[charIndex] === '\n') {
continue
}
await typeChar(char, signal)
// Delay between characters (configurable)
if (typingDelay.value > 0 && charIndex < totalLength) {
await sleep(typingDelay.value)
}
}
// Success - close the modal after a brief delay
if (!signal.aborted) {
await sleep(200)
text.value = ''
@@ -155,11 +140,9 @@ async function handlePaste() {
} catch (error) {
console.error('[Paste] Error during paste operation:', error)
} finally {
// Reset HID to ensure no keys are stuck
try {
await hidApi.reset()
} catch {
// Ignore reset errors
}
isPasting.value = false
progress.value = 0
@@ -180,14 +163,12 @@ function cancelPaste() {
}
function handleKeydown(e: KeyboardEvent) {
// Ctrl/Cmd + Enter to paste
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault()
if (!isPasting.value) {
handlePaste()
}
}
// Escape to cancel or close
if (e.key === 'Escape') {
e.preventDefault()
if (isPasting.value) {
@@ -196,7 +177,6 @@ function handleKeydown(e: KeyboardEvent) {
emit('close')
}
}
// Stop propagation to prevent HID interference
e.stopPropagation()
}
</script>

View File

@@ -29,19 +29,16 @@ const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()
// Chart containers
const stabilityChartRef = ref<HTMLDivElement | null>(null)
const delayChartRef = ref<HTMLDivElement | null>(null)
const packetLossChartRef = ref<HTMLDivElement | null>(null)
const fpsChartRef = ref<HTMLDivElement | null>(null)
// Chart instances
let stabilityChart: uPlot | null = null
let delayChart: uPlot | null = null
let packetLossChart: uPlot | null = null
let fpsChart: uPlot | null = null
// Data history (last 120 seconds)
const MAX_POINTS = 120
const timestamps = ref<number[]>([])
const jitterHistory = ref<number[]>([])
@@ -50,7 +47,6 @@ const packetLossHistory = ref<number[]>([])
const fpsHistory = ref<number[]>([])
const bitrateHistory = ref<number[]>([])
// For delta calculations
let lastBytesReceived = 0
let lastPacketsLost = 0
let lastTimestamp = 0
@@ -58,13 +54,11 @@ let lastTimestamp = 0
// Is WebRTC mode
const isWebRTC = computed(() => props.videoMode !== 'mjpeg')
// Format time for axis
function formatTime(ts: number): string {
const date = new Date(ts * 1000)
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// Chart theme colors
const chartColors = {
line: '#3b82f6',
fill: 'rgba(59, 130, 246, 0.1)',
@@ -73,7 +67,6 @@ const chartColors = {
text: '#94a3b8',
}
// Chart options factory
function createChartOptions(
container: HTMLElement,
_yLabel: string,
@@ -129,7 +122,6 @@ function createChartOptions(
}
}
// Tooltip state for each chart
const activeTooltip = ref<{
chartId: string
time: string
@@ -186,12 +178,10 @@ function createTooltipPlugin(chartId: string, unit: string): uPlot.Plugin {
}
}
// Initialize charts
function initCharts() {
if (!props.open) return
nextTick(() => {
// Initialize timestamps if empty
if (timestamps.value.length === 0) {
const now = Date.now() / 1000
for (let i = MAX_POINTS - 1; i >= 0; i--) {
@@ -204,7 +194,6 @@ function initCharts() {
bitrateHistory.value = new Array(MAX_POINTS).fill(0)
}
// Network Stability (Jitter) Chart
if (stabilityChartRef.value && !stabilityChart) {
const opts = createChartOptions(stabilityChartRef.value, 'ms', (v) => `${v.toFixed(0)} ms`)
opts.plugins = [createTooltipPlugin('stability', 'ms')]
@@ -215,7 +204,6 @@ function initCharts() {
)
}
// Playback Delay Chart
if (delayChartRef.value && !delayChart) {
const opts = createChartOptions(delayChartRef.value, 'ms', (v) => `${v.toFixed(0)} ms`)
opts.plugins = [createTooltipPlugin('delay', 'ms')]
@@ -237,7 +225,6 @@ function initCharts() {
)
}
// FPS Chart
if (fpsChartRef.value && !fpsChart) {
const opts = createChartOptions(fpsChartRef.value, 'fps', (v) => `${v.toFixed(0)} fps`)
opts.plugins = [createTooltipPlugin('fps', 'fps')]
@@ -250,7 +237,6 @@ function initCharts() {
})
}
// Destroy charts
function destroyCharts() {
stabilityChart?.destroy()
stabilityChart = null
@@ -262,22 +248,18 @@ function destroyCharts() {
fpsChart = null
}
// Add data point
function addDataPoint() {
const now = Date.now() / 1000
// Shift timestamps
timestamps.value.push(now)
if (timestamps.value.length > MAX_POINTS) {
timestamps.value.shift()
}
if (isWebRTC.value && props.webrtcStats) {
// Jitter in ms
const jitter = (props.webrtcStats.jitter || 0) * 1000
jitterHistory.value.push(jitter)
// RTT (round trip time) as delay in ms
const rtt = (props.webrtcStats.roundTripTime || 0) * 1000
delayHistory.value.push(rtt)
@@ -287,10 +269,8 @@ function addDataPoint() {
lastPacketsLost = currentLost
packetLossHistory.value.push(lostDelta)
// FPS
fpsHistory.value.push(props.webrtcStats.framesPerSecond || 0)
// Calculate bitrate
const currentBytes = props.webrtcStats.bytesReceived || 0
const currentTime = Date.now()
if (lastTimestamp > 0 && currentBytes > lastBytesReceived) {
@@ -312,18 +292,15 @@ function addDataPoint() {
bitrateHistory.value.push(0)
}
// Trim arrays
if (jitterHistory.value.length > MAX_POINTS) jitterHistory.value.shift()
if (delayHistory.value.length > MAX_POINTS) delayHistory.value.shift()
if (packetLossHistory.value.length > MAX_POINTS) packetLossHistory.value.shift()
if (fpsHistory.value.length > MAX_POINTS) fpsHistory.value.shift()
if (bitrateHistory.value.length > MAX_POINTS) bitrateHistory.value.shift()
// Update charts
updateCharts()
}
// Update charts with new data
function updateCharts() {
stabilityChart?.setData([timestamps.value, jitterHistory.value])
delayChart?.setData([timestamps.value, delayHistory.value])
@@ -331,7 +308,6 @@ function updateCharts() {
fpsChart?.setData([timestamps.value, fpsHistory.value])
}
// Data collection interval
let dataInterval: number | null = null
function startDataCollection() {
@@ -346,7 +322,6 @@ function stopDataCollection() {
}
}
// Format candidate type for display
function formatCandidateType(type: string): string {
const typeMap: Record<string, string> = {
host: 'Host (Local)',
@@ -393,10 +368,8 @@ const currentStats = computed(() => {
}
})
// Watch open state
watch(() => props.open, (isOpen) => {
if (isOpen) {
// Reset data
timestamps.value = []
jitterHistory.value = []
delayHistory.value = []
@@ -417,7 +390,6 @@ watch(() => props.open, (isOpen) => {
}
})
// Resize handler
function handleResize() {
if (!props.open) return
destroyCharts()

View File

@@ -92,7 +92,6 @@ const statusIcon = computed(() => {
}
})
// Localized status text
const statusText = computed(() => {
switch (props.status) {
case 'connected':
@@ -108,7 +107,6 @@ const statusText = computed(() => {
}
})
// Localized status badge text (for hover card)
const statusBadgeText = computed(() => {
switch (props.status) {
case 'connected':

View File

@@ -68,7 +68,6 @@ const router = useRouter()
const devices = ref<VideoDevice[]>([])
const loadingDevices = ref(false)
// Codec list
const codecs = ref<VideoCodecInfo[]>([])
const loadingCodecs = ref(false)
@@ -135,7 +134,6 @@ function detectBrowserCodecSupport() {
if (capabilities?.codecs) {
for (const codec of capabilities.codecs) {
const mimeType = codec.mimeType.toLowerCase()
// Map MIME types to our codec IDs
if (mimeType.includes('h264') || mimeType.includes('avc')) {
supported.add('h264')
}
@@ -154,7 +152,6 @@ function detectBrowserCodecSupport() {
}
}
} else {
// Fallback: assume basic codecs are supported
supported.add('h264')
supported.add('vp8')
supported.add('vp9')
@@ -191,12 +188,10 @@ const translateBackendName = (backend: string | undefined): string => {
return backend
}
// Check if a format has fps >= 30 in any resolution
const hasHighFps = (format: { resolutions: { fps: number[] }[] }): boolean => {
return format.resolutions.some(res => res.fps.some(fps => fps >= 30))
}
// Check if a format is recommended based on video mode
const isFormatRecommended = (formatName: string): boolean => {
if (!isVideoFormatSelectable(formatName, props.videoMode, currentEncoderBackend.value)) {
return false
@@ -214,20 +209,16 @@ const isFormatRecommended = (formatName: string): boolean => {
const currentFormat = formats.find(f => f.format.toUpperCase() === upperFormat)
if (!currentFormat) return false
// Check if NV12 exists with fps >= 30
const nv12Format = formats.find(f => f.format.toUpperCase() === 'NV12')
const nv12HasHighFps = nv12Format && hasHighFps(nv12Format)
// Check if YUYV exists with fps >= 30
const yuyvFormat = formats.find(f => f.format.toUpperCase() === 'YUYV')
const yuyvHasHighFps = yuyvFormat && hasHighFps(yuyvFormat)
// Priority 1: NV12 with high fps
if (nv12HasHighFps) {
return upperFormat === 'NV12'
}
// Priority 2: YUYV with high fps (only if NV12 doesn't qualify)
if (yuyvHasHighFps) {
return upperFormat === 'YUYV'
}
@@ -235,13 +226,11 @@ const isFormatRecommended = (formatName: string): boolean => {
return false
}
// Check if a format is not recommended for current video mode
// In WebRTC mode, compressed formats (MJPEG/JPEG) are not recommended
const isFormatNotRecommended = (formatName: string): boolean => {
return getFormatState(formatName) === 'not_recommended'
}
// Selected values (mode comes from props)
const selectedDevice = ref<string>('')
const selectedFormat = ref<string>('')
const selectedResolution = ref<string>('')
@@ -249,11 +238,9 @@ const selectedFps = ref<number>(30)
const selectedBitratePreset = ref<'Speed' | 'Balanced' | 'Quality'>('Balanced')
const isDirty = ref(false)
// UI state
const applying = ref(false)
const applyingBitrate = ref(false)
// Current config from store
const currentConfig = computed(() => ({
device: configStore.video?.device || '',
format: configStore.video?.format || '',
@@ -262,7 +249,6 @@ const currentConfig = computed(() => ({
fps: configStore.video?.fps || 30,
}))
// Button display text - simplified to just show label
const buttonText = computed(() => t('actionbar.videoConfig'))
// Available codecs for selection (filtered by backend support and enriched with backend info)
@@ -305,7 +291,6 @@ const availableCodecs = computed(() => {
return backendFiltered.filter(codec => allowed.includes(codec.id))
})
// Cascading filters
const availableFormats = computed(() => {
const device = devices.value.find(d => d.path === selectedDevice.value)
return device?.formats || []
@@ -331,13 +316,11 @@ const availableFps = computed(() => {
return resolution?.fps || []
})
// Get selected format description for display in trigger
const selectedFormatInfo = computed(() => {
const format = availableFormatOptions.value.find(f => f.format === selectedFormat.value)
return format
})
// Get selected codec info for display in trigger
const selectedCodecInfo = computed(() => {
const codec = availableCodecs.value.find(c => c.id === props.videoMode)
return codec || null
@@ -366,7 +349,6 @@ async function loadCodecs() {
backends.value = result.backends || []
} catch (e) {
console.info('[VideoConfig] Failed to load codecs')
// Fallback to default codecs
codecs.value = [
{ id: 'mjpeg', name: 'MJPEG / HTTP', protocol: 'http', hardware: false, backend: 'software', available: true },
{ id: 'h264', name: 'H.264 / WebRTC', protocol: 'webrtc', hardware: false, backend: 'software', available: true },
@@ -384,12 +366,10 @@ async function loadConstraints() {
}
}
// Navigate to settings page (video tab)
function goToSettings() {
router.push('/settings?tab=video')
}
// Initialize selected values from current config
function initializeFromCurrent() {
const config = currentConfig.value
selectedDevice.value = config.device
@@ -417,7 +397,6 @@ function syncFromCurrentIfChanged() {
isDirty.value = false
}
// Handle video mode change
function handleVideoModeChange(mode: unknown) {
if (typeof mode !== 'string') return
@@ -476,7 +455,6 @@ function handleDeviceChange(devicePath: unknown) {
selectedDevice.value = devicePath
isDirty.value = true
// Auto-select first format
const device = devices.value.find(d => d.path === devicePath)
const format = device ? findFirstSelectableFormat(device.formats) : undefined
if (!format) {
@@ -487,7 +465,6 @@ function handleDeviceChange(devicePath: unknown) {
selectFormatWithDefaults(format.format)
}
// Handle format change
function handleFormatChange(format: unknown) {
if (typeof format !== 'string') return
if (isFormatUnsupported(format)) return
@@ -496,13 +473,11 @@ function handleFormatChange(format: unknown) {
isDirty.value = true
}
// Handle resolution change
function handleResolutionChange(resolution: unknown) {
if (typeof resolution !== 'string') return
selectedResolution.value = resolution
isDirty.value = true
// Auto-select first FPS for this resolution
const resolutionData = availableResolutions.value.find(
r => `${r.width}x${r.height}` === resolution
)
@@ -511,14 +486,12 @@ function handleResolutionChange(resolution: unknown) {
}
}
// Handle FPS change
function handleFpsChange(fps: unknown) {
if (typeof fps !== 'string' && typeof fps !== 'number') return
selectedFps.value = typeof fps === 'string' ? Number(fps) : fps
isDirty.value = true
}
// Apply bitrate preset change
async function applyBitratePreset(preset: 'Speed' | 'Balanced' | 'Quality') {
if (applyingBitrate.value) return
applyingBitrate.value = true
@@ -532,7 +505,6 @@ async function applyBitratePreset(preset: 'Speed' | 'Balanced' | 'Quality') {
}
}
// Handle bitrate preset selection
function handleBitratePresetChange(preset: 'Speed' | 'Balanced' | 'Quality') {
selectedBitratePreset.value = preset
if (props.videoMode !== 'mjpeg') {
@@ -540,7 +512,6 @@ function handleBitratePresetChange(preset: 'Speed' | 'Balanced' | 'Quality') {
}
}
// Apply video configuration
async function applyVideoConfig() {
const [width, height] = selectedResolution.value.split('x').map(Number)
@@ -559,13 +530,11 @@ async function applyVideoConfig() {
// Stream state will be updated via WebSocket system.device_info event
} catch (e) {
console.info('[VideoConfig] Failed to apply config:', e)
// Error toast already shown by API layer
} finally {
applying.value = false
}
}
// Watch open state
watch(() => props.open, (isOpen) => {
if (!isOpen) {
isDirty.value = false

View File

@@ -38,20 +38,16 @@ const emit = defineEmits<{
const { t } = useI18n()
// State
const isAttached = ref(props.attached ?? true)
const selectedOs = ref<KeyboardOsType>('windows')
// Keyboard instances
const mainKeyboard = ref<Keyboard | null>(null)
const controlKeyboard = ref<Keyboard | null>(null)
const arrowsKeyboard = ref<Keyboard | null>(null)
// Pressed keys tracking
const pressedModifiers = ref<number>(0)
const keysDown = ref<CanonicalKey[]>([])
// Shift state for display
const isShiftActive = computed(() => {
return (pressedModifiers.value & 0x22) !== 0
})
@@ -64,7 +60,6 @@ const layoutName = computed(() => {
return isShiftActive.value ? 'shift' : 'default'
})
// Keys currently pressed (for highlighting)
const keyNamesForDownKeys = computed(() => {
const activeModifierMask = pressedModifiers.value || 0
const modifierNames = Object.entries(modifiers)
@@ -79,19 +74,15 @@ const keyNamesForDownKeys = computed(() => {
]))
})
// Dragging state (for floating mode)
const keyboardRef = ref<HTMLDivElement | null>(null)
const isDragging = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
const position = ref({ x: 100, y: 100 })
// Unique ID for this keyboard instance
const keyboardId = ref(`kb-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`)
// Get bottom row based on selected OS
const getBottomRow = () => osBottomRows[selectedOs.value].join(' ')
// Keyboard layouts - matching JetKVM style
const keyboardLayout = {
main: {
default: [
@@ -143,19 +134,15 @@ function setCompactLayout(active: boolean) {
updateKeyboardLayout()
}
// Key display mapping with Unicode symbols (JetKVM style)
const keyDisplayMap = computed<Record<string, string>>(() => {
// OS-specific Meta key labels
const metaLabel = selectedOs.value === 'windows' ? '⊞ Win'
: selectedOs.value === 'mac' ? '⌘ Cmd' : 'Meta'
return {
// Macros - compact format
CtrlAltDelete: 'Ctrl+Alt+Del',
AltMetaEscape: 'Alt+Meta+Esc',
CtrlAltBackspace: 'Ctrl+Alt+Bksp',
// Modifiers - simplified
ControlLeft: 'Ctrl',
ControlRight: 'Ctrl',
ShiftLeft: 'Shift',
@@ -166,7 +153,6 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
MetaRight: metaLabel,
ContextMenu: 'Menu',
// Special keys
Escape: 'Esc',
Backspace: '⌫',
Tab: 'Tab',
@@ -174,7 +160,6 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
Enter: 'Enter',
Space: ' ',
// Navigation
Insert: 'Ins',
Delete: 'Del',
Home: 'Home',
@@ -182,23 +167,19 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
PageUp: 'PgUp',
PageDown: 'PgDn',
// Arrows
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
// Control cluster
PrintScreen: 'PrtSc',
ScrollLock: 'ScrLk',
Pause: 'Pause',
// Function keys
F1: 'F1', F2: 'F2', F3: 'F3', F4: 'F4',
F5: 'F5', F6: 'F6', F7: 'F7', F8: 'F8',
F9: 'F9', F10: 'F10', F11: 'F11', F12: 'F12',
// Letters
KeyA: areLettersUppercase.value ? 'A' : 'a',
KeyB: areLettersUppercase.value ? 'B' : 'b',
KeyC: areLettersUppercase.value ? 'C' : 'c',
@@ -226,7 +207,6 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
KeyY: areLettersUppercase.value ? 'Y' : 'y',
KeyZ: areLettersUppercase.value ? 'Z' : 'z',
// Letter labels in the shifted layout follow CapsLock xor Shift too
'(KeyA)': areLettersUppercase.value ? 'A' : 'a',
'(KeyB)': areLettersUppercase.value ? 'B' : 'b',
'(KeyC)': areLettersUppercase.value ? 'C' : 'c',
@@ -254,15 +234,12 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
'(KeyY)': areLettersUppercase.value ? 'Y' : 'y',
'(KeyZ)': areLettersUppercase.value ? 'Z' : 'z',
// Numbers
Digit1: '1', Digit2: '2', Digit3: '3', Digit4: '4', Digit5: '5',
Digit6: '6', Digit7: '7', Digit8: '8', Digit9: '9', Digit0: '0',
// Shifted Numbers
'(Digit1)': '!', '(Digit2)': '@', '(Digit3)': '#', '(Digit4)': '$', '(Digit5)': '%',
'(Digit6)': '^', '(Digit7)': '&', '(Digit8)': '*', '(Digit9)': '(', '(Digit0)': ')',
// Symbols
Minus: '-', '(Minus)': '_',
Equal: '=', '(Equal)': '+',
BracketLeft: '[', '(BracketLeft)': '{',
@@ -277,7 +254,6 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
}
})
// Handle media key press (Consumer Control)
async function onMediaKeyPress(key: string) {
if (key in consumerKeys) {
const usage = consumerKeys[key as ConsumerKeyName]
@@ -289,16 +265,12 @@ async function onMediaKeyPress(key: string) {
}
}
// Switch OS layout
function switchOsLayout(os: KeyboardOsType) {
selectedOs.value = os
// Save preference to localStorage
localStorage.setItem('vkb-os-layout', os)
// Update keyboard layout
updateKeyboardLayout()
}
// Update keyboard layout based on selected OS
function updateKeyboardLayout() {
const bottomRow = getBottomRow()
const baseLayout = isCompactLayout.value ? compactMainLayout : keyboardLayout.main
@@ -319,9 +291,7 @@ function updateKeyboardLayout() {
updateKeyboardButtonTheme()
}
// Key press handler
async function onKeyDown(key: string) {
// Handle macro keys
if (key === 'CtrlAltDelete') {
await executeMacro([
{ keys: ['Delete'], modifiers: ['ControlLeft', 'AltLeft'] },
@@ -343,10 +313,8 @@ async function onKeyDown(key: string) {
return
}
// Clean key name (remove parentheses for shifted keys)
const cleanKey = key.replace(/[()]/g, '')
// Check if key exists
if (!(cleanKey in keys)) {
console.warn(`[VirtualKeyboard] Unknown key: ${cleanKey}`)
return
@@ -354,7 +322,6 @@ async function onKeyDown(key: string) {
const keyCode = keys[cleanKey as KeyName]
// Handle latching keys (Caps Lock, etc.)
if (latchingKeys.some(latchingKey => latchingKey === keyCode)) {
emit('keyDown', keyCode)
const currentMask = pressedModifiers.value & 0xff
@@ -366,7 +333,6 @@ async function onKeyDown(key: string) {
return
}
// Handle modifier keys (toggle)
const mask = modifiers[keyCode] ?? 0
if (mask !== 0) {
const isCurrentlyDown = (pressedModifiers.value & mask) !== 0
@@ -386,7 +352,6 @@ async function onKeyDown(key: string) {
return
}
// Regular key: press and release
keysDown.value.push(keyCode)
emit('keyDown', keyCode)
const currentMask = pressedModifiers.value & 0xff
@@ -401,7 +366,6 @@ async function onKeyDown(key: string) {
}
async function onKeyUp() {
// Not used for now - we handle up in onKeyDown with setTimeout
}
async function sendKeyPress(keyCode: CanonicalKey, press: boolean, modifierMask: number) {
@@ -453,7 +417,6 @@ async function executeMacro(steps: MacroStep[]) {
}
}
// Update keyboard button theme for pressed keys
function updateKeyboardButtonTheme() {
const downKeys = keyNamesForDownKeys.value.join(' ')
const buttonTheme = [
@@ -472,7 +435,6 @@ function updateKeyboardButtonTheme() {
arrowsKeyboard.value?.setOptions({ buttonTheme })
}
// Update layout when shift state changes
watch([layoutName, () => props.capsLock], ([name]) => {
mainKeyboard.value?.setOptions({
layoutName: name,
@@ -481,11 +443,9 @@ watch([layoutName, () => props.capsLock], ([name]) => {
updateKeyboardButtonTheme()
})
// Initialize keyboards with unique selectors
function initKeyboards() {
const id = keyboardId.value
// Check if elements exist - use full selector with #
const mainEl = document.querySelector(`#${id}-main`)
const controlEl = document.querySelector(`#${id}-control`)
const arrowsEl = document.querySelector(`#${id}-arrows`)
@@ -496,7 +456,6 @@ function initKeyboards() {
return
}
// Main keyboard - pass element directly instead of selector string
mainKeyboard.value = new Keyboard(mainEl, {
layout: isCompactLayout.value ? compactMainLayout : keyboardLayout.main,
layoutName: layoutName.value,
@@ -517,7 +476,6 @@ function initKeyboards() {
stopMouseUpPropagation: true,
})
// Control keyboard
controlKeyboard.value = new Keyboard(controlEl, {
layout: keyboardLayout.control,
layoutName: 'default',
@@ -532,7 +490,6 @@ function initKeyboards() {
stopMouseUpPropagation: true,
})
// Arrows keyboard
arrowsKeyboard.value = new Keyboard(arrowsEl, {
layout: keyboardLayout.arrows,
layoutName: 'default',
@@ -551,7 +508,6 @@ function initKeyboards() {
console.log('[VirtualKeyboard] Keyboards initialized:', id)
}
// Destroy keyboards
function destroyKeyboards() {
mainKeyboard.value?.destroy()
controlKeyboard.value?.destroy()
@@ -561,7 +517,6 @@ function destroyKeyboards() {
arrowsKeyboard.value = null
}
// Dragging handlers
function getClientCoords(e: MouseEvent | TouchEvent): { x: number; y: number } | null {
if ('touches' in e) {
const touch = e.touches[0]
@@ -609,11 +564,9 @@ async function toggleAttached() {
isAttached.value = !isAttached.value
emit('update:attached', isAttached.value)
// Wait for Teleport to move the component
await nextTick()
await nextTick() // Extra tick for Teleport
// Reinitialize keyboards in new location
setTimeout(() => {
initKeyboards()
}, 100)
@@ -623,7 +576,6 @@ function close() {
emit('update:visible', false)
}
// Watch visibility to init/destroy keyboards
watch(() => props.visible, async (visible) => {
console.log('[VirtualKeyboard] Visibility changed:', visible, 'attached:', isAttached.value, 'id:', keyboardId.value)
if (visible) {
@@ -641,7 +593,6 @@ watch(() => props.attached, (value) => {
})
onMounted(() => {
// Load saved OS layout preference
const savedOs = localStorage.getItem('vkb-os-layout') as KeyboardOsType | null
if (savedOs && ['windows', 'mac', 'android'].includes(savedOs)) {
selectedOs.value = savedOs