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) {
-
-