From e0959fa32f09d6955023f8479d5f4ebc4ce4fcc0 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Fri, 24 Jul 2026 15:43:28 +0200 Subject: [PATCH 1/6] IONOS: feat(viewer): always-show-viewer with default fallback component Add an `always_show_viewer` app config value that, when enabled, makes the viewer open a generic Default component for any file whose mimetype has no dedicated handler, instead of refusing to open it. Includes: - the `always_show_viewer` app config value and a config module that exposes `alwaysShowViewer` / `defaultMimeType` to the frontend - the Default component (mimetype icon + basename), with proper text color, a Safari icon-width fix and a consistent title-color variable - registration of a default (`*/*`) viewer handler and the fallback wiring in Viewer.vue - folder exclusion in the viewer file action's `enabled` callback - previous/next navigation support for the default component enable via: ./occ config:app:set --value yes --type string viewer always_show_viewer Signed-off-by: Misha M.-Kupriyanov Signed-off-by: Franziska Bath Signed-off-by: Thomas Lehmann Co-authored-by: Franziska Bath Co-authored-by: Thomas Lehmann --- lib/Listener/LoadViewerScript.php | 6 +++ src/components/Default.vue | 69 +++++++++++++++++++++++++++++++ src/files_actions/viewerAction.ts | 7 ++++ src/models/config.ts | 12 ++++++ src/models/default.ts | 17 ++++++++ src/services/Viewer.js | 2 + src/views/Viewer.vue | 12 +++++- 7 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/components/Default.vue create mode 100644 src/models/config.ts create mode 100644 src/models/default.ts diff --git a/lib/Listener/LoadViewerScript.php b/lib/Listener/LoadViewerScript.php index 552a22b78..51e3466bc 100644 --- a/lib/Listener/LoadViewerScript.php +++ b/lib/Listener/LoadViewerScript.php @@ -11,6 +11,7 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Viewer\AppInfo\Application; use OCA\Viewer\Event\LoadViewer; +use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Services\IInitialState; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -28,6 +29,7 @@ class LoadViewerScript implements IEventListener { public function __construct( IInitialState $initialStateService, IPreview $previewManager, + private readonly IAppConfig $appConfig, ) { $this->initialStateService = $initialStateService; $this->previewManager = $previewManager; @@ -39,9 +41,13 @@ public function handle(Event $event): void { } Util::addStyle(Application::APP_ID, 'viewer-init'); + + $alwaysShowViewer = $this->appConfig->getAppValue('always_show_viewer', 'no') === 'yes'; + Util::addStyle(Application::APP_ID, 'viewer-main'); Util::addInitScript(Application::APP_ID, 'viewer-init'); Util::addScript(Application::APP_ID, 'viewer-main', 'files'); $this->initialStateService->provideInitialState('enabled_preview_providers', array_keys($this->previewManager->getProviders())); + $this->initialStateService->provideInitialState('always_show_viewer', $alwaysShowViewer); } } diff --git a/src/components/Default.vue b/src/components/Default.vue new file mode 100644 index 000000000..686b3a5bd --- /dev/null +++ b/src/components/Default.vue @@ -0,0 +1,69 @@ + + + + + + diff --git a/src/files_actions/viewerAction.ts b/src/files_actions/viewerAction.ts index 0ebde4433..9011172db 100644 --- a/src/files_actions/viewerAction.ts +++ b/src/files_actions/viewerAction.ts @@ -10,6 +10,7 @@ import { emit } from '@nextcloud/event-bus' import { t } from '@nextcloud/l10n' import svgEye from '@mdi/svg/svg/eye.svg?raw' +import configModule from '../models/config.ts' import logger from '../services/logger.js' /** @@ -106,6 +107,12 @@ export function registerViewerAction() { return false } + // Always enabled if configured so + if (configModule.alwaysShowViewer) { + // disable for folders + return !nodes.some(node => node.type === 'folder') + } + return nodes.every((node) => Boolean(node.permissions & Permission.READ) && window.OCA.Viewer.mimetypes.includes(node.mime), diff --git a/src/models/config.ts b/src/models/config.ts new file mode 100644 index 000000000..20d87b7bd --- /dev/null +++ b/src/models/config.ts @@ -0,0 +1,12 @@ +/** + * SPDX-FileCopyrightText: 2024 STRATO AG + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { loadState } from '@nextcloud/initial-state' + +const alwaysShowViewer = loadState('viewer', 'always_show_viewer', false) + +export default { + alwaysShowViewer, + defaultMimeType: 'all', +} diff --git a/src/models/default.ts b/src/models/default.ts new file mode 100644 index 000000000..94ef98f3a --- /dev/null +++ b/src/models/default.ts @@ -0,0 +1,17 @@ +/** + * SPDX-FileCopyrightText: 2024 STRATO AG + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Default from '../components/Default.vue' +import config from './config.ts' + +export default { + id: 'default', + group: 'other', + mimes: [ + config.defaultMimeType, + ], + mimesAliases: {}, + component: Default, +} diff --git a/src/services/Viewer.js b/src/services/Viewer.js index 3b1076be0..2c671f8f5 100644 --- a/src/services/Viewer.js +++ b/src/services/Viewer.js @@ -6,6 +6,7 @@ import Images from '../models/images.js' import Videos from '../models/videos.js' import Audios from '../models/audios.js' +import Default from '../models/default.ts' import logger from './logger.js' /** @@ -62,6 +63,7 @@ export default class Viewer { this.registerHandler(Images) this.registerHandler(Videos) this.registerHandler(Audios) + this.registerHandler(Default) logger.debug('OCA.Viewer initialized') } diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 7c2245422..a899962b1 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -192,6 +192,7 @@ import { canDownload } from '../utils/canDownload.ts' import { extractFilePaths, extractFilePathFromSource } from '../utils/fileUtils.ts' import { toggleEditor } from '../files_actions/viewerAction.ts' import cancelableRequest from '../utils/CancelableRequest.js' +import configModule from '../models/config.ts' import Error from '../components/Error.vue' import fetchNode from '../services/FetchFile.ts' import File from '../models/file.js' @@ -728,6 +729,11 @@ export default defineComponent({ handler = this.registeredHandlers[mime] ?? this.registeredHandlers[alias] } + // fallback to default viewer if enabled + if (!handler && configModule.alwaysShowViewer) { + handler = this.registeredHandlers[configModule.defaultMimeType] + } + // if we don't have a handler for this mime, abort if (!handler) { logger.error('The following file could not be displayed', { fileInfo }) @@ -745,8 +751,10 @@ export default defineComponent({ this.comparisonFile = null this.updatePreviousNext() + // fallback to default viewer group if enabled + const groupFallback = configModule.alwaysShowViewer ? this.mimeGroups[configModule.defaultMimeType] : undefined // check if part of a group, if so retrieve full files list - const group = this.mimeGroups[mime] + const group = this.mimeGroups[mime] ?? groupFallback if (this.files && this.files.length > 0) { logger.debug('A files list have been provided. No folder content will be fetched.') // we won't sort files here, let's use the order the array has @@ -814,7 +822,7 @@ export default defineComponent({ openFileFromList(fileInfo) { // override mimetype if existing alias const mime = fileInfo.mime - this.currentFile = new File(fileInfo, mime, this.components[mime]) + this.currentFile = new File(fileInfo, mime, this.components[mime] || this.components[configModule.defaultMimeType]) this.changeSidebar() this.updatePreviousNext() }, From 050ef645ea7dc146ba17b8dc2db42043b494e009 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Fri, 24 Jul 2026 15:46:01 +0200 Subject: [PATCH 2/6] IONOS: feat(viewer): hide file name in modal header for default/image view Extract the modal title into its own computed property and, when the always-show-viewer fallback is active, hide the file name in the modal header for the Default component and for images (where the name is already shown elsewhere). Signed-off-by: Misha M.-Kupriyanov Signed-off-by: Franziska Bath Co-authored-by: Franziska Bath --- src/views/Viewer.vue | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index a899962b1..490501067 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -44,7 +44,7 @@ :inline-actions="canEdit ? 1 : 0" :spread-navigation="true" :style="{ width: isSidebarShown ? `${sidebarPosition}px` : null }" - :name="currentFile.basename" + :name="modalTitle" class="viewer" size="full" @close="close" @@ -391,6 +391,14 @@ export default defineComponent({ } }, + modalTitle() { + if (!configModule.alwaysShowViewer) { + return this.currentFile.basename + } + + return this.currentFile?.modal?.name === 'Default' ? '' : this.currentFile.basename + }, + showComparison() { return !this.isMobile }, @@ -1336,6 +1344,14 @@ export default defineComponent({ } } + // The header actions (play/pause, actions menu, close) are normally pushed + // to the right by the full-width `.modal-header__name` element. When the + // modal name is empty (Default handler / always-show-viewer), NcModal omits + // that element, so keep the menu right-aligned explicitly. + :deep(.modal-header .icons-menu) { + margin-inline-start: auto; + } + &__content { width: 100%; height: 100%; From 35285b5ac4e3d945d2d2550f99682a77e871bc27 Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Fri, 24 Jul 2026 15:46:58 +0200 Subject: [PATCH 3/6] IONOS: fix(viewer): exclude directories from the file list Don't include directories as they can not be displayed. Note: including directories could also cause a follow-up error with certain directory structures which happen to include a directory named like a number (i.e. 123) because of sloppy, too broad type casting in fileUtils.ts's genFileInfo() accidentally converting such a folder name to a Number, which then can not be used in string comparisons. Filter the folder file list down to entries that actually carry a mime type, which excludes directories. Signed-off-by: Thomas Lehmann Signed-off-by: Franziska Bath Co-authored-by: Franziska Bath --- src/views/Viewer.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 490501067..52208f808 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -785,8 +785,14 @@ export default defineComponent({ const fileList = await folderRequest(dirPath) - // filter out the unwanted mimes - const filteredFiles = fileList.filter(file => file.mime && mimes.indexOf(file.mime) !== -1) + let filteredFiles + if (configModule.alwaysShowViewer) { + // only include files with mime to exclude directories, otherwise accept all mimes + filteredFiles = fileList.filter(file => file?.mime) + } else { + // filter out the unwanted mimes + filteredFiles = fileList.filter(file => file.mime && mimes.indexOf(file.mime) !== -1) + } // sort like the files list // TODO: implement global sorting API From 28ae444bd01096c42cd280f4004a613043dd67e4 Mon Sep 17 00:00:00 2001 From: Franziska Bath Date: Fri, 24 Jul 2026 15:47:09 +0200 Subject: [PATCH 4/6] IONOS: feat(viewer): exclude Collabora office documents and PDFs from preview When the always-show-viewer fallback is active, exclude office documents and PDFs from the folder file list so Collabora-handled files are not shown by the viewer. Signed-off-by: Franziska Bath --- src/views/Viewer.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 52208f808..62e6924a1 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -787,8 +787,16 @@ export default defineComponent({ let filteredFiles if (configModule.alwaysShowViewer) { - // only include files with mime to exclude directories, otherwise accept all mimes - filteredFiles = fileList.filter(file => file?.mime) + // only include files with mime to exclude directories + // and office documents/pdfs to exclude collabora files + // otherwise accept all mimes + filteredFiles = fileList.filter(file => { + const mime = file?.mime + const isOfficeDocument = mime && OC.MimeTypeList.aliases[mime]?.startsWith('x-office') + const isPdf = mime && mime === 'application/pdf' + + return mime && !isOfficeDocument && !isPdf + }) } else { // filter out the unwanted mimes filteredFiles = fileList.filter(file => file.mime && mimes.indexOf(file.mime) !== -1) From 144f296656aba9a238a72f0572c1fd06a04d486f Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Fri, 24 Jul 2026 15:47:25 +0200 Subject: [PATCH 5/6] IONOS: fix(images): render default fallback and stop spinner on failed image load == The cause Previously the code attempted to load a preview of an image. If loading this preview image failed it was attempted to load the original image. Load errors of images were only handled _once_. This meant that a load error for the original image was never handled, thus the viewer was still in loading state and showed a browser-dependant "broken image" replacement icon. == The fix Now further image load errors are handled too. In case the original fails too, the loading state is ended and a placeholder text is shown. The default preview component, which was introduced to show something for any mimetype if configured, is now also used as a fallback. Signed-off-by: Thomas Lehmann --- src/components/Images.vue | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/Images.vue b/src/components/Images.vue index dd07e1441..ecd7534ec 100644 --- a/src/components/Images.vue +++ b/src/components/Images.vue @@ -12,7 +12,8 @@ @close="onClose" />