Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/Listener/LoadViewerScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
}
69 changes: 69 additions & 0 deletions src/components/Default.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!--
- SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- SPDX-FileCopyrightText: 2024 STRATO AG
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="default_container">
<img :src="mimeIcon" alt="mimetype-icon">
<span class="title">
{{ basename }}
</span>
</div>
</template>

<script>
export default {
name: 'Default',

props: {
basename: {
type: String,
default: '',
},
mime: {
type: String,
default: '',
},
},

computed: {
mimeIcon() {
return OC.MimeType.getIconUrl(this.mime)
},
},

mounted() {
if (typeof this.doneLoading === 'function') {
this.doneLoading()
}
},
Comment thread
Copilot marked this conversation as resolved.
}
</script>

<style scoped lang="scss">
.default_container {
display: flex;
align-items: center;
height: 100%;
justify-content: center;
flex-direction: column;
}

img {
align-self: center;
justify-self: center;
margin-bottom: 16px;
min-width: 100%;
height: 10em;
}

.title {
color: var(--color-primary-text);
font-weight: bold;
font-size: 1.4em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
16 changes: 14 additions & 2 deletions src/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
@close="onClose" />

<template v-else-if="data !== null">
<img v-if="!livePhotoCanBePlayed"
<Default v-if="originalFailed" :basename="basename" :mime="mime" />
<img v-else-if="!livePhotoCanBePlayed"
ref="image"
:alt="alt"
:class="{
Expand All @@ -22,7 +23,7 @@
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@error.capture.prevent.stop="onFail"
@load="updateImgSize"
@wheel.stop.prevent="updateZoom"
@dblclick.prevent="onDblclick"
Expand Down Expand Up @@ -83,6 +84,7 @@ import { basename } from '@nextcloud/paths'
import { translate } from '@nextcloud/l10n'
import { NcLoadingIcon } from '@nextcloud/vue'

import Default from './Default.vue'
import ImageEditor from './ImageEditor.vue'
import { findLivePhotoPeerFromFileId } from '../utils/livePhotoUtils'
import { getDavPath } from '../utils/fileUtils'
Expand All @@ -94,6 +96,7 @@ export default {
name: 'Images',

components: {
Default,
ImageEditor,
PlayCircleOutline,
NcLoadingIcon,
Expand All @@ -112,6 +115,7 @@ export default {
shiftY: 0,
zoomRatio: 1,
fallback: false,
originalFailed: false,
livePhotoCanBePlayed: false,
zooming: false,
pinchDistance: 0,
Expand Down Expand Up @@ -422,9 +426,17 @@ export default {

// Fallback to the original image if not already done
onFail() {
if (this.originalFailed) {
// Loading the original image was already attempted, don't bother handling more errors
return
}
if (!this.fallback) {
console.error(`Loading of file preview ${basename(this.src)} failed, falling back to original file`)
this.fallback = true
} else {
this.originalFailed = true
console.error(`Loading of the original image ${basename(this.source)} failed too`)
this.doneLoading()
}
},
doneLoadingLivePhoto() {
Expand Down
7 changes: 7 additions & 0 deletions src/files_actions/viewerAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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),
Expand Down
12 changes: 12 additions & 0 deletions src/models/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* SPDX-FileCopyrightText: 2024 STRATO AG
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
Comment thread
Copilot marked this conversation as resolved.
import { loadState } from '@nextcloud/initial-state'

const alwaysShowViewer = loadState<boolean>('viewer', 'always_show_viewer', false)

export default {
alwaysShowViewer,
defaultMimeType: 'all',
}
17 changes: 17 additions & 0 deletions src/models/default.ts
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 2 additions & 0 deletions src/services/Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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')
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/livePhotoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ export function findLivePhotoPeerFromName(referenceFile: BasicFileInfo, fileList
return fileList.find(comparedFile => {
// if same filename and extension is allowed
return comparedFile.filename !== referenceFile.filename
&& (comparedFile.basename.startsWith(referenceFile.name) && livePictureExtRegex.test(comparedFile.basename))
&& (comparedFile.basename.toString().startsWith(referenceFile.name) && livePictureExtRegex.test(comparedFile.basename))
})
}
48 changes: 43 additions & 5 deletions src/views/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -390,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
},
Expand Down Expand Up @@ -728,6 +737,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 })
Expand All @@ -745,8 +759,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
Expand All @@ -769,8 +785,22 @@ 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
// 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
})
Comment on lines +793 to +799
} 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
Expand Down Expand Up @@ -814,7 +844,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()
},
Expand Down Expand Up @@ -1328,6 +1358,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%;
Expand Down
Loading