You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Sidebar.vue 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <!--
  2. - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <NcAppSidebar v-if="file"
  24. ref="sidebar"
  25. v-bind="appSidebar"
  26. :force-menu="true"
  27. tabindex="0"
  28. @close="close"
  29. @update:active="setActiveTab"
  30. @update:starred="toggleStarred"
  31. @[defaultActionListener].stop.prevent="onDefaultAction"
  32. @opening="handleOpening"
  33. @opened="handleOpened"
  34. @closing="handleClosing"
  35. @closed="handleClosed">
  36. <!-- TODO: create a standard to allow multiple elements here? -->
  37. <template v-if="fileInfo" #description>
  38. <LegacyView v-for="view in views"
  39. :key="view.cid"
  40. :component="view"
  41. :file-info="fileInfo" />
  42. </template>
  43. <!-- Actions menu -->
  44. <template v-if="fileInfo" #secondary-actions>
  45. <!-- TODO: create proper api for apps to register actions
  46. And inject themselves here. -->
  47. <NcActionButton v-if="isSystemTagsEnabled"
  48. :close-after-click="true"
  49. icon="icon-tag"
  50. @click="toggleTags">
  51. {{ t('files', 'Tags') }}
  52. </NcActionButton>
  53. </template>
  54. <!-- Error display -->
  55. <NcEmptyContent v-if="error" icon="icon-error">
  56. {{ error }}
  57. </NcEmptyContent>
  58. <!-- If fileInfo fetch is complete, render tabs -->
  59. <template v-for="tab in tabs" v-else-if="fileInfo">
  60. <!-- Hide them if we're loading another file but keep them mounted -->
  61. <SidebarTab v-if="tab.enabled(fileInfo)"
  62. v-show="!loading"
  63. :id="tab.id"
  64. :key="tab.id"
  65. :name="tab.name"
  66. :icon="tab.icon"
  67. :on-mount="tab.mount"
  68. :on-update="tab.update"
  69. :on-destroy="tab.destroy"
  70. :on-scroll-bottom-reached="tab.scrollBottomReached"
  71. :file-info="fileInfo">
  72. <template v-if="tab.iconSvg !== undefined" #icon>
  73. <!-- eslint-disable-next-line vue/no-v-html -->
  74. <span class="svg-icon" v-html="tab.iconSvg" />
  75. </template>
  76. </SidebarTab>
  77. </template>
  78. </NcAppSidebar>
  79. </template>
  80. <script>
  81. import { encodePath } from '@nextcloud/paths'
  82. import $ from 'jquery'
  83. import axios from '@nextcloud/axios'
  84. import { emit } from '@nextcloud/event-bus'
  85. import moment from '@nextcloud/moment'
  86. import { Type as ShareTypes } from '@nextcloud/sharing'
  87. import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
  88. import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
  89. import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
  90. import FileInfo from '../services/FileInfo.js'
  91. import SidebarTab from '../components/SidebarTab.vue'
  92. import LegacyView from '../components/LegacyView.vue'
  93. export default {
  94. name: 'Sidebar',
  95. components: {
  96. NcActionButton,
  97. NcAppSidebar,
  98. NcEmptyContent,
  99. LegacyView,
  100. SidebarTab,
  101. },
  102. data() {
  103. return {
  104. // reactive state
  105. Sidebar: OCA.Files.Sidebar.state,
  106. error: null,
  107. loading: true,
  108. fileInfo: null,
  109. starLoading: false,
  110. isFullScreen: false,
  111. hasLowHeight: false,
  112. }
  113. },
  114. computed: {
  115. /**
  116. * Current filename
  117. * This is bound to the Sidebar service and
  118. * is used to load a new file
  119. *
  120. * @return {string}
  121. */
  122. file() {
  123. return this.Sidebar.file
  124. },
  125. /**
  126. * List of all the registered tabs
  127. *
  128. * @return {Array}
  129. */
  130. tabs() {
  131. return this.Sidebar.tabs
  132. },
  133. /**
  134. * List of all the registered views
  135. *
  136. * @return {Array}
  137. */
  138. views() {
  139. return this.Sidebar.views
  140. },
  141. /**
  142. * Current user dav root path
  143. *
  144. * @return {string}
  145. */
  146. davPath() {
  147. const user = OC.getCurrentUser().uid
  148. return OC.linkToRemote(`dav/files/${user}${encodePath(this.file)}`)
  149. },
  150. /**
  151. * Current active tab handler
  152. *
  153. * @param {string} id the tab id to set as active
  154. * @return {string} the current active tab
  155. */
  156. activeTab() {
  157. return this.Sidebar.activeTab
  158. },
  159. /**
  160. * Sidebar subtitle
  161. *
  162. * @return {string}
  163. */
  164. subtitle() {
  165. return `${this.size}, ${this.time}`
  166. },
  167. /**
  168. * File last modified formatted string
  169. *
  170. * @return {string}
  171. */
  172. time() {
  173. return OC.Util.relativeModifiedDate(this.fileInfo.mtime)
  174. },
  175. /**
  176. * File last modified full string
  177. *
  178. * @return {string}
  179. */
  180. fullTime() {
  181. return moment(this.fileInfo.mtime).format('LLL')
  182. },
  183. /**
  184. * File size formatted string
  185. *
  186. * @return {string}
  187. */
  188. size() {
  189. return OC.Util.humanFileSize(this.fileInfo.size)
  190. },
  191. /**
  192. * File background/figure to illustrate the sidebar header
  193. *
  194. * @return {string}
  195. */
  196. background() {
  197. return this.getPreviewIfAny(this.fileInfo)
  198. },
  199. /**
  200. * App sidebar v-binding object
  201. *
  202. * @return {object}
  203. */
  204. appSidebar() {
  205. if (this.fileInfo) {
  206. return {
  207. 'data-mimetype': this.fileInfo.mimetype,
  208. 'star-loading': this.starLoading,
  209. active: this.activeTab,
  210. background: this.background,
  211. class: {
  212. 'app-sidebar--has-preview': this.fileInfo.hasPreview && !this.isFullScreen,
  213. 'app-sidebar--full': this.isFullScreen,
  214. },
  215. compact: this.hasLowHeight || !this.fileInfo.hasPreview || this.isFullScreen,
  216. loading: this.loading,
  217. starred: this.fileInfo.isFavourited,
  218. subtitle: this.subtitle,
  219. subtitleTooltip: this.fullTime,
  220. title: this.fileInfo.name,
  221. titleTooltip: this.fileInfo.name,
  222. }
  223. } else if (this.error) {
  224. return {
  225. key: 'error', // force key to re-render
  226. subtitle: '',
  227. title: '',
  228. }
  229. }
  230. // no fileInfo yet, showing empty data
  231. return {
  232. loading: this.loading,
  233. subtitle: '',
  234. title: '',
  235. }
  236. },
  237. /**
  238. * Default action object for the current file
  239. *
  240. * @return {object}
  241. */
  242. defaultAction() {
  243. return this.fileInfo
  244. && OCA.Files && OCA.Files.App && OCA.Files.App.fileList
  245. && OCA.Files.App.fileList.fileActions
  246. && OCA.Files.App.fileList.fileActions.getDefaultFileAction
  247. && OCA.Files.App.fileList
  248. .fileActions.getDefaultFileAction(this.fileInfo.mimetype, this.fileInfo.type, OC.PERMISSION_READ)
  249. },
  250. /**
  251. * Dynamic header click listener to ensure
  252. * nothing is listening for a click if there
  253. * is no default action
  254. *
  255. * @return {string|null}
  256. */
  257. defaultActionListener() {
  258. return this.defaultAction ? 'figure-click' : null
  259. },
  260. isSystemTagsEnabled() {
  261. return OCA && 'SystemTags' in OCA
  262. },
  263. },
  264. created() {
  265. window.addEventListener('resize', this.handleWindowResize)
  266. this.handleWindowResize()
  267. },
  268. beforeDestroy() {
  269. window.removeEventListener('resize', this.handleWindowResize)
  270. },
  271. methods: {
  272. /**
  273. * Can this tab be displayed ?
  274. *
  275. * @param {object} tab a registered tab
  276. * @return {boolean}
  277. */
  278. canDisplay(tab) {
  279. return tab.enabled(this.fileInfo)
  280. },
  281. resetData() {
  282. this.error = null
  283. this.fileInfo = null
  284. this.$nextTick(() => {
  285. if (this.$refs.tabs) {
  286. this.$refs.tabs.updateTabs()
  287. }
  288. })
  289. },
  290. getPreviewIfAny(fileInfo) {
  291. if (fileInfo.hasPreview && !this.isFullScreen) {
  292. return OC.generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true`)
  293. }
  294. return this.getIconUrl(fileInfo)
  295. },
  296. /**
  297. * Copied from https://github.com/nextcloud/server/blob/16e0887ec63591113ee3f476e0c5129e20180cde/apps/files/js/filelist.js#L1377
  298. * TODO: We also need this as a standalone library
  299. *
  300. * @param {object} fileInfo the fileinfo
  301. * @return {string} Url to the icon for mimeType
  302. */
  303. getIconUrl(fileInfo) {
  304. const mimeType = fileInfo.mimetype || 'application/octet-stream'
  305. if (mimeType === 'httpd/unix-directory') {
  306. // use default folder icon
  307. if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') {
  308. return OC.MimeType.getIconUrl('dir-shared')
  309. } else if (fileInfo.mountType === 'external-root') {
  310. return OC.MimeType.getIconUrl('dir-external')
  311. } else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
  312. return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType)
  313. } else if (fileInfo.shareTypes && (
  314. fileInfo.shareTypes.indexOf(ShareTypes.SHARE_TYPE_LINK) > -1
  315. || fileInfo.shareTypes.indexOf(ShareTypes.SHARE_TYPE_EMAIL) > -1)
  316. ) {
  317. return OC.MimeType.getIconUrl('dir-public')
  318. } else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) {
  319. return OC.MimeType.getIconUrl('dir-shared')
  320. }
  321. return OC.MimeType.getIconUrl('dir')
  322. }
  323. return OC.MimeType.getIconUrl(mimeType)
  324. },
  325. /**
  326. * Set current active tab
  327. *
  328. * @param {string} id tab unique id
  329. */
  330. setActiveTab(id) {
  331. OCA.Files.Sidebar.setActiveTab(id)
  332. },
  333. /**
  334. * Toggle favourite state
  335. * TODO: better implementation
  336. *
  337. * @param {boolean} state favourited or not
  338. */
  339. async toggleStarred(state) {
  340. try {
  341. this.starLoading = true
  342. await axios({
  343. method: 'PROPPATCH',
  344. url: this.davPath,
  345. data: `<?xml version="1.0"?>
  346. <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  347. ${state ? '<d:set>' : '<d:remove>'}
  348. <d:prop>
  349. <oc:favorite>1</oc:favorite>
  350. </d:prop>
  351. ${state ? '</d:set>' : '</d:remove>'}
  352. </d:propertyupdate>`,
  353. })
  354. // TODO: Obliterate as soon as possible and use events with new files app
  355. // Terrible fallback for legacy files: toggle filelist as well
  356. if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) {
  357. OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList)
  358. }
  359. } catch (error) {
  360. OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file'))
  361. console.error('Unable to change favourite state', error)
  362. }
  363. this.starLoading = false
  364. },
  365. onDefaultAction() {
  366. if (this.defaultAction) {
  367. // generate fake context
  368. this.defaultAction.action(this.fileInfo.name, {
  369. fileInfo: this.fileInfo,
  370. dir: this.fileInfo.dir,
  371. fileList: OCA.Files.App.fileList,
  372. $file: $('body'),
  373. })
  374. }
  375. },
  376. /**
  377. * Toggle the tags selector
  378. */
  379. toggleTags() {
  380. if (OCA.SystemTags && OCA.SystemTags.View) {
  381. OCA.SystemTags.View.toggle()
  382. }
  383. },
  384. /**
  385. * Open the sidebar for the given file
  386. *
  387. * @param {string} path the file path to load
  388. * @return {Promise}
  389. * @throws {Error} loading failure
  390. */
  391. async open(path) {
  392. // update current opened file
  393. this.Sidebar.file = path
  394. if (path && path.trim() !== '') {
  395. // reset data, keep old fileInfo to not reload all tabs and just hide them
  396. this.error = null
  397. this.loading = true
  398. try {
  399. this.fileInfo = await FileInfo(this.davPath)
  400. // adding this as fallback because other apps expect it
  401. this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
  402. // DEPRECATED legacy views
  403. // TODO: remove
  404. this.views.forEach(view => {
  405. view.setFileInfo(this.fileInfo)
  406. })
  407. this.$nextTick(() => {
  408. if (this.$refs.tabs) {
  409. this.$refs.tabs.updateTabs()
  410. }
  411. })
  412. } catch (error) {
  413. this.error = t('files', 'Error while loading the file data')
  414. console.error('Error while loading the file data', error)
  415. throw new Error(error)
  416. } finally {
  417. this.loading = false
  418. }
  419. }
  420. },
  421. /**
  422. * Close the sidebar
  423. */
  424. close() {
  425. this.Sidebar.file = ''
  426. this.resetData()
  427. },
  428. /**
  429. * Allow to set the Sidebar as fullscreen from OCA.Files.Sidebar
  430. *
  431. * @param {boolean} isFullScreen - Whether or not to render the Sidebar in fullscreen.
  432. */
  433. setFullScreenMode(isFullScreen) {
  434. this.isFullScreen = isFullScreen
  435. if (isFullScreen) {
  436. document.querySelector('#content')?.classList.add('with-sidebar--full')
  437. || document.querySelector('#content-vue')?.classList.add('with-sidebar--full')
  438. } else {
  439. document.querySelector('#content')?.classList.remove('with-sidebar--full')
  440. || document.querySelector('#content-vue')?.classList.remove('with-sidebar--full')
  441. }
  442. },
  443. /**
  444. * Emit SideBar events.
  445. */
  446. handleOpening() {
  447. emit('files:sidebar:opening')
  448. },
  449. handleOpened() {
  450. emit('files:sidebar:opened')
  451. },
  452. handleClosing() {
  453. emit('files:sidebar:closing')
  454. },
  455. handleClosed() {
  456. emit('files:sidebar:closed')
  457. },
  458. handleWindowResize() {
  459. this.hasLowHeight = document.documentElement.clientHeight < 1024
  460. },
  461. },
  462. }
  463. </script>
  464. <style lang="scss" scoped>
  465. .app-sidebar {
  466. &--has-preview::v-deep {
  467. .app-sidebar-header__figure {
  468. background-size: cover;
  469. }
  470. &[data-mimetype="text/plain"],
  471. &[data-mimetype="text/markdown"] {
  472. .app-sidebar-header__figure {
  473. background-size: contain;
  474. }
  475. }
  476. }
  477. &--full {
  478. position: fixed !important;
  479. z-index: 2025 !important;
  480. top: 0 !important;
  481. height: 100% !important;
  482. }
  483. .svg-icon {
  484. ::v-deep svg {
  485. width: 20px;
  486. height: 20px;
  487. fill: currentColor;
  488. }
  489. }
  490. }
  491. </style>