Skip to content

Commit 64169fb

Browse files
committed
fixup! fixup! fixup! feat: viewer 6.0.0
Signed-off-by: John Molakvoæ (skjnldsv) <[email protected]>
1 parent fe11657 commit 64169fb

File tree

10 files changed

+272
-61
lines changed

10 files changed

+272
-61
lines changed

src/api_package/custom.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
declare module '*.svg?raw' {
6+
const content: string
7+
export default content
8+
}
9+
10+
declare module '*.vue' {
11+
import Vue from 'vue'
12+
export default Vue
13+
}

src/api_package/index.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
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'
816
export 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

74165
export function getHandlers() : Map<string, IHandler> {

src/api_package/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"include": ["index.ts", "global.d.ts"],
2+
"include": ["*.ts"],
33
"compilerOptions": {
44
"lib": [
55
"DOM",

src/components/ImageEditor.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
</div>
99
</template>
1010
<script>
11-
import { showError, showSuccess, DialogBuilder } from '@nextcloud/dialogs'
11+
import { basename, dirname, extname, join } from 'path'
1212
import { emit } from '@nextcloud/event-bus'
13-
import { Node } from '@nextcloud/files'
1413
import { linkTo } from '@nextcloud/router'
14+
import { Node } from '@nextcloud/files'
15+
import { showError, showSuccess, DialogBuilder } from '@nextcloud/dialogs'
1516
import axios from '@nextcloud/axios'
16-
import { basename, dirname, extname, join } from 'path'
17-
import translations from '../models/editorTranslations.js'
17+
18+
import { logger } from '../services/logger.ts'
1819
import { rawStat } from '../services/FileInfo.ts'
19-
import logger from '../services/logger.js'
20+
import translations from '../models/editorTranslations.js'
2021
2122
let TABS, TOOLS
2223

src/components/Images.vue

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,18 @@
7373
</template>
7474

7575
<script lang="ts">
76-
import Vue from 'vue'
77-
import AsyncComputed from 'vue-async-computed'
78-
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'
79-
80-
import axios from '@nextcloud/axios'
8176
import { basename } from '@nextcloud/paths'
77+
import { Node } from '@nextcloud/files'
8278
import { translate } from '@nextcloud/l10n'
83-
import { NcLoadingIcon } from '@nextcloud/vue'
79+
import axios from '@nextcloud/axios'
8480
81+
import { NcLoadingIcon } from '@nextcloud/vue'
82+
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'
8583
import ImageEditor from './ImageEditor.vue'
84+
8685
import { findLivePhotoPeerFromFileId } from '../utils/livePhotoUtils'
8786
import { getDavPath } from '../utils/fileUtils'
8887
import { preloadMedia } from '../services/mediaPreloader'
89-
90-
Vue.use(AsyncComputed)
91-
9288
export default {
9389
name: 'Images',
9490
@@ -103,6 +99,10 @@ export default {
10399
type: Boolean,
104100
default: false,
105101
},
102+
node: {
103+
type: Node,
104+
required: true,
105+
}
106106
},
107107
data() {
108108
return {
@@ -121,7 +121,7 @@ export default {
121121
122122
computed: {
123123
src() {
124-
return this.source ?? this.davPath
124+
return this.node.source ?? this.davPath
125125
},
126126
zoomHeight() {
127127
return Math.round(this.height * this.zoomRatio)

src/components/Videos.vue

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,12 @@
3737
</template>
3838

3939
<script lang='ts'>
40-
// eslint-disable-next-line n/no-missing-import
41-
import Vue from 'vue'
42-
import AsyncComputed from 'vue-async-computed'
4340
import '@skjnldsv/vue-plyr/dist/vue-plyr.css'
41+
import type { File } from '@nextcloud/files'
4442
4543
import { imagePath } from '@nextcloud/router'
4644
47-
import logger from '../services/logger.js'
45+
import { logger } from '../services/logger.ts'
4846
import { findLivePhotoPeerFromName } from '../utils/livePhotoUtils'
4947
import { getPreviewIfAny } from '../utils/previewUtils'
5048
import { preloadMedia } from '../services/mediaPreloader.js'
@@ -53,14 +51,39 @@ const VuePlyr = () => import(/* webpackChunkName: 'plyr' */'@skjnldsv/vue-plyr')
5351
5452
const blankVideo = imagePath('viewer', 'blank.mp4')
5553
56-
Vue.use(AsyncComputed)
57-
5854
export default {
5955
name: 'Videos',
6056
6157
components: {
6258
VuePlyr,
6359
},
60+
61+
props: {
62+
node: {
63+
type: Node,
64+
required: true,
65+
},
66+
nodes: {
67+
type: Array as () => File[],
68+
required: true,
69+
},
70+
isFullScreen: {
71+
type: Boolean,
72+
default: false,
73+
},
74+
isSidebarShown: {
75+
type: Boolean,
76+
default: false,
77+
},
78+
height: {
79+
type: Number,
80+
required: true,
81+
},
82+
width: {
83+
type: Number,
84+
required: true,
85+
},
86+
},
6487
data() {
6588
return {
6689
isFullscreenButtonVisible: false,
@@ -70,7 +93,7 @@ export default {
7093
7194
computed: {
7295
livePhotoPath() {
73-
const peerFile = findLivePhotoPeerFromName(this, this.fileList)
96+
const peerFile = findLivePhotoPeerFromName(this, this.nodes)
7497
7598
if (peerFile === undefined) {
7699
return undefined
@@ -83,7 +106,7 @@ export default {
83106
},
84107
options() {
85108
return {
86-
autoplay: this.active === true,
109+
autoplay: true,
87110
// Used to reset the video streams https://github.com/sampotts/plyr#javascript-1
88111
blankVideo,
89112
controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'fullscreen'],

src/init.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { getViewer } from './api_package/viewer.ts'
88
import { logger } from './services/logger.ts'
99
import Viewer from './views/Viewer.vue'
1010

11+
import { registerVideoCustomElement, registerVideoHandler } from './models/videos.ts'
12+
1113
const ViewerService = getViewer()
1214
const ViewerApp = createApp(Viewer)
13-
ViewerApp.config.devtools = { appId: 'Viewer' }
1415

1516
// Create top wrapper element
1617
const ViewerRoot = document.createElement('div')
@@ -20,4 +21,8 @@ document.body.appendChild(ViewerRoot)
2021
// Mount and set the viewer instance
2122
const ViewerInstance = ViewerApp.mount(ViewerRoot)
2223
ViewerService._setViewer(ViewerInstance as InstanceType<typeof Viewer>)
23-
logger.debug('Viewer initialized', { ViewerInstance })
24+
logger.debug('Viewer initialized', { ViewerInstance })
25+
26+
// register the custom elements for all handlers
27+
registerVideoCustomElement()
28+
registerVideoHandler()

src/models/images.js renamed to src/models/images.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import { defineCustomElement } from 'vue'
67
import { loadState } from '@nextcloud/initial-state'
7-
import logger from '../services/logger.js'
8+
import { logger} from '../services/logger.ts'
89
import Images from '../components/Images.vue'
10+
import { registerHandler } from '../api_package/index.ts'
11+
import { t } from '@nextcloud/l10n'
912

10-
const enabledPreviewProviders = loadState(appName, 'enabled_preview_providers', [])
13+
const enabledPreviewProviders = loadState<string[]>('viewer', 'enabled_preview_providers', [])
1114

1215
/**
1316
* Those mimes needs a proper preview to be displayed
@@ -55,12 +58,40 @@ if (ignoredMimes.length > 0) {
5558
logger.warn('Some mimes were ignored because they are not enabled in the server previews config', { ignoredMimes })
5659
}
5760

58-
export default {
59-
id: 'images',
60-
group: 'media',
61-
mimes: [
62-
...browserSupportedMimes,
63-
...enabledMimes,
64-
],
65-
component: Images,
61+
export const tagname = 'oca-viewer-image'
62+
export function registerImageCustomElement() {
63+
const ImageElement = defineCustomElement(Images, {
64+
shadowRoot: false,
65+
})
66+
67+
// Register the custom element.
68+
customElements.define(tagname, ImageElement)
6669
}
70+
71+
export function registerImageHandler() {
72+
registerHandler({
73+
id: 'images',
74+
displayName: t('viewer', 'Images'),
75+
tagname,
76+
77+
enabled: (nodes) => {
78+
if (nodes.length === 0) {
79+
return false
80+
}
81+
82+
return nodes.every(node => {
83+
// Always allow browser supported mimes
84+
if (browserSupportedMimes.includes(node.mime)) {
85+
return true
86+
}
87+
88+
// Only allow preview supported mimes if they are enabled in the server config
89+
if (enabledMimes.includes(node.mime)) {
90+
return true
91+
}
92+
return false
93+
})
94+
},
95+
})
96+
logger.info('Image handler registered', { tagname, enabledMimes, browserSupportedMimes })
97+
}

0 commit comments

Comments
 (0)