From 4f61388dbd24b39c5769a664c5eebff0131bfb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= <14975046+skjnldsv@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:21:13 +0200 Subject: [PATCH] feat(viewer): say what to expect on Nextcloud 35 and older MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library runs next to the viewer app of older servers, which nothing warned about. The README says what was checked on 35, and the viewer logs a warning when it finds the viewer app, or a server that says it is older. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com> --- README.md | 21 ++++++++++++++++++ __tests__/component/Viewer.spec.ts | 18 +++++++++++++++ __tests__/legacyServer.spec.ts | 35 ++++++++++++++++++++++++++++++ lib/global.d.ts | 10 +++++++++ lib/mount.ts | 5 +++++ lib/utils/legacyServer.ts | 21 ++++++++++++++++++ lib/views/Viewer.vue | 9 ++++++-- 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 __tests__/legacyServer.spec.ts create mode 100644 lib/utils/legacyServer.ts diff --git a/README.md b/README.md index 0e61835..048d008 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,27 @@ renders one element with `file`, `files: [base, file]` and `comparison: 'differences'`. `compare(file, base, { view: 'differences' })` opens on it. The view chosen is kept until the viewer closes. +### 🕰️ Nextcloud 35 and older + +The library is made for Nextcloud 36, whose server ships a copy of it. Older +servers still ship the viewer app, and the library runs next to it. Checked on +35.0.2: + +- Call `registerDefaultHandlers()` yourself: nothing else registers the + handlers for images, video and audio there. +- Pictures, videos and sounds open in this library's viewer, from your app and + from the Files list, where its action comes first. +- The viewer app stays enabled, with its own `OCA.Viewer` and Files action. The + handlers other apps register with `OCA.Viewer` are not this library's. +- Types that need a server preview, like HEIC and TIFF, are not offered: the + server has no `core.previews.enabled_providers` capability yet. +- The viewer has no settings button, and `?` does not open them: there is no + server side to keep them on. + +> [!WARNING] +> Two viewers on one page is not a supported setup. The library logs a warning +> when it finds the viewer app, and only images, video and audio were checked. + ### 🧭 Migrating from `OCA.Viewer` The `OCA.Viewer` global is gone. Everything is imported from the diff --git a/__tests__/component/Viewer.spec.ts b/__tests__/component/Viewer.spec.ts index 7688c7a..6fdd7ed 100644 --- a/__tests__/component/Viewer.spec.ts +++ b/__tests__/component/Viewer.spec.ts @@ -562,6 +562,24 @@ describe('the viewer settings', () => { await open(wrapper) await vi.waitFor(() => expect(wrapper.findComponent({ name: 'ViewerSettings' }).exists()).toBe(true)) }) + + // Nextcloud 35 and older, with the viewer app still on the page + it('are not offered on a server older than the library', async () => { + window.OCA = { Viewer: { registerHandler: () => {} } } + try { + const { vm, wrapper } = mountViewer([imageHandler()]) + const file = makeFile({ mime: 'image/jpeg' }) + await vm.open([file], file) + await wrapper.vm.$nextTick() + expect(wrapper.find('.viewer__settings').exists()).toBe(false) + + document.body.dispatchEvent(new KeyboardEvent('keydown', { key: '?', bubbles: true, cancelable: true })) + await flushPromises() + expect(wrapper.findComponent({ name: 'ViewerSettings' }).exists()).toBe(false) + } finally { + delete window.OCA + } + }) }) describe('Viewer sidebar', () => { diff --git a/__tests__/legacyServer.spec.ts b/__tests__/legacyServer.spec.ts new file mode 100644 index 0000000..4986bdc --- /dev/null +++ b/__tests__/legacyServer.spec.ts @@ -0,0 +1,35 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { afterEach, describe, expect, it } from 'vitest' +import { isLegacyServer } from '../lib/utils/legacyServer.ts' + +afterEach(() => { + delete window.OC + delete window.OCA +}) + +describe('a server older than the library is made for', () => { + it('is one still shipping the viewer app', () => { + window.OCA = { Viewer: { registerHandler: () => {} } } + expect(isLegacyServer()).toBe(true) + }) + + it('is one that says it is older', () => { + window.OC = { config: { version: '35.0.2.1' } } + expect(isLegacyServer()).toBe(true) + }) + + // The compatibility layer of 36 keeps OCA.Viewer, but not registerHandler + it('is not 36, with its OCA.Viewer of old', () => { + window.OC = { config: { version: '36.0.0.5' } } + window.OCA = { Viewer: {} } + expect(isLegacyServer()).toBe(false) + }) + + // The playground, or a page that says nothing of itself + it('is not a page that says nothing', () => { + expect(isLegacyServer()).toBe(false) + }) +}) diff --git a/lib/global.d.ts b/lib/global.d.ts index 6ca80fd..97f64e3 100644 --- a/lib/global.d.ts +++ b/lib/global.d.ts @@ -30,6 +30,16 @@ declare global { const __TRANSLATIONS_EAGER__: import('./utils/l10n.ts').Catalog[] interface Window { + OC?: { + config?: { + version?: string + } + } + OCA?: { + Viewer?: { + registerHandler?: unknown + } + } OCP?: { Files?: { Router?: OCPFilesRouter diff --git a/lib/mount.ts b/lib/mount.ts index f2f7293..730128f 100644 --- a/lib/mount.ts +++ b/lib/mount.ts @@ -9,6 +9,7 @@ import { registerImageCustomElement } from './models/images.ts' import { registerSheetmusicCustomElement } from './models/sheetmusic.ts' import { registerVideoCustomElement } from './models/videos.ts' import { logger } from './services/logger.ts' +import { isLegacyServer } from './utils/legacyServer.ts' import { getViewer } from './viewer.ts' /** @@ -38,4 +39,8 @@ export async function mount(): Promise { const ViewerInstance = ViewerApp.mount(ViewerRoot) getViewer()._setViewer(ViewerInstance as InstanceType) logger.info('Viewer mounted', { ViewerInstance }) + + if (isLegacyServer()) { + logger.warn('@nextcloud/viewer is made for Nextcloud 36 and later. This server is older and has a viewer of its own, so Files may open either one, some handlers are missing, and the viewer has no settings. See "Nextcloud 35 and older" in the README of @nextcloud/viewer.') + } } diff --git a/lib/utils/legacyServer.ts b/lib/utils/legacyServer.ts new file mode 100644 index 0000000..35a548b --- /dev/null +++ b/lib/utils/legacyServer.ts @@ -0,0 +1,21 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** The first Nextcloud whose server is made for this library */ +const FIRST_SUPPORTED_MAJOR = 36 + +/** + * Whether the page comes from a Nextcloud older than this library is made + * for: one still shipping the viewer app, whose `OCA.Viewer` has a + * `registerHandler()` that the compatibility layer of 36 does not, or one + * that says it is older. + */ +export function isLegacyServer(): boolean { + if (typeof window.OCA?.Viewer?.registerHandler === 'function') { + return true + } + const major = Number.parseInt(window.OC?.config?.version ?? '', 10) + return major > 0 && major < FIRST_SUPPORTED_MAJOR +} diff --git a/lib/views/Viewer.vue b/lib/views/Viewer.vue index 25c9b6f..7895419 100644 --- a/lib/views/Viewer.vue +++ b/lib/views/Viewer.vue @@ -264,6 +264,7 @@ { editing.value = true } }, { ctrl: true, allowInModal: true }) -// The settings, which list these shortcuts, on ? as in the Files app +// The settings, which list these shortcuts, on ? as in the Files app. Not on +// a server older than the library, which has a viewer of its own and keeps +// none of them +const hasSettings = !isLegacyServer() const settingsOpen = ref(false) const { slideshowDelay } = useViewerSettings() useHotKey('?', (event) => { - if (currentFile.value === undefined || editing.value) { + if (!hasSettings || currentFile.value === undefined || editing.value) { return } event.preventDefault()