From cb3fd3e64660b08b83d2d982f3925ecaa5a3e611 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Wed, 29 Jul 2026 19:32:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=AD=89=E6=AF=94=E4=BE=8B=E7=BC=A9=E6=94=BE?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/ActionBar.vue | 129 ++++++++-------- web/src/components/VideoDisplayControls.vue | 104 +++++++++++++ web/src/composables/useVideoScaling.ts | 95 ++++++++++++ web/src/i18n/en-US.ts | 4 + web/src/i18n/zh-CN.ts | 4 + web/src/views/ConsoleView.vue | 155 ++++++++++++-------- 6 files changed, 362 insertions(+), 129 deletions(-) create mode 100644 web/src/components/VideoDisplayControls.vue create mode 100644 web/src/composables/useVideoScaling.ts diff --git a/web/src/components/ActionBar.vue b/web/src/components/ActionBar.vue index dc28a2f0..ae82bebf 100644 --- a/web/src/components/ActionBar.vue +++ b/web/src/components/ActionBar.vue @@ -3,6 +3,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import { useSystemStore } from '@/stores/system' +import type { VideoScaleMode } from '@/composables/useVideoScaling' import { Button } from '@/components/ui/button' import { ButtonGroup } from '@/components/ui/button-group' import { @@ -32,9 +33,7 @@ import { import { ClipboardPaste, HardDrive, - Keyboard, Settings, - Maximize, Power, BarChart3, Terminal, @@ -47,6 +46,7 @@ import VideoConfigPopover, { type VideoMode } from '@/components/VideoConfigPopo import HidConfigPopover from '@/components/HidConfigPopover.vue' import AudioConfigPopover from '@/components/AudioConfigPopover.vue' import MsdDialog from '@/components/MsdDialog.vue' +import VideoDisplayControls from '@/components/VideoDisplayControls.vue' const { t, locale } = useI18n() const router = useRouter() @@ -69,6 +69,8 @@ const props = defineProps<{ showComputerUse?: boolean showPasteText?: boolean showMic?: boolean + scaleMode?: VideoScaleMode + sourceSizeAvailable?: boolean }>() const showStats = computed(() => (props.videoMode ?? 'mjpeg') !== 'mjpeg') const showPasteText = computed(() => props.showPasteText !== false) @@ -77,6 +79,7 @@ const showMic = computed(() => props.showMic === true) const emit = defineEmits<{ (e: 'toggleFullscreen'): void + (e: 'update:scaleMode', mode: VideoScaleMode): void (e: 'toggleStats'): void (e: 'toggleVirtualKeyboard'): void (e: 'toggleMouseMode'): void @@ -130,7 +133,8 @@ const openMobilePaste = () => openFromOverflow(() => { const barRef = ref(null) const measureRef = ref(null) const barWidth = ref(0) -let resizeObserver: ResizeObserver | null = null +const alwaysRightWidth = ref(152) +let layoutResizeObserver: ResizeObserver | null = null type CollapsibleItem = | 'video' | 'audio' | 'hid' @@ -157,16 +161,18 @@ const ITEM_SPECS: ItemSpec[] = [ const measuredWidths = ref>(new Map()) const measurementReady = ref(false) -const measureButtonWidths = async () => { +const measureLayout = async () => { await nextTick() - if (!measureRef.value) return + const bar = barRef.value + const measureContainer = measureRef.value + if (!bar || !measureContainer) return + + barWidth.value = bar.clientWidth const newWidths = new Map() - for (const spec of ITEM_SPECS) { - const iconEl = measureRef.value.querySelector(`[data-measure="${spec.id}-icon"]`) as HTMLElement - const labelEl = measureRef.value.querySelector(`[data-measure="${spec.id}-label"]`) as HTMLElement - + const iconEl = measureContainer.querySelector(`[data-measure="${spec.id}-icon"]`) as HTMLElement + const labelEl = measureContainer.querySelector(`[data-measure="${spec.id}-label"]`) as HTMLElement if (iconEl && labelEl) { newWidths.set(spec.id, { icon: Math.ceil(iconEl.offsetWidth) + 8, @@ -174,31 +180,48 @@ const measureButtonWidths = async () => { }) } } - measuredWidths.value = newWidths + + const elements = Array.from(bar.querySelectorAll('[data-fixed-action]')) as HTMLElement[] + const width = elements.reduce((sum, element) => { + const style = window.getComputedStyle(element) + return sum + + element.getBoundingClientRect().width + + Number.parseFloat(style.marginLeft || '0') + + Number.parseFloat(style.marginRight || '0') + }, 0) + if (width > 0) alwaysRightWidth.value = Math.ceil(width) + measurementReady.value = true } +const observeLayout = async () => { + await measureLayout() + layoutResizeObserver?.disconnect() + layoutResizeObserver = new ResizeObserver(() => { + void measureLayout() + }) + if (barRef.value) layoutResizeObserver.observe(barRef.value) + barRef.value?.querySelectorAll('[data-fixed-action]').forEach((element) => { + layoutResizeObserver?.observe(element) + }) +} + onMounted(() => { - if (barRef.value) { - resizeObserver = new ResizeObserver((entries) => { - const entry = entries[0] - if (entry) barWidth.value = entry.contentRect.width - }) - resizeObserver.observe(barRef.value) - barWidth.value = barRef.value.clientWidth - } - - measureButtonWidths() + void observeLayout() }) -onUnmounted(() => { - resizeObserver?.disconnect() +onUnmounted(() => { + layoutResizeObserver?.disconnect() }) watch(locale, () => { measurementReady.value = false - measureButtonWidths() + void measureLayout() +}) + +watch(() => props.showComputerUse, () => { + void observeLayout() }) watch(showAtx, (visible) => { @@ -215,7 +238,7 @@ watch(showPasteText, (visible) => { } }) -const RIGHT_FIXED_PX = 120 +const OVERFLOW_BUTTON_BUDGET_PX = 36 const collapsibleItems = computed(() => { const items = ITEM_SPECS.slice(3).filter(item => { @@ -234,7 +257,7 @@ const visibleSet = computed(() => { return new Map() } - const available = barWidth.value - RIGHT_FIXED_PX + const available = barWidth.value - alwaysRightWidth.value - OVERFLOW_BUTTON_BUDGET_PX let used = 0 if (barRef.value) { @@ -277,6 +300,7 @@ const hasLeftOverflow = computed(() => { const hasRightOverflow = computed(() => { return collapsibleItems.value.some(i => i.side === 'right' && !visibleSet.value.has(i.id)) }) +