diff --git a/README.md b/README.md index 701582a..b9955c6 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,7 @@ The full handler shape (see the `IHandler` interface): | `group` | `string` | no | Group used to combine handlers when opening a folder | | `preload` | `(node: File) => Promise` | no | Preload data for neighbouring files | | `theme` | `'dark' \| 'light' \| 'default'` | no | Viewer modal theme | +| `canCompare` | `boolean` | no | Comparing two versions is worth offering, see `canCompare(node)` | | `supportsEndToEndEncryption` | `boolean` | no | Whether the handler supports end-to-end encrypted files | Gotchas: @@ -453,7 +454,7 @@ instead, and the viewer works with `@nextcloud/files` nodes rather than the | `OCA.Viewer.mimetypesCompare.includes(node.mime)` | `canView(node)` | | `OCA.Viewer.availableHandlers` | `getHandlers()`, or `canView(node)` to test one file | | `OCA.Viewer.registerHandler({ component })` | `registerHandler({ tagName })`, see above | -| `canCompare: true` on a handler | nothing, any handler can be compared | +| `canCompare: true` on a handler | the same, and `canCompare(node)` to decide whether to offer it | | `\OCP\Util::addScript` for the registration | `\OCP\Util::addInitScript` | | A listener for `OCA\Viewer\Event\LoadViewer` | a listener for `BeforeTemplateRenderedEvent`, see the [tutorial](#4-load-it-on-every-page) | | Dispatching `OCA\Viewer\Event\LoadViewer` | nothing, the viewer is on every page already | diff --git a/__tests__/canView.spec.ts b/__tests__/canView.spec.ts index d3d5b3a..bb7acb4 100644 --- a/__tests__/canView.spec.ts +++ b/__tests__/canView.spec.ts @@ -4,7 +4,7 @@ */ import { Folder, Permission } from '@nextcloud/files' import { describe, expect, it } from 'vitest' -import { canView, registerHandler } from '../lib/index.ts' +import { canCompare, canView, registerHandler } from '../lib/index.ts' import { makeFile, makeHandler } from './factories.ts' describe('canView', () => { @@ -44,3 +44,27 @@ describe('canView', () => { expect(canView([image, video])).toBe(false) }) }) + +describe('canCompare', () => { + it('is true for a file whose handler says comparing it is worth it', () => { + registerHandler(makeHandler({ id: 'images', canCompare: true, enabled: (nodes) => nodes.every((node) => node.mime === 'image/jpeg') })) + registerHandler(makeHandler({ id: 'videos', enabled: (nodes) => nodes.every((node) => node.mime === 'video/mp4') })) + + expect(canCompare(makeFile({ mime: 'image/jpeg' }))).toBe(true) + expect(canCompare(makeFile({ mime: 'video/mp4' }))).toBe(false) + }) + + // The handler the viewer would show it with, not any that takes it + it('asks the first handler to take the file', () => { + registerHandler(makeHandler({ id: 'first', enabled: () => true })) + registerHandler(makeHandler({ id: 'second', canCompare: true, enabled: () => true })) + + expect(canCompare(makeFile())).toBe(false) + }) + + it('is false for a file that cannot be viewed', () => { + registerHandler(makeHandler({ id: 'everything', canCompare: true, enabled: () => true })) + + expect(canCompare(makeFile({ permissions: Permission.NONE }))).toBe(false) + }) +}) diff --git a/__tests__/entry.spec.ts b/__tests__/entry.spec.ts index 0ecaa1b..7389583 100644 --- a/__tests__/entry.spec.ts +++ b/__tests__/entry.spec.ts @@ -53,6 +53,7 @@ describe('importing @nextcloud/viewer', () => { expect(Object.keys(entry).sort()).toEqual([ 'Viewer', + 'canCompare', 'canView', 'getHandlers', 'getViewer', diff --git a/__tests__/models.spec.ts b/__tests__/models.spec.ts index 1af0d03..1203313 100644 --- a/__tests__/models.spec.ts +++ b/__tests__/models.spec.ts @@ -55,6 +55,9 @@ describe('handler groups', () => { expect(handlerById('videos').group).toBe('media') expect(handlerById('audios').group).toBe('media') expect(handlerById('images').group).toBe('media') + // Two versions of a picture side by side show what changed + expect(handlerById('images').canCompare).toBe(true) + expect(handlerById('videos').canCompare).toBeUndefined() }) }) diff --git a/lib/handlers.ts b/lib/handlers.ts index f2bed6b..ce00bd0 100644 --- a/lib/handlers.ts +++ b/lib/handlers.ts @@ -74,6 +74,14 @@ export interface IHandler { */ canEdit?: boolean + /** + * Whether comparing two versions of a file side by side is worth offering + * for this handler's files. It is a hint for callers deciding whether to + * offer it at all (see `canCompare()`): `getViewer().compare()` shows any + * pair either way. + */ + canCompare?: boolean + /** * Whether this handler works with end-to-end encrypted files. * @@ -103,6 +111,24 @@ export function canView(nodes: INode | INode[]): boolean { return countEnabledHandlers(Array.isArray(nodes) ? nodes : [nodes], 1) } +/** + * Whether comparing two versions of a file is worth offering. + * + * For a caller deciding whether to show a "Compare" action, like the + * versions tab of the Files sidebar: the file has to be viewable, and the + * handler the viewer would open it with has to say so with `canCompare`. + * + * @param node - The file to compare versions of + */ +export function canCompare(node: INode): boolean { + if (!canView(node)) { + return false + } + // The one the viewer would show it with: the first to take it + const handler = [...getHandlers().values()].find((candidate) => isHandlerEnabled(candidate, [node as IFile])) + return handler?.canCompare === true +} + /** * Whether a handler accepts the given files. * diff --git a/lib/index.ts b/lib/index.ts index 8334a1a..cb6cdc7 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -26,7 +26,7 @@ registerImplementation({ // It is an empty shell until the viewer is mounted. getViewer() -export { canView, getHandlers, registerHandler } from './handlers.ts' +export { canCompare, canView, getHandlers, registerHandler } from './handlers.ts' export type { IHandler } from './handlers.ts' export { getViewer, Viewer } from './viewer.ts' export type { ViewerAPI, ViewerEmits, ViewerOptions, ViewerProps } from './viewer.ts' diff --git a/lib/models/images.ts b/lib/models/images.ts index e0f75fa..50dc417 100644 --- a/lib/models/images.ts +++ b/lib/models/images.ts @@ -103,6 +103,8 @@ export function registerImageHandler() { group: 'media', supportsEndToEndEncryption: true, canEdit: true, + // Side by side is how two versions of a picture show what changed + canCompare: true, enabled: (nodes) => { if (nodes.length === 0) {