Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions __tests__/component/Viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
35 changes: 35 additions & 0 deletions __tests__/legacyServer.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
10 changes: 10 additions & 0 deletions lib/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -38,4 +39,8 @@ export async function mount(): Promise<void> {
const ViewerInstance = ViewerApp.mount(ViewerRoot)
getViewer()._setViewer(ViewerInstance as InstanceType<typeof Viewer>)
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.')
}
}
21 changes: 21 additions & 0 deletions lib/utils/legacyServer.ts
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 7 additions & 2 deletions lib/views/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@

<!-- Where apps keep their settings, floating as there is no navigation -->
<NcButton
v-if="hasSettings"
class="viewer__settings"
variant="tertiary"
:aria-label="t('Viewer settings')"
Expand Down Expand Up @@ -354,6 +355,7 @@ import { initHandlerElement } from '../utils/customElements.ts'
import { restoreTitle, setViewerTitle } from '../utils/documentTitle.ts'
import { emittedValue, toError } from '../utils/handlerEvents.ts'
import { t } from '../utils/l10n.ts'
import { isLegacyServer } from '../utils/legacyServer.ts'
import { blurhash, fitInto, photoSize } from '../utils/photoMetadata.ts'
import { renameFile } from '../utils/rename.ts'
import { getViewer } from '../viewer.ts'
Expand Down Expand Up @@ -792,11 +794,14 @@ useHotKey('e', (event) => {
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()
Expand Down
Loading