22 * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
33 * SPDX-License-Identifier: AGPL-3.0-or-later
44 */
5+ import type { File , Node } from '@nextcloud/files'
56
6- import { File } from '@nextcloud/files'
7+ import { DefaultType , FileAction , FileType , getFileActions , registerFileAction } from '@nextcloud/files'
8+ import FileSvg from '@mdi/svg/svg/file.svg?raw'
9+ import OpenInAppSvg from '@mdi/svg/svg/open-in-app.svg?raw'
710
11+ import { getViewer } from './viewer.ts'
12+ import { t } from '@nextcloud/l10n'
13+ import { logger } from '../services/logger.ts'
14+
15+ const ACTION_VIEWER = 'viewer-open'
816export interface IHandler {
917 /**
1018 * Unique identifier for the handler
@@ -16,6 +24,11 @@ export interface IHandler {
1624 */
1725 displayName : string
1826
27+ /**
28+ * Optional icon for the handler
29+ */
30+ iconSvgInline ?: string
31+
1932 /**
2033 * The custom element tag name to use for this handler.
2134 */
@@ -51,6 +64,43 @@ export interface IHandler {
5164 theme ?: 'dark' | 'light' | 'default'
5265}
5366
67+ const topLevelViewerAction = new FileAction ( {
68+ id : ACTION_VIEWER ,
69+ displayName : ( ) => t ( 'viewer' , 'Open with …' ) ,
70+ iconSvgInline : ( ) => OpenInAppSvg ,
71+ order : - 1000 ,
72+
73+ enabled : ( files : Node [ ] ) => {
74+ if ( files . length === 0 ) {
75+ return false
76+ }
77+
78+ // We do not support folders
79+ if ( files . some ( file => file . type !== FileType . File ) ) {
80+ return false
81+ }
82+
83+ // Check if we have more than one handler that can handle this mime
84+ // If yes, we return true to show the "Open with ..." menu
85+ let supportedHandlerCount = 0
86+ for ( const handler of getHandlers ( ) . values ( ) ) {
87+ if ( handler . enabled ( files ) ) {
88+ supportedHandlerCount ++
89+ }
90+ // TODO: Change to 1
91+ if ( supportedHandlerCount > 0 ) {
92+ return true
93+ }
94+ }
95+
96+ logger . debug ( 'No hander found for the given nodes' , { files } )
97+ return false
98+ } ,
99+ exec ( ) {
100+ return Promise . resolve ( null )
101+ }
102+ } )
103+
54104/**
55105 * Register a new handler for the viewer.
56106 * This needs to be called before the viewer is initialized to ensure the handler is available.
@@ -69,6 +119,47 @@ export function registerHandler(handler: IHandler): void {
69119 }
70120
71121 window . _nc_viewer_handlers . set ( handler . id , handler )
122+
123+ registerFileAction ( new FileAction ( {
124+ id : `${ ACTION_VIEWER } -${ handler . id } ` ,
125+ // TRANSLATORS: handler is the translated name of the handler.
126+ displayName : ( ) => t ( 'viewer' , 'Open with {handler}' , { handler : handler . displayName } ) ,
127+
128+ iconSvgInline : ( ) => handler . iconSvgInline ?? FileSvg ,
129+ parent : ACTION_VIEWER ,
130+ order : - 999 ,
131+ default : DefaultType . HIDDEN ,
132+
133+ enabled : ( files : Node [ ] ) => {
134+ if ( files . length === 0 ) {
135+ return false
136+ }
137+
138+ // We do not support folders
139+ if ( files . some ( file => file . type !== FileType . File ) ) {
140+ return false
141+ }
142+
143+ return handler . enabled ( files )
144+ } ,
145+ async exec ( node : Node ) {
146+ if ( node . type !== FileType . File ) {
147+ return null
148+ }
149+
150+ getViewer ( ) . open ( [ node as File ] , node as File )
151+ return null
152+ }
153+ } ) )
154+
155+ // Only register the main "Open with ..." action if not already registered
156+ // This action will be shown if more than one handler is available for the given mime
157+ const actions = getFileActions ( )
158+ if ( ! actions . find ( action => action . id === ACTION_VIEWER ) ) {
159+ registerFileAction ( topLevelViewerAction )
160+
161+ logger . info ( 'Registered top level viewer file action' , { id : ACTION_VIEWER } )
162+ }
72163}
73164
74165export function getHandlers ( ) : Map < string , IHandler > {
0 commit comments