From e23fa413beceb61ee7840691a95e0ae50ff72713 Mon Sep 17 00:00:00 2001 From: yulonger's MacBook Pro 14 Date: Sun, 30 Aug 2026 22:20:30 +0800 Subject: [PATCH] feat: sync interactive image previews --- README.md | 6 +- .../theme/components/ContentScaleControls.vue | 467 +++++++++++++++++- dist-cli/theme/components/PresenterAccess.vue | 14 - dist-cli/theme/styles/layouts.css | 128 ++++- dist-cli/theme/styles/presenter.css | 26 +- .../components/ContentScaleControls.vue | 467 +++++++++++++++++- .../components/PresenterAccess.vue | 14 - .../slidev-theme-easy-jyy/styles/layouts.css | 128 ++++- .../styles/presenter.css | 26 +- tests/core.test.ts | 6 + tests/e2e/presenter.spec.ts | 131 ++++- 11 files changed, 1296 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 8b4f4e4..4d8977e 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,9 @@ const slides = await discover('slides') 同样的包裹方式也适用于表格,例如 `
`。不添加固定高度类时,单个代码框会占满标题或前置正文之后的剩余区域;同一区域有多个代码框时保持内容高度并限制在内容区内。 -公开放映页和演示者预览中的图片可以点击进入全屏预览,也可以聚焦图片后按 `Enter` 或空格打开;通过关闭按钮、点击背景或按 `Esc` 返回幻灯片。演示者点击图片时会优先在已连接的放映页打开;没有放映页确认时,图片在约半秒后回退到演示者页打开。`present` 模式支持局域网跨设备联动,静态站点支持同一浏览器标签页联动。该交互不改变图片原始布局,也不会进入导出文件。 +公开放映页和演示者预览中的图片可以点击进入全屏预览,也可以聚焦图片后按 `Enter` 或空格打开;通过关闭按钮、点击背景或按 `Esc` 返回幻灯片。预览底部提供 `− / 百分比 / +` 工具栏,100% 对应图片原始像素尺寸;还可以使用鼠标滚轮、触控板或触屏双指缩放,并在放大后拖动查看细节,缩放范围为 10%–400%。 + +演示者点击图片时会优先在已连接的放映页打开;没有放映页确认时,图片在约半秒后回退到演示者页打开。演示者发起的同一轮预览会在所有放映页之间同步缩放、拖动和关闭;放映页自行点击打开的图片仍只影响本机。`present` 模式支持局域网跨设备联动,静态站点支持同一浏览器标签页联动。该交互不改变图片原始布局,也不会进入导出文件。 表格和代码块会在悬停或键盘聚焦时显示 `− / 百分比 / +` 缩放工具栏,可在 `60%–160%` 之间按 `10%` 调整;同一课件的放映页与演示者标签页会实时同步。代码缩放只改变字号,不改变高度或滚动位置。缩放只保存在当前页面内存中,刷新即恢复默认,也不会进入导出文件。 @@ -198,7 +200,7 @@ Token 在内容仓库或部署平台中配置: PRESENTER_TOKEN=a-long-non-obvious-token pnpm build ``` -构建只写入 SHA-256 哈希。演示者页面未解锁时使用全局模态锁屏,背景控件、翻页快捷键和焦点均不可操作;正确 token 解锁后,状态保存在当前标签页的 `sessionStorage`,刷新保持,锁定或关闭标签页后失效。未设置 token 时,公开页面隐藏演示者入口,`check` 会提示警告。 +构建只写入 SHA-256 哈希。普通放映页不显示演示者入口;演示者通过 `/presenter/` 或 `easy-slides present` 输出的专用地址进入。演示者页面未解锁时使用全局模态锁屏,背景控件、翻页快捷键和焦点均不可操作;正确 token 解锁后,状态保存在当前标签页的 `sessionStorage`,刷新保持,锁定或关闭标签页后失效。未设置 token 时,`check` 会提示警告。 这只是静态站点的便利门槛,不是服务端安全边界。备注和演示者代码仍可被技术用户分析。 diff --git a/dist-cli/theme/components/ContentScaleControls.vue b/dist-cli/theme/components/ContentScaleControls.vue index f2074ce..6dc68d2 100644 --- a/dist-cli/theme/components/ContentScaleControls.vue +++ b/dist-cli/theme/components/ContentScaleControls.vue @@ -52,9 +52,34 @@ interface ImagePreviewAcknowledgement { viewerId: string } +interface ImagePreviewView { + centerX: number + centerY: number + scale: number +} + +interface ImagePreviewCommandBase { + id: string + requestId: string + sentAt: number + viewerId: string +} + +interface ImagePreviewCloseCommand extends ImagePreviewCommandBase { + type: 'close' +} + +interface ImagePreviewViewCommand extends ImagePreviewCommandBase { + type: 'view' + view: ImagePreviewView +} + +type ImagePreviewCommand = ImagePreviewCloseCommand | ImagePreviewViewCommand + interface EasySlidesSharedState { easySlidesImagePreview?: { acknowledgement?: ImagePreviewAcknowledgement + command?: ImagePreviewCommand request?: ImagePreviewRequest } } @@ -67,6 +92,15 @@ interface PendingImagePreview { requestId: string } +interface ImagePointer { + x: number + y: number +} + +const IMAGE_MIN_SCALE = 0.1 +const IMAGE_MAX_SCALE = 4 +const IMAGE_SCALE_STEP = 0.1 + const nav = useNav() const easySharedState = sharedState as typeof sharedState & EasySlidesSharedState const clientId = createClientId() @@ -78,16 +112,35 @@ const imageInteractions = new Map() let observer: MutationObserver | undefined let resizeObserver: ResizeObserver | undefined let channel: BroadcastChannel | undefined +let imageChannel: BroadcastChannel | undefined let lightbox: HTMLElement | undefined +let lightboxViewport: HTMLElement | undefined let lightboxImage: HTMLImageElement | undefined let lightboxClose: HTMLButtonElement | undefined +let lightboxDecrease: HTMLButtonElement | undefined +let lightboxIncrease: HTMLButtonElement | undefined +let lightboxScaleOutput: HTMLOutputElement | undefined let activeImage: HTMLImageElement | undefined +let activePreviewRequestId: string | undefined let pendingImagePreview: PendingImagePreview | undefined let stopAcknowledgementWatch: (() => void) | undefined +let stopCommandWatch: (() => void) | undefined let stopRequestWatch: (() => void) | undefined let fitFrame: number | undefined +let imageViewFrame: number | undefined +let pendingSharedView: ImagePreviewView | undefined +let pendingRemoteView: ImagePreviewView | undefined let lastHandledRequestId = easySharedState.easySlidesImagePreview?.request?.id ?? '' +let lastHandledCommandId = easySharedState.easySlidesImagePreview?.command?.id ?? '' let bodyOverflow = '' +let imageScale = 1 +let imageCenterX = 0.5 +let imageCenterY = 0.5 +let imageViewTouched = false +let lightboxPointerMoved = false +let pinchDistance = 0 +let pinchMidpoint: ImagePointer | undefined +const imagePointers = new Map() function createClientId() { if (typeof crypto.randomUUID === 'function') @@ -227,6 +280,260 @@ function applyScale(key: string, kind: ScalableKind, scale: number) { }) } +function clampImageScale(scale: number) { + return Math.min(IMAGE_MAX_SCALE, Math.max(IMAGE_MIN_SCALE, Math.round(scale * 10_000) / 10_000)) +} + +function imageGeometry(scale = imageScale) { + if (!lightboxViewport || !lightboxImage || !lightboxImage.naturalWidth || !lightboxImage.naturalHeight) + return undefined + return { + imageHeight: lightboxImage.naturalHeight * scale, + imageWidth: lightboxImage.naturalWidth * scale, + viewportHeight: lightboxViewport.clientHeight, + viewportWidth: lightboxViewport.clientWidth, + } +} + +function clampImageCenter(center: number, imageSize: number, viewportSize: number) { + if (imageSize <= viewportSize || imageSize <= 0) + return 0.5 + const edge = viewportSize / (2 * imageSize) + return Math.min(1 - edge, Math.max(edge, center)) +} + +function currentImageView(): ImagePreviewView { + return { + centerX: imageCenterX, + centerY: imageCenterY, + scale: imageScale, + } +} + +function renderImageView() { + if (!lightboxImage || !lightboxViewport) + return + const geometry = imageGeometry() + if (!geometry) + return + imageCenterX = clampImageCenter(imageCenterX, geometry.imageWidth, geometry.viewportWidth) + imageCenterY = clampImageCenter(imageCenterY, geometry.imageHeight, geometry.viewportHeight) + lightboxImage.style.width = `${geometry.imageWidth}px` + lightboxImage.style.height = `${geometry.imageHeight}px` + lightboxImage.style.transform = `translate(${-imageCenterX * 100}%, ${-imageCenterY * 100}%)` + lightboxImage.dataset.easyImageScale = String(imageScale) + lightboxImage.dataset.easyImageCenterX = String(imageCenterX) + lightboxImage.dataset.easyImageCenterY = String(imageCenterY) + lightboxViewport.classList.toggle( + 'easy-image-lightbox-pannable', + geometry.imageWidth > geometry.viewportWidth + 1 || geometry.imageHeight > geometry.viewportHeight + 1, + ) + const percentage = `${Math.round(imageScale * 100)}%` + if (lightboxScaleOutput) { + lightboxScaleOutput.value = percentage + lightboxScaleOutput.textContent = percentage + lightboxScaleOutput.title = `当前图片缩放 ${percentage}` + } + if (lightboxDecrease) + lightboxDecrease.disabled = imageScale <= IMAGE_MIN_SCALE + if (lightboxIncrease) + lightboxIncrease.disabled = imageScale >= IMAGE_MAX_SCALE +} + +function broadcastImageView() { + if (!activePreviewRequestId) + return + pendingSharedView = currentImageView() + if (imageViewFrame !== undefined) + return + imageViewFrame = requestAnimationFrame(() => { + imageViewFrame = undefined + const view = pendingSharedView + pendingSharedView = undefined + if (!activePreviewRequestId || !view) + return + const command: ImagePreviewViewCommand = { + id: createClientId(), + requestId: activePreviewRequestId, + sentAt: Date.now(), + type: 'view', + view, + viewerId: clientId, + } + lastHandledCommandId = command.id + imageChannel?.postMessage(command) + updateImagePreviewState({ command }) + }) +} + +function applyImageView(view: ImagePreviewView, shouldBroadcast = true, touched = true) { + if (!Number.isFinite(view.scale) || !Number.isFinite(view.centerX) || !Number.isFinite(view.centerY)) + return + imageScale = clampImageScale(view.scale) + imageCenterX = Math.min(1, Math.max(0, view.centerX)) + imageCenterY = Math.min(1, Math.max(0, view.centerY)) + imageViewTouched ||= touched + renderImageView() + if (shouldBroadcast) + broadcastImageView() +} + +function resetImageView() { + const geometry = imageGeometry(1) + if (!geometry) + return + const scale = clampImageScale(Math.min( + 1, + geometry.viewportWidth / geometry.imageWidth, + geometry.viewportHeight / geometry.imageHeight, + )) + imageViewTouched = false + applyImageView({ centerX: 0.5, centerY: 0.5, scale }, false, false) +} + +function zoomImageAt(requestedScale: number, clientX?: number, clientY?: number, shouldBroadcast = true) { + const oldGeometry = imageGeometry() + if (!oldGeometry || !lightboxViewport) + return + const scale = clampImageScale(requestedScale) + let centerX = imageCenterX + let centerY = imageCenterY + if (clientX !== undefined && clientY !== undefined && scale !== imageScale) { + const viewportRect = lightboxViewport.getBoundingClientRect() + const pointerX = clientX - viewportRect.left + const pointerY = clientY - viewportRect.top + const imageLeft = oldGeometry.viewportWidth / 2 - imageCenterX * oldGeometry.imageWidth + const imageTop = oldGeometry.viewportHeight / 2 - imageCenterY * oldGeometry.imageHeight + const anchorX = (pointerX - imageLeft) / oldGeometry.imageWidth + const anchorY = (pointerY - imageTop) / oldGeometry.imageHeight + const nextWidth = lightboxImage!.naturalWidth * scale + const nextHeight = lightboxImage!.naturalHeight * scale + centerX = (oldGeometry.viewportWidth / 2 - pointerX + anchorX * nextWidth) / nextWidth + centerY = (oldGeometry.viewportHeight / 2 - pointerY + anchorY * nextHeight) / nextHeight + } + applyImageView({ centerX, centerY, scale }, shouldBroadcast) +} + +function panImageBy(deltaX: number, deltaY: number, shouldBroadcast = true) { + const geometry = imageGeometry() + if (!geometry) + return + applyImageView({ + centerX: imageCenterX - deltaX / geometry.imageWidth, + centerY: imageCenterY - deltaY / geometry.imageHeight, + scale: imageScale, + }, shouldBroadcast) +} + +function pinchMetrics() { + const pointers = Array.from(imagePointers.values()).slice(0, 2) + if (pointers.length < 2) + return undefined + return { + distance: Math.hypot(pointers[1].x - pointers[0].x, pointers[1].y - pointers[0].y), + midpoint: { + x: (pointers[0].x + pointers[1].x) / 2, + y: (pointers[0].y + pointers[1].y) / 2, + }, + } +} + +function handleImagePointerDown(event: PointerEvent) { + if (!lightboxViewport || (event.pointerType === 'mouse' && event.button !== 0)) + return + event.preventDefault() + event.stopPropagation() + if (imagePointers.size === 0) + lightboxPointerMoved = false + imagePointers.set(event.pointerId, { x: event.clientX, y: event.clientY }) + try { + lightboxViewport.setPointerCapture(event.pointerId) + } + catch { + // Synthetic pointer events used by tests do not participate in native pointer capture. + } + lightboxViewport.classList.add('easy-image-lightbox-dragging') + const metrics = pinchMetrics() + pinchDistance = metrics?.distance ?? 0 + pinchMidpoint = metrics?.midpoint +} + +function handleImagePointerMove(event: PointerEvent) { + const previous = imagePointers.get(event.pointerId) + if (!previous) + return + event.preventDefault() + event.stopPropagation() + const current = { x: event.clientX, y: event.clientY } + imagePointers.set(event.pointerId, current) + if (Math.hypot(current.x - previous.x, current.y - previous.y) > 1) + lightboxPointerMoved = true + + if (imagePointers.size === 1) { + panImageBy(current.x - previous.x, current.y - previous.y) + return + } + + const metrics = pinchMetrics() + if (!metrics) + return + if (pinchDistance > 0) + zoomImageAt(imageScale * metrics.distance / pinchDistance, metrics.midpoint.x, metrics.midpoint.y, false) + if (pinchMidpoint) + panImageBy(metrics.midpoint.x - pinchMidpoint.x, metrics.midpoint.y - pinchMidpoint.y) + pinchDistance = metrics.distance + pinchMidpoint = metrics.midpoint +} + +function handleImagePointerEnd(event: PointerEvent) { + if (!imagePointers.has(event.pointerId)) + return + event.preventDefault() + event.stopPropagation() + imagePointers.delete(event.pointerId) + if (lightboxViewport?.hasPointerCapture(event.pointerId)) + lightboxViewport.releasePointerCapture(event.pointerId) + const metrics = pinchMetrics() + pinchDistance = metrics?.distance ?? 0 + pinchMidpoint = metrics?.midpoint + if (imagePointers.size === 0) + lightboxViewport?.classList.remove('easy-image-lightbox-dragging') +} + +function handleImageWheel(event: WheelEvent) { + event.preventDefault() + event.stopPropagation() + const sensitivity = event.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 0.002 : 0.05 + zoomImageAt(imageScale * Math.exp(-event.deltaY * sensitivity), event.clientX, event.clientY) +} + +function handleLightboxImageLoad() { + if (!lightbox || lightbox.hidden) + return + resetImageView() + if (pendingRemoteView) { + const view = pendingRemoteView + pendingRemoteView = undefined + applyImageView(view, false) + } +} + +function createImageScaleButton(label: string, text: string, onClick: () => void) { + const button = document.createElement('button') + button.type = 'button' + button.className = 'easy-image-scale-button' + button.setAttribute('aria-label', label) + button.title = label + button.textContent = text + button.addEventListener('pointerdown', event => event.stopPropagation()) + button.addEventListener('click', (event) => { + event.preventDefault() + event.stopPropagation() + onClick() + }) + return button +} + function createLightbox() { const overlay = document.createElement('div') overlay.className = 'easy-image-lightbox' @@ -235,9 +542,20 @@ function createLightbox() { overlay.setAttribute('aria-modal', 'true') overlay.setAttribute('aria-label', '图片全屏预览') + const viewport = document.createElement('div') + viewport.className = 'easy-image-lightbox-viewport' + const preview = document.createElement('img') preview.className = 'easy-image-lightbox-preview' preview.alt = '' + preview.draggable = false + preview.addEventListener('load', handleLightboxImageLoad) + viewport.addEventListener('pointerdown', handleImagePointerDown) + viewport.addEventListener('pointermove', handleImagePointerMove) + viewport.addEventListener('pointerup', handleImagePointerEnd) + viewport.addEventListener('pointercancel', handleImagePointerEnd) + viewport.addEventListener('wheel', handleImageWheel, { passive: false }) + viewport.append(preview) const close = document.createElement('button') close.type = 'button' @@ -245,38 +563,66 @@ function createLightbox() { close.setAttribute('aria-label', '关闭图片全屏预览') close.title = '关闭(Esc)' close.textContent = '×' + close.addEventListener('pointerdown', event => event.stopPropagation()) close.addEventListener('click', (event) => { event.preventDefault() event.stopPropagation() closeLightbox() }) + const toolbar = document.createElement('nav') + toolbar.className = 'easy-image-scale-toolbar' + toolbar.setAttribute('aria-label', '图片缩放') + toolbar.addEventListener('pointerdown', event => event.stopPropagation()) + const decrease = createImageScaleButton('缩小图片', '−', () => zoomImageAt(imageScale - IMAGE_SCALE_STEP)) + const scaleOutput = document.createElement('output') + scaleOutput.className = 'easy-image-scale-value' + scaleOutput.setAttribute('aria-live', 'polite') + scaleOutput.value = '100%' + scaleOutput.textContent = '100%' + const increase = createImageScaleButton('放大图片', '+', () => zoomImageAt(imageScale + IMAGE_SCALE_STEP)) + toolbar.append(decrease, scaleOutput, increase) + overlay.addEventListener('pointerdown', event => event.stopPropagation()) overlay.addEventListener('click', (event) => { event.preventDefault() event.stopPropagation() - if (event.target === overlay) + const clickedBackdrop = event.target === overlay || event.target === viewport + if (clickedBackdrop && !lightboxPointerMoved) closeLightbox() + lightboxPointerMoved = false }) - overlay.append(preview, close) + overlay.append(viewport, close, toolbar) document.body.append(overlay) lightbox = overlay + lightboxViewport = viewport lightboxImage = preview lightboxClose = close + lightboxDecrease = decrease + lightboxIncrease = increase + lightboxScaleOutput = scaleOutput } -function showLightbox(src: string, alt: string, image?: HTMLImageElement) { +function showLightbox(src: string, alt: string, image?: HTMLImageElement, requestId?: string) { if (!lightbox || !lightboxImage || !lightboxClose) return const wasHidden = lightbox.hidden activeImage = image - lightboxImage.src = src + activePreviewRequestId = requestId + pendingRemoteView = undefined + imageViewTouched = false + imageScale = 1 + imageCenterX = 0.5 + imageCenterY = 0.5 lightboxImage.alt = alt lightbox.hidden = false + lightboxImage.src = src if (wasHidden) bodyOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' document.addEventListener('keydown', handleLightboxKeydown, true) + if (lightboxImage.complete && lightboxImage.naturalWidth) + requestAnimationFrame(handleLightboxImageLoad) lightboxClose.focus() } @@ -284,17 +630,54 @@ function openLightbox(image: HTMLImageElement) { showLightbox(image.currentSrc || image.src, image.alt, image) } -function closeLightbox(restoreFocus = true) { +function broadcastImageClose() { + if (!activePreviewRequestId) + return + const command: ImagePreviewCloseCommand = { + id: createClientId(), + requestId: activePreviewRequestId, + sentAt: Date.now(), + type: 'close', + viewerId: clientId, + } + lastHandledCommandId = command.id + imageChannel?.postMessage(command) + updateImagePreviewState({ command }) +} + +function closeLightbox(restoreFocus = true, shouldBroadcast = true) { if (!lightbox || lightbox.hidden) return + if (shouldBroadcast) + broadcastImageClose() document.removeEventListener('keydown', handleLightboxKeydown, true) lightbox.hidden = true - if (lightboxImage) + if (imageViewFrame !== undefined) { + cancelAnimationFrame(imageViewFrame) + imageViewFrame = undefined + } + pendingSharedView = undefined + pendingRemoteView = undefined + imagePointers.clear() + lightboxViewport?.classList.remove('easy-image-lightbox-dragging', 'easy-image-lightbox-pannable') + if (lightboxImage) { lightboxImage.removeAttribute('src') + lightboxImage.style.removeProperty('width') + lightboxImage.style.removeProperty('height') + lightboxImage.style.removeProperty('transform') + delete lightboxImage.dataset.easyImageScale + delete lightboxImage.dataset.easyImageCenterX + delete lightboxImage.dataset.easyImageCenterY + } document.body.style.overflow = bodyOverflow if (restoreFocus) activeImage?.focus() activeImage = undefined + activePreviewRequestId = undefined + imageScale = 1 + imageCenterX = 0.5 + imageCenterY = 0.5 + imageViewTouched = false } function handleLightboxKeydown(event: KeyboardEvent) { @@ -304,8 +687,18 @@ function handleLightboxKeydown(event: KeyboardEvent) { closeLightbox() } else if (event.key === 'Tab') { + const focusable = [lightboxClose, lightboxDecrease, lightboxIncrease].filter( + (element): element is HTMLButtonElement => Boolean(element && !element.disabled), + ) + if (!focusable.length) + return event.preventDefault() - lightboxClose?.focus() + const currentIndex = focusable.indexOf(document.activeElement as HTMLButtonElement) + const direction = event.shiftKey ? -1 : 1 + const nextIndex = currentIndex < 0 + ? 0 + : (currentIndex + direction + focusable.length) % focusable.length + focusable[nextIndex].focus() } } @@ -368,13 +761,43 @@ function handleImagePreviewAcknowledgement(acknowledgement: ImagePreviewAcknowle closeLightbox(false) } +function isImagePreviewCommand(command: ImagePreviewCommand | undefined): command is ImagePreviewCommand { + if (!command || !command.id || !command.requestId || !command.viewerId || !Number.isFinite(command.sentAt)) + return false + if (command.type === 'close') + return true + return command.type === 'view' + && Number.isFinite(command.view?.scale) + && Number.isFinite(command.view?.centerX) + && Number.isFinite(command.view?.centerY) +} + +function handleImagePreviewCommand(command: ImagePreviewCommand | undefined) { + if (!isImagePreviewCommand(command) || !isProjectionRoute() || command.id === lastHandledCommandId) + return + if (command.requestId !== activePreviewRequestId) + return + if (command.sentAt < mountedAt - 100 || Date.now() - command.sentAt > 10_000) + return + lastHandledCommandId = command.id + if (command.type === 'close') { + closeLightbox(false, false) + return + } + if (!lightboxImage?.naturalWidth) { + pendingRemoteView = command.view + return + } + applyImageView(command.view, false) +} + function handleImagePreviewRequest(request: ImagePreviewRequest | undefined) { if (!request || !isProjectionRoute() || request.id === lastHandledRequestId) return if (request.sentAt < mountedAt - 100 || Date.now() - request.sentAt > 10_000) return lastHandledRequestId = request.id - showLightbox(request.src, request.alt) + showLightbox(request.src, request.alt, undefined, request.id) updateImagePreviewState({ acknowledgement: { requestId: request.id, @@ -574,6 +997,16 @@ function scheduleCodeHeightFit() { }) } +function handleViewportResize() { + scheduleCodeHeightFit() + if (!lightbox || lightbox.hidden || !lightboxImage?.naturalWidth) + return + if (imageViewTouched) + renderImageView() + else + resetImageView() +} + function cleanupImage(image: HTMLImageElement, interaction: ImageInteraction) { image.removeEventListener('click', interaction.click) image.removeEventListener('keydown', interaction.keydown) @@ -628,13 +1061,17 @@ function cleanupCodeHosts() { function cleanupContent() { clearPendingImagePreview() - closeLightbox() + closeLightbox(false, false) imageInteractions.forEach((interaction, image) => cleanupImage(image, interaction)) imageInteractions.clear() lightbox?.remove() lightbox = undefined + lightboxViewport = undefined lightboxImage = undefined lightboxClose = undefined + lightboxDecrease = undefined + lightboxIncrease = undefined + lightboxScaleOutput = undefined cleanupTableHosts() cleanupCodeHosts() } @@ -659,11 +1096,17 @@ onMounted(() => { if (isScaleMessage(event.data)) updateScale(event.data.key, event.data.kind, event.data.scale, false) }) + imageChannel = new BroadcastChannel(`easy-slides:image-preview:${import.meta.env.BASE_URL}`) + imageChannel.addEventListener('message', event => handleImagePreviewCommand(event.data as ImagePreviewCommand)) } stopAcknowledgementWatch = watch( () => easySharedState.easySlidesImagePreview?.acknowledgement, handleImagePreviewAcknowledgement, ) + stopCommandWatch = watch( + () => easySharedState.easySlidesImagePreview?.command, + handleImagePreviewCommand, + ) stopRequestWatch = watch( () => easySharedState.easySlidesImagePreview?.request, handleImagePreviewRequest, @@ -676,7 +1119,7 @@ onMounted(() => { scheduleCodeHeightFit() }) observer.observe(document.body, { childList: true, subtree: true }) - window.addEventListener('resize', scheduleCodeHeightFit) + window.addEventListener('resize', handleViewportResize) void document.fonts?.ready.then(scheduleCodeHeightFit) scheduleCodeHeightFit() }) @@ -685,9 +1128,11 @@ onBeforeUnmount(() => { observer?.disconnect() resizeObserver?.disconnect() channel?.close() + imageChannel?.close() stopAcknowledgementWatch?.() + stopCommandWatch?.() stopRequestWatch?.() - window.removeEventListener('resize', scheduleCodeHeightFit) + window.removeEventListener('resize', handleViewportResize) if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) cleanupContent() diff --git a/dist-cli/theme/components/PresenterAccess.vue b/dist-cli/theme/components/PresenterAccess.vue index c5fc84f..5631b36 100644 --- a/dist-cli/theme/components/PresenterAccess.vue +++ b/dist-cli/theme/components/PresenterAccess.vue @@ -47,10 +47,6 @@ function lock() { syncBodyState() } -function openPresenter() { - window.location.href = `${import.meta.env.BASE_URL}presenter/` -} - function resizeNotes(delta: number) { noteSize.value = Math.min(38, Math.max(14, noteSize.value + delta)) document.documentElement.style.setProperty('--easy-presenter-note-size', `${noteSize.value}px`) @@ -93,16 +89,6 @@ async function sha256(value: string) {