123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- <!--
- - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
-
- <template>
- <NcAppSidebar v-if="file"
- ref="sidebar"
- v-bind="appSidebar"
- :force-menu="true"
- tabindex="0"
- @close="close"
- @update:active="setActiveTab"
- @update:starred="toggleStarred"
- @[defaultActionListener].stop.prevent="onDefaultAction"
- @opening="handleOpening"
- @opened="handleOpened"
- @closing="handleClosing"
- @closed="handleClosed">
- <!-- TODO: create a standard to allow multiple elements here? -->
- <template v-if="fileInfo" #description>
- <LegacyView v-for="view in views"
- :key="view.cid"
- :component="view"
- :file-info="fileInfo" />
- </template>
-
- <!-- Actions menu -->
- <template v-if="fileInfo" #secondary-actions>
- <!-- TODO: create proper api for apps to register actions
- And inject themselves here. -->
- <NcActionButton v-if="isSystemTagsEnabled"
- :close-after-click="true"
- icon="icon-tag"
- @click="toggleTags">
- {{ t('files', 'Tags') }}
- </NcActionButton>
- </template>
-
- <!-- Error display -->
- <NcEmptyContent v-if="error" icon="icon-error">
- {{ error }}
- </NcEmptyContent>
-
- <!-- If fileInfo fetch is complete, render tabs -->
- <template v-for="tab in tabs" v-else-if="fileInfo">
- <!-- Hide them if we're loading another file but keep them mounted -->
- <SidebarTab v-if="tab.enabled(fileInfo)"
- v-show="!loading"
- :id="tab.id"
- :key="tab.id"
- :name="tab.name"
- :icon="tab.icon"
- :on-mount="tab.mount"
- :on-update="tab.update"
- :on-destroy="tab.destroy"
- :on-scroll-bottom-reached="tab.scrollBottomReached"
- :file-info="fileInfo">
- <template v-if="tab.iconSvg !== undefined" #icon>
- <!-- eslint-disable-next-line vue/no-v-html -->
- <span class="svg-icon" v-html="tab.iconSvg" />
- </template>
- </SidebarTab>
- </template>
- </NcAppSidebar>
- </template>
- <script>
- import { encodePath } from '@nextcloud/paths'
- import $ from 'jquery'
- import axios from '@nextcloud/axios'
- import { emit } from '@nextcloud/event-bus'
- import moment from '@nextcloud/moment'
- import { Type as ShareTypes } from '@nextcloud/sharing'
-
- import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
- import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
- import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-
- import FileInfo from '../services/FileInfo.js'
- import SidebarTab from '../components/SidebarTab.vue'
- import LegacyView from '../components/LegacyView.vue'
-
- export default {
- name: 'Sidebar',
-
- components: {
- NcActionButton,
- NcAppSidebar,
- NcEmptyContent,
- LegacyView,
- SidebarTab,
- },
-
- data() {
- return {
- // reactive state
- Sidebar: OCA.Files.Sidebar.state,
- error: null,
- loading: true,
- fileInfo: null,
- starLoading: false,
- isFullScreen: false,
- hasLowHeight: false,
- }
- },
-
- computed: {
- /**
- * Current filename
- * This is bound to the Sidebar service and
- * is used to load a new file
- *
- * @return {string}
- */
- file() {
- return this.Sidebar.file
- },
-
- /**
- * List of all the registered tabs
- *
- * @return {Array}
- */
- tabs() {
- return this.Sidebar.tabs
- },
-
- /**
- * List of all the registered views
- *
- * @return {Array}
- */
- views() {
- return this.Sidebar.views
- },
-
- /**
- * Current user dav root path
- *
- * @return {string}
- */
- davPath() {
- const user = OC.getCurrentUser().uid
- return OC.linkToRemote(`dav/files/${user}${encodePath(this.file)}`)
- },
-
- /**
- * Current active tab handler
- *
- * @param {string} id the tab id to set as active
- * @return {string} the current active tab
- */
- activeTab() {
- return this.Sidebar.activeTab
- },
-
- /**
- * Sidebar subtitle
- *
- * @return {string}
- */
- subtitle() {
- return `${this.size}, ${this.time}`
- },
-
- /**
- * File last modified formatted string
- *
- * @return {string}
- */
- time() {
- return OC.Util.relativeModifiedDate(this.fileInfo.mtime)
- },
-
- /**
- * File last modified full string
- *
- * @return {string}
- */
- fullTime() {
- return moment(this.fileInfo.mtime).format('LLL')
- },
-
- /**
- * File size formatted string
- *
- * @return {string}
- */
- size() {
- return OC.Util.humanFileSize(this.fileInfo.size)
- },
-
- /**
- * File background/figure to illustrate the sidebar header
- *
- * @return {string}
- */
- background() {
- return this.getPreviewIfAny(this.fileInfo)
- },
-
- /**
- * App sidebar v-binding object
- *
- * @return {object}
- */
- appSidebar() {
- if (this.fileInfo) {
- return {
- 'data-mimetype': this.fileInfo.mimetype,
- 'star-loading': this.starLoading,
- active: this.activeTab,
- background: this.background,
- class: {
- 'app-sidebar--has-preview': this.fileInfo.hasPreview && !this.isFullScreen,
- 'app-sidebar--full': this.isFullScreen,
- },
- compact: this.hasLowHeight || !this.fileInfo.hasPreview || this.isFullScreen,
- loading: this.loading,
- starred: this.fileInfo.isFavourited,
- subtitle: this.subtitle,
- subtitleTooltip: this.fullTime,
- title: this.fileInfo.name,
- titleTooltip: this.fileInfo.name,
- }
- } else if (this.error) {
- return {
- key: 'error', // force key to re-render
- subtitle: '',
- title: '',
- }
- }
- // no fileInfo yet, showing empty data
- return {
- loading: this.loading,
- subtitle: '',
- title: '',
- }
- },
-
- /**
- * Default action object for the current file
- *
- * @return {object}
- */
- defaultAction() {
- return this.fileInfo
- && OCA.Files && OCA.Files.App && OCA.Files.App.fileList
- && OCA.Files.App.fileList.fileActions
- && OCA.Files.App.fileList.fileActions.getDefaultFileAction
- && OCA.Files.App.fileList
- .fileActions.getDefaultFileAction(this.fileInfo.mimetype, this.fileInfo.type, OC.PERMISSION_READ)
-
- },
-
- /**
- * Dynamic header click listener to ensure
- * nothing is listening for a click if there
- * is no default action
- *
- * @return {string|null}
- */
- defaultActionListener() {
- return this.defaultAction ? 'figure-click' : null
- },
-
- isSystemTagsEnabled() {
- return OCA && 'SystemTags' in OCA
- },
- },
- created() {
- window.addEventListener('resize', this.handleWindowResize)
- this.handleWindowResize()
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.handleWindowResize)
- },
-
- methods: {
- /**
- * Can this tab be displayed ?
- *
- * @param {object} tab a registered tab
- * @return {boolean}
- */
- canDisplay(tab) {
- return tab.enabled(this.fileInfo)
- },
- resetData() {
- this.error = null
- this.fileInfo = null
- this.$nextTick(() => {
- if (this.$refs.tabs) {
- this.$refs.tabs.updateTabs()
- }
- })
- },
-
- getPreviewIfAny(fileInfo) {
- if (fileInfo.hasPreview && !this.isFullScreen) {
- return OC.generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true`)
- }
- return this.getIconUrl(fileInfo)
- },
-
- /**
- * Copied from https://github.com/nextcloud/server/blob/16e0887ec63591113ee3f476e0c5129e20180cde/apps/files/js/filelist.js#L1377
- * TODO: We also need this as a standalone library
- *
- * @param {object} fileInfo the fileinfo
- * @return {string} Url to the icon for mimeType
- */
- getIconUrl(fileInfo) {
- const mimeType = fileInfo.mimetype || 'application/octet-stream'
- if (mimeType === 'httpd/unix-directory') {
- // use default folder icon
- if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') {
- return OC.MimeType.getIconUrl('dir-shared')
- } else if (fileInfo.mountType === 'external-root') {
- return OC.MimeType.getIconUrl('dir-external')
- } else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
- return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType)
- } else if (fileInfo.shareTypes && (
- fileInfo.shareTypes.indexOf(ShareTypes.SHARE_TYPE_LINK) > -1
- || fileInfo.shareTypes.indexOf(ShareTypes.SHARE_TYPE_EMAIL) > -1)
- ) {
- return OC.MimeType.getIconUrl('dir-public')
- } else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) {
- return OC.MimeType.getIconUrl('dir-shared')
- }
- return OC.MimeType.getIconUrl('dir')
- }
- return OC.MimeType.getIconUrl(mimeType)
- },
-
- /**
- * Set current active tab
- *
- * @param {string} id tab unique id
- */
- setActiveTab(id) {
- OCA.Files.Sidebar.setActiveTab(id)
- },
-
- /**
- * Toggle favourite state
- * TODO: better implementation
- *
- * @param {boolean} state favourited or not
- */
- async toggleStarred(state) {
- try {
- this.starLoading = true
- await axios({
- method: 'PROPPATCH',
- url: this.davPath,
- data: `<?xml version="1.0"?>
- <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
- ${state ? '<d:set>' : '<d:remove>'}
- <d:prop>
- <oc:favorite>1</oc:favorite>
- </d:prop>
- ${state ? '</d:set>' : '</d:remove>'}
- </d:propertyupdate>`,
- })
-
- // TODO: Obliterate as soon as possible and use events with new files app
- // Terrible fallback for legacy files: toggle filelist as well
- if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) {
- OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList)
- }
-
- } catch (error) {
- OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file'))
- console.error('Unable to change favourite state', error)
- }
- this.starLoading = false
- },
-
- onDefaultAction() {
- if (this.defaultAction) {
- // generate fake context
- this.defaultAction.action(this.fileInfo.name, {
- fileInfo: this.fileInfo,
- dir: this.fileInfo.dir,
- fileList: OCA.Files.App.fileList,
- $file: $('body'),
- })
- }
- },
-
- /**
- * Toggle the tags selector
- */
- toggleTags() {
- if (OCA.SystemTags && OCA.SystemTags.View) {
- OCA.SystemTags.View.toggle()
- }
- },
-
- /**
- * Open the sidebar for the given file
- *
- * @param {string} path the file path to load
- * @return {Promise}
- * @throws {Error} loading failure
- */
- async open(path) {
- // update current opened file
- this.Sidebar.file = path
-
- if (path && path.trim() !== '') {
- // reset data, keep old fileInfo to not reload all tabs and just hide them
- this.error = null
- this.loading = true
-
- try {
- this.fileInfo = await FileInfo(this.davPath)
- // adding this as fallback because other apps expect it
- this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
-
- // DEPRECATED legacy views
- // TODO: remove
- this.views.forEach(view => {
- view.setFileInfo(this.fileInfo)
- })
-
- this.$nextTick(() => {
- if (this.$refs.tabs) {
- this.$refs.tabs.updateTabs()
- }
- })
- } catch (error) {
- this.error = t('files', 'Error while loading the file data')
- console.error('Error while loading the file data', error)
-
- throw new Error(error)
- } finally {
- this.loading = false
- }
- }
- },
-
- /**
- * Close the sidebar
- */
- close() {
- this.Sidebar.file = ''
- this.resetData()
- },
-
- /**
- * Allow to set the Sidebar as fullscreen from OCA.Files.Sidebar
- *
- * @param {boolean} isFullScreen - Whether or not to render the Sidebar in fullscreen.
- */
- setFullScreenMode(isFullScreen) {
- this.isFullScreen = isFullScreen
- if (isFullScreen) {
- document.querySelector('#content')?.classList.add('with-sidebar--full')
- || document.querySelector('#content-vue')?.classList.add('with-sidebar--full')
- } else {
- document.querySelector('#content')?.classList.remove('with-sidebar--full')
- || document.querySelector('#content-vue')?.classList.remove('with-sidebar--full')
- }
- },
-
- /**
- * Emit SideBar events.
- */
- handleOpening() {
- emit('files:sidebar:opening')
- },
- handleOpened() {
- emit('files:sidebar:opened')
- },
- handleClosing() {
- emit('files:sidebar:closing')
- },
- handleClosed() {
- emit('files:sidebar:closed')
- },
- handleWindowResize() {
- this.hasLowHeight = document.documentElement.clientHeight < 1024
- },
- },
- }
- </script>
- <style lang="scss" scoped>
- .app-sidebar {
- &--has-preview::v-deep {
- .app-sidebar-header__figure {
- background-size: cover;
- }
-
- &[data-mimetype="text/plain"],
- &[data-mimetype="text/markdown"] {
- .app-sidebar-header__figure {
- background-size: contain;
- }
- }
- }
-
- &--full {
- position: fixed !important;
- z-index: 2025 !important;
- top: 0 !important;
- height: 100% !important;
- }
-
- .svg-icon {
- ::v-deep svg {
- width: 20px;
- height: 20px;
- fill: currentColor;
- }
- }
- }
- </style>
|