From efa4120b6a39bc76e44ef4b08fc92af881277f9c Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 27 Aug 2024 12:48:43 +0200 Subject: [PATCH 01/22] IONOS: feat: add always_show_viewer app config value enable via: ./occ config:app:set --value yes --type string viewer always_show_viewer Signed-off-by: Misha M.-Kupriyanov --- lib/Listener/LoadViewerScript.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/Listener/LoadViewerScript.php b/lib/Listener/LoadViewerScript.php index 6d125b59b..f62250e62 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; @@ -23,13 +24,16 @@ class LoadViewerScript implements IEventListener { private IInitialState $initialStateService; private IPreview $previewManager; + private IAppConfig $appConfig; public function __construct( IInitialState $initialStateService, IPreview $previewManager, + IAppConfig $appConfig, ) { $this->initialStateService = $initialStateService; $this->previewManager = $previewManager; + $this->appConfig = $appConfig; } public function handle(Event $event): void { @@ -37,10 +41,13 @@ public function handle(Event $event): void { return; } + $alwaysShowViewer = $this->appConfig->getAppValue('always_show_viewer', 'no') === 'yes'; + Util::addStyle(Application::APP_ID, 'viewer-init'); 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); } } From 41171a5ab14becb520ecf1bcf56a5a223b76ee4b Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 18 Jun 2025 17:35:45 +0200 Subject: [PATCH 02/22] fixup! IONOS: feat: add always_show_viewer app config value Signed-off-by: Misha M.-Kupriyanov --- lib/Listener/LoadViewerScript.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Listener/LoadViewerScript.php b/lib/Listener/LoadViewerScript.php index f62250e62..e946fbafa 100644 --- a/lib/Listener/LoadViewerScript.php +++ b/lib/Listener/LoadViewerScript.php @@ -48,6 +48,6 @@ public function handle(Event $event): void { 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); + $this->initialStateService->provideInitialState('always_show_viewer', $alwaysShowViewer); } } From 6c1b75d733b78c442307cddb27057bc724af38ec Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 27 Aug 2024 17:05:56 +0200 Subject: [PATCH 03/22] IONOS: feat: add config module to expose alwaysShowViewer Signed-off-by: Misha M.-Kupriyanov --- src/models/config.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/models/config.ts diff --git a/src/models/config.ts b/src/models/config.ts new file mode 100644 index 000000000..cbe1653fd --- /dev/null +++ b/src/models/config.ts @@ -0,0 +1,12 @@ +/** + * SPDX-FileLicenseText: 2024 STRATO AG + * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileContributor: Mikhailo Matiyenko-Kupriyanov + */ +import { loadState } from '@nextcloud/initial-state' + +const alwaysShowViewer = loadState('viewer', 'always_show_viewer', false) + +export default { + alwaysShowViewer, +} From 6fa056b54df9006bc56046bd277a2fefe72a477b Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 27 Aug 2024 17:16:53 +0200 Subject: [PATCH 04/22] IONOS: feat: add default component stub in order later to use it as default viewer Signed-off-by: Misha M.-Kupriyanov --- src/components/Default.vue | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/components/Default.vue diff --git a/src/components/Default.vue b/src/components/Default.vue new file mode 100644 index 000000000..e9a47575b --- /dev/null +++ b/src/components/Default.vue @@ -0,0 +1,22 @@ + + + + + + From 854a60b402ba44e585accd280e9f720f86e6bc9d Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 28 Aug 2024 12:50:49 +0200 Subject: [PATCH 05/22] IONOS: feat: add default viewer in order later to display it for all not known mime types Signed-off-by: Misha M.-Kupriyanov --- src/models/default.ts | 16 ++++++++++++++++ src/services/Viewer.js | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 src/models/default.ts diff --git a/src/models/default.ts b/src/models/default.ts new file mode 100644 index 000000000..9de86a667 --- /dev/null +++ b/src/models/default.ts @@ -0,0 +1,16 @@ +/** + * SPDX-FileCopyrightText: 2024 STRATO AG + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Default from '../components/Default.vue' + +export default { + id: 'default', + group: 'other', + mimes: [ + '*/*', + ], + mimesAliases: {}, + component: Default, +} diff --git a/src/services/Viewer.js b/src/services/Viewer.js index 9e91eb6dc..db1889439 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' /** @@ -61,6 +62,7 @@ export default class Viewer { this.registerHandler(Images) this.registerHandler(Videos) this.registerHandler(Audios) + this.registerHandler(Default) logger.debug('OCA.Viewer initialized') } From 69bf7105ca5fc848b4d3488d9c356cc14feb09ec Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 28 Aug 2024 12:54:29 +0200 Subject: [PATCH 06/22] IONOS: feat: enable default viewer Signed-off-by: Misha M.-Kupriyanov --- src/views/Viewer.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index cb7279caf..334854240 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -190,6 +190,7 @@ import { canDownload } from '../utils/canDownload.ts' import { extractFilePaths, sortCompare } from '../utils/fileUtils.ts' import getSortingConfig from '../services/FileSortingConfig.ts' import cancelableRequest from '../utils/CancelableRequest.js' +import configModule from '../models/config.ts' import Error from '../components/Error.vue' import File from '../models/file.js' import getFileInfo from '../services/FileInfo.ts' @@ -680,6 +681,11 @@ export default defineComponent({ handler = this.registeredHandlers[mime] ?? this.registeredHandlers[alias] } + // fallback to default viewer if enabled + if (!handler && configModule.alwaysShowViewer) { + handler = this.registeredHandlers['*/*'] + } + // if we don't have a handler for this mime, abort if (!handler) { logger.error('The following file could not be displayed', { fileInfo }) From 779f7e08b4f1b296d61c20da197f95045a1f2fcf Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Thu, 19 Jun 2025 16:29:20 +0200 Subject: [PATCH 07/22] fixup! IONOS: feat: enable default viewer Signed-off-by: Misha M.-Kupriyanov --- src/files_actions/viewerAction.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/files_actions/viewerAction.ts b/src/files_actions/viewerAction.ts index 15f7e4451..b5b7e5d8a 100644 --- a/src/files_actions/viewerAction.ts +++ b/src/files_actions/viewerAction.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { Node, View } from '@nextcloud/files' - +import configModule from '../models/config' import { DefaultType, FileAction, Permission, registerFileAction } from '@nextcloud/files' import { t } from '@nextcloud/l10n' import svgEye from '@mdi/svg/svg/eye.svg?raw' @@ -75,6 +75,11 @@ export function registerViewerAction() { return false } + // Always enabled if configured so + if (configModule.alwaysShowViewer) { + return true + } + return nodes.every((node) => Boolean(node.permissions & Permission.READ) && window.OCA.Viewer.mimetypes.includes(node.mime), From ccbf4f92ad75bc1f89b9a4de7ed1afe5cfc9a5b7 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 3 Sep 2024 14:28:53 +0200 Subject: [PATCH 08/22] IONOS: feat: extract default mime type to config in order to reuse it later Signed-off-by: Misha M.-Kupriyanov --- src/models/config.ts | 1 + src/views/Viewer.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/config.ts b/src/models/config.ts index cbe1653fd..989d1f719 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -9,4 +9,5 @@ const alwaysShowViewer = loadState('viewer', 'always_show_viewer', fals export default { alwaysShowViewer, + defaultMimeType: '*/*', } diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 334854240..9e23ef08c 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -683,7 +683,7 @@ export default defineComponent({ // fallback to default viewer if enabled if (!handler && configModule.alwaysShowViewer) { - handler = this.registeredHandlers['*/*'] + handler = this.registeredHandlers[configModule.defaultMimeType] } // if we don't have a handler for this mime, abort From cb962de7ca70ab7770b585d9ddb4de5bddbeffe9 Mon Sep 17 00:00:00 2001 From: Franziska Bath Date: Mon, 2 Sep 2024 13:37:47 +0200 Subject: [PATCH 09/22] IONOS: feat: properly implement default component Signed-off-by: Franziska Bath --- src/components/Default.vue | 49 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/Default.vue b/src/components/Default.vue index e9a47575b..3fcb7b698 100644 --- a/src/components/Default.vue +++ b/src/components/Default.vue @@ -5,18 +5,63 @@ --> From c9923846e3321c08106d9c42d9603e28d9fafdf0 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 3 Sep 2024 13:26:55 +0200 Subject: [PATCH 10/22] IONOS: feat(Viewer): extract modal title as own method in order to be able to influence it later Signed-off-by: Misha M.-Kupriyanov --- src/views/Viewer.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 9e23ef08c..5451285cd 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" @@ -396,6 +396,10 @@ export default defineComponent({ } }, + modalTitle() { + return this.currentFile.basename + }, + showComparison() { return !this.isMobile }, From 9bf0bf3f4450c6eb9ab62d6d5930c574bb3583c2 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 2 Sep 2024 13:41:26 +0200 Subject: [PATCH 11/22] IONOS: feat(Viewer): ensure previous and next navigation works with default component Signed-off-by: Franziska Bath Signed-off-by: Misha M.-Kupriyanov --- src/views/Viewer.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 5451285cd..472aeac2a 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -707,8 +707,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 @@ -731,8 +733,8 @@ 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) + // filter out the unwanted mimes if configModule.alwaysShowViewer not enabled + const filteredFiles = configModule.alwaysShowViewer ? fileList : fileList.filter(file => file.mime && mimes.indexOf(file.mime) !== -1) // sort like the files list // TODO: implement global sorting API @@ -759,7 +761,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 9ca1c15953ea85b3cbb8ebb5e9a161e09d4e4755 Mon Sep 17 00:00:00 2001 From: Franziska Bath Date: Mon, 2 Sep 2024 13:40:22 +0200 Subject: [PATCH 12/22] IONOS: feat(Viewer): hide file name in modal header when mime image shown Signed-off-by: Franziska Bath --- src/views/Viewer.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 472aeac2a..89430e840 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -397,7 +397,11 @@ export default defineComponent({ }, modalTitle() { - return this.currentFile.basename + if (!configModule.alwaysShowViewer) { + return this.currentFile.basename + } + + return this.currentFile?.modal?.name === 'Default' ? '' : this.currentFile.basename }, showComparison() { From 643fec2ec19ec19713c58147c39e9a1daf5c9f88 Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Wed, 11 Sep 2024 17:50:37 +0200 Subject: [PATCH 13/22] IONOS: fix(Viewer): remove directories from fileList 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. Signed-off-by: Thomas Lehmann --- 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 89430e840..6516ddbf4 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -737,8 +737,14 @@ export default defineComponent({ const fileList = await folderRequest(dirPath) - // filter out the unwanted mimes if configModule.alwaysShowViewer not enabled - const filteredFiles = configModule.alwaysShowViewer ? fileList : fileList.filter(file => file.mime && mimes.indexOf(file.mime) !== -1) + let filteredFiles + if (configModule.alwaysShowViewer) { + // don't include directories, otherwise accept all mimes + filteredFiles = fileList.filter(({ type }) => type !== 'directory') + } 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 577e6e4d5197f576795d7f26b0ce42ecce9c733a Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 12 Sep 2024 12:19:32 +0200 Subject: [PATCH 14/22] IONOS: fix(Viewer): remove contextmenu view action for folders Signed-off-by: Kai Henseler Signed-off-by: Misha M.-Kupriyanov # Conflicts: # src/views/Viewer.vue --- src/files_actions/viewerAction.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/files_actions/viewerAction.ts b/src/files_actions/viewerAction.ts index b5b7e5d8a..ae47e2b98 100644 --- a/src/files_actions/viewerAction.ts +++ b/src/files_actions/viewerAction.ts @@ -77,7 +77,8 @@ export function registerViewerAction() { // Always enabled if configured so if (configModule.alwaysShowViewer) { - return true + // disable for folders + return !nodes.some(node => node.type === 'folder') } return nodes.every((node) => From cb8b7d5f08e9abb8753174a927864de42099df94 Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Thu, 12 Sep 2024 13:58:00 +0200 Subject: [PATCH 15/22] IONOS: refactor: use config module, not magic string Reference the default mimetype from the config module, don't add a magic string. Signed-off-by: Thomas Lehmann --- src/models/default.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/default.ts b/src/models/default.ts index 9de86a667..94ef98f3a 100644 --- a/src/models/default.ts +++ b/src/models/default.ts @@ -4,12 +4,13 @@ */ import Default from '../components/Default.vue' +import config from './config.ts' export default { id: 'default', group: 'other', mimes: [ - '*/*', + config.defaultMimeType, ], mimesAliases: {}, component: Default, From da0b25bfe000824421f80b95fa9e9cfbcdd53134 Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Thu, 12 Sep 2024 13:58:57 +0200 Subject: [PATCH 16/22] IONOS: fix: change default mimetype */* -> "all" == The bug The app config "always_show_viewer" enables the preview for all mimetypes. If this config is not set and no handler is registered for a mimetype, the file will be downloaded. In the share/public view, with this config enabled some file types were downloaded instead of opened in the preview. The code would not progress up to Viewer's openFileInfo() because it would not find a preview component candidate in [1] to even attempt opening the preview. == The fix As per reverse engineering it was found that special string "all" is used as symbol for handling any mimetype (at least in [2]). The decision was made to change the special mimetype for a registered previewer to "all", because then handling any file is already coverered this way. All discovered places: 1. Files/fileactions: getDefaultFileAction() [2] 2. Shares: attach(), "fileActionsReady" event handler, registerAction() call [3] [1]: https://github.com/nextcloud/server/blob/v29.0.6/apps/files/js/filelist.js#L912 [2]: https://github.com/nextcloud/server/blob/v29.0.6/apps/files/js/fileactions.js#L315 [3]: https://github.com/nextcloud/server/blob/v29.0.6/apps/files_sharing/src/share.js#L230 Signed-off-by: Thomas Lehmann --- src/models/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/config.ts b/src/models/config.ts index 989d1f719..ca6096b6b 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -9,5 +9,5 @@ const alwaysShowViewer = loadState('viewer', 'always_show_viewer', fals export default { alwaysShowViewer, - defaultMimeType: '*/*', + defaultMimeType: 'all', } From 56bdc7330251ce5fce66585d428eae1653e7aeeb Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Thu, 12 Sep 2024 15:45:24 +0200 Subject: [PATCH 17/22] IONOS: refactor(Images): rename preview load failed state variable Another state will be added and this prepares for a consistent naming pattern.t Signed-off-by: Thomas Lehmann --- src/components/Images.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Images.vue b/src/components/Images.vue index 7d3b30f39..a239b96b6 100644 --- a/src/components/Images.vue +++ b/src/components/Images.vue @@ -110,7 +110,7 @@ export default { shiftX: 0, shiftY: 0, zoomRatio: 1, - fallback: false, + previewFailed: false, livePhotoCanBePlayed: false, zooming: false, pinchDistance: 0, @@ -191,7 +191,7 @@ export default { } // If loading the preview failed once, let's load the original file - if (this.fallback) { + if (this.previewFailed) { return this.src } @@ -420,9 +420,9 @@ export default { // Fallback to the original image if not already done onFail() { - if (!this.fallback) { + if (!this.previewFailed) { console.error(`Loading of file preview ${basename(this.src)} failed, falling back to original file`) - this.fallback = true + this.previewFailed = true } }, doneLoadingLivePhoto() { From 81932dba80d7b6e56f1ef98a54d2379b70c39330 Mon Sep 17 00:00:00 2001 From: Thomas Lehmann Date: Thu, 12 Sep 2024 15:41:35 +0200 Subject: [PATCH 18/22] IONOS: fix(Images): hide loading 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 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/Images.vue b/src/components/Images.vue index a239b96b6..09c8a4b33 100644 --- a/src/components/Images.vue +++ b/src/components/Images.vue @@ -12,7 +12,8 @@ @close="onClose" />