diff --git a/client/src/features/reader/pdf-v4/__tests__/PdfV4ReaderView.spec.ts b/client/src/features/reader/pdf-v4/__tests__/PdfV4ReaderView.spec.ts
index 63aa11c4b..69162b56c 100644
--- a/client/src/features/reader/pdf-v4/__tests__/PdfV4ReaderView.spec.ts
+++ b/client/src/features/reader/pdf-v4/__tests__/PdfV4ReaderView.spec.ts
@@ -45,6 +45,9 @@ vi.mock('../../shared/composables/useReadingSession', () => ({
}),
}))
+const requestFullscreenMock = vi.fn<() => Promise
>().mockResolvedValue(undefined)
+const exitFullscreenMock = vi.fn<() => Promise>().mockResolvedValue(undefined)
+
vi.mock('@/lib/api', () => ({
getAccessToken: () => 'test-token-abc',
}))
@@ -82,13 +85,30 @@ describe('PdfV4ReaderView', () => {
document.documentElement.className = ''
localStorage.clear()
setActivePinia(createPinia())
+ requestFullscreenMock.mockClear()
+ exitFullscreenMock.mockClear()
+ Object.defineProperty(document.documentElement, 'requestFullscreen', {
+ configurable: true,
+ value: requestFullscreenMock,
+ })
+ Object.defineProperty(document, 'exitFullscreen', {
+ configurable: true,
+ value: exitFullscreenMock,
+ })
})
async function mountComponent() {
const pinia = createPinia()
const wrapper = mount(PdfV4ReaderView, {
props: { bookId: 42, fileId: 101 },
- global: { plugins: [pinia] },
+ global: {
+ plugins: [pinia],
+ stubs: {
+ Tooltip: { template: '
' },
+ TooltipTrigger: { template: '
' },
+ TooltipContent: { template: '
' },
+ },
+ },
})
await flushPromises()
await nextTick()
@@ -105,6 +125,14 @@ describe('PdfV4ReaderView', () => {
const wrapper = await mountComponent()
expect(wrapper.find('.mock-pdf-viewer').exists()).toBe(true)
})
+
+ it('requests fullscreen from the floating fullscreen control', async () => {
+ const wrapper = await mountComponent()
+ await wrapper.get('button[aria-label="Enter fullscreen"]').trigger('click')
+ await flushPromises()
+
+ expect(requestFullscreenMock).toHaveBeenCalled()
+ })
})
describe('config construction', () => {
diff --git a/client/src/features/reader/shared/composables/useFullscreen.ts b/client/src/features/reader/shared/composables/useFullscreen.ts
new file mode 100644
index 000000000..8c4f9aba6
--- /dev/null
+++ b/client/src/features/reader/shared/composables/useFullscreen.ts
@@ -0,0 +1,68 @@
+import { onMounted, onUnmounted, ref } from 'vue'
+
+export function useFullscreen(target: () => Element | null = () => document.documentElement) {
+ const isFullscreen = ref(false)
+ const isFullscreenSupported = ref(false)
+
+ function getTarget() {
+ if (typeof document === 'undefined') return null
+ return target() ?? document.documentElement
+ }
+
+ function updateFullscreenState() {
+ if (typeof document === 'undefined') return
+ const fullscreenTarget = getTarget()
+ isFullscreen.value = !!document.fullscreenElement
+ isFullscreenSupported.value = !!document.fullscreenEnabled && !!fullscreenTarget?.requestFullscreen && !!document.exitFullscreen
+ }
+
+ async function enterFullscreen() {
+ const fullscreenTarget = getTarget()
+ if (!fullscreenTarget?.requestFullscreen) return false
+
+ try {
+ await fullscreenTarget.requestFullscreen()
+ return true
+ } catch {
+ return false
+ } finally {
+ updateFullscreenState()
+ }
+ }
+
+ async function exitFullscreen() {
+ if (typeof document === 'undefined' || !document.exitFullscreen) return false
+
+ try {
+ await document.exitFullscreen()
+ return true
+ } catch {
+ return false
+ } finally {
+ updateFullscreenState()
+ }
+ }
+
+ async function toggleFullscreen() {
+ if (typeof document === 'undefined') return false
+ if (document.fullscreenElement) return exitFullscreen()
+ return enterFullscreen()
+ }
+
+ onMounted(() => {
+ updateFullscreenState()
+ document.addEventListener('fullscreenchange', updateFullscreenState)
+ })
+
+ onUnmounted(() => {
+ document.removeEventListener('fullscreenchange', updateFullscreenState)
+ })
+
+ return {
+ isFullscreen,
+ isFullscreenSupported,
+ enterFullscreen,
+ exitFullscreen,
+ toggleFullscreen,
+ }
+}