mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 19:51:58 +08:00
refactor: 删除部分多余的代码和注释
This commit is contained in:
@@ -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'>()
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user