Sidebar.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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. <AppSidebar
  24. v-if="file"
  25. ref="sidebar"
  26. v-bind="appSidebar"
  27. @close="onClose"
  28. @update:starred="toggleStarred"
  29. @[defaultActionListener].stop.prevent="onDefaultAction">
  30. <!-- TODO: create a standard to allow multiple elements here? -->
  31. <template v-if="fileInfo" #primary-actions>
  32. <LegacyView v-for="view in views"
  33. :key="view.cid"
  34. :component="view"
  35. :file-info="fileInfo" />
  36. </template>
  37. <!-- Error display -->
  38. <div v-if="error" class="emptycontent">
  39. <div class="icon-error" />
  40. <h2>{{ error }}</h2>
  41. </div>
  42. <!-- If fileInfo fetch is complete, display tabs -->
  43. <template v-for="tab in tabs" v-else-if="fileInfo">
  44. <component
  45. :is="tabComponent(tab).is"
  46. v-if="canDisplay(tab)"
  47. :key="tab.id"
  48. :component="tabComponent(tab).component"
  49. :name="tab.name"
  50. :file-info="fileInfo" />
  51. </template>
  52. </AppSidebar>
  53. </template>
  54. <script>
  55. import $ from 'jquery'
  56. import axios from '@nextcloud/axios'
  57. import AppSidebar from 'nextcloud-vue/dist/Components/AppSidebar'
  58. import FileInfo from '../services/FileInfo'
  59. import LegacyTab from '../components/LegacyTab'
  60. import LegacyView from '../components/LegacyView'
  61. export default {
  62. name: 'Sidebar',
  63. components: {
  64. AppSidebar,
  65. LegacyView
  66. },
  67. data() {
  68. return {
  69. // reactive state
  70. Sidebar: OCA.Files.Sidebar.state,
  71. error: null,
  72. fileInfo: null,
  73. starLoading: false
  74. }
  75. },
  76. computed: {
  77. /**
  78. * Current filename
  79. * This is bound to the Sidebar service and
  80. * is used to load a new file
  81. * @returns {string}
  82. */
  83. file() {
  84. return this.Sidebar.file
  85. },
  86. /**
  87. * List of all the registered tabs
  88. * @returns {Array}
  89. */
  90. tabs() {
  91. return this.Sidebar.tabs
  92. },
  93. /**
  94. * List of all the registered views
  95. * @returns {Array}
  96. */
  97. views() {
  98. return this.Sidebar.views
  99. },
  100. /**
  101. * Current user dav root path
  102. * @returns {string}
  103. */
  104. davPath() {
  105. const user = OC.getCurrentUser().uid
  106. return OC.linkToRemote(`dav/files/${user}${encodeURIComponent(this.file)}`)
  107. },
  108. /**
  109. * Current active tab handler
  110. * @param {string} id the tab id to set as active
  111. * @returns {string} the current active tab
  112. */
  113. activeTab: {
  114. get: function() {
  115. return this.Sidebar.activeTab
  116. },
  117. set: function(id) {
  118. OCA.Files.Sidebar.activeTab = id
  119. }
  120. },
  121. /**
  122. * Sidebar subtitle
  123. * @returns {string}
  124. */
  125. subtitle() {
  126. return `${this.size}, ${this.time}`
  127. },
  128. /**
  129. * File last modified formatted string
  130. * @returns {string}
  131. */
  132. time() {
  133. return OC.Util.relativeModifiedDate(this.fileInfo.mtime)
  134. },
  135. /**
  136. * File size formatted string
  137. * @returns {string}
  138. */
  139. size() {
  140. return OC.Util.humanFileSize(this.fileInfo.size)
  141. },
  142. /**
  143. * File background/figure to illustrate the sidebar header
  144. * @returns {string}
  145. */
  146. background() {
  147. return this.getPreviewIfAny(this.fileInfo)
  148. },
  149. /**
  150. * App sidebar v-binding object
  151. *
  152. * @returns {Object}
  153. */
  154. appSidebar() {
  155. if (this.fileInfo) {
  156. return {
  157. background: this.background,
  158. active: this.activeTab,
  159. class: { 'has-preview': this.fileInfo.hasPreview },
  160. compact: !this.fileInfo.hasPreview,
  161. 'star-loading': this.starLoading,
  162. starred: this.fileInfo.isFavourited,
  163. subtitle: this.subtitle,
  164. title: this.fileInfo.name
  165. }
  166. } else if (this.error) {
  167. return {
  168. key: 'error', // force key to re-render
  169. subtitle: '',
  170. title: ''
  171. }
  172. } else {
  173. return {
  174. class: 'icon-loading',
  175. subtitle: '',
  176. title: ''
  177. }
  178. }
  179. },
  180. /**
  181. * Default action object for the current file
  182. *
  183. * @returns {Object}
  184. */
  185. defaultAction() {
  186. return this.fileInfo
  187. && OCA.Files && OCA.Files.App && OCA.Files.App.fileList
  188. && OCA.Files.App.fileList
  189. .fileActions.getDefaultFileAction(this.fileInfo.mimetype, this.fileInfo.type, OC.PERMISSION_READ)
  190. },
  191. /**
  192. * Dynamic header click listener to ensure
  193. * nothing is listening for a click if there
  194. * is no default action
  195. *
  196. * @returns {string|null}
  197. */
  198. defaultActionListener() {
  199. return this.defaultAction ? 'figure-click' : null
  200. }
  201. },
  202. watch: {
  203. // update the sidebar data
  204. async file(curr, prev) {
  205. this.resetData()
  206. if (curr && curr.trim() !== '') {
  207. try {
  208. this.fileInfo = await FileInfo(this.davPath)
  209. // adding this as fallback because other apps expect it
  210. this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
  211. // DEPRECATED legacy views
  212. // TODO: remove
  213. this.views.forEach(view => {
  214. view.setFileInfo(this.fileInfo)
  215. })
  216. this.$nextTick(() => {
  217. if (this.$refs.sidebar) {
  218. this.$refs.sidebar.updateTabs()
  219. }
  220. })
  221. } catch (error) {
  222. this.error = t('files', 'Error while loading the file data')
  223. console.error('Error while loading the file data')
  224. }
  225. }
  226. }
  227. },
  228. methods: {
  229. /**
  230. * Can this tab be displayed ?
  231. *
  232. * @param {Object} tab a registered tab
  233. * @returns {boolean}
  234. */
  235. canDisplay(tab) {
  236. if (tab.isLegacyTab) {
  237. return this.fileInfo && tab.component.canDisplay && tab.component.canDisplay(this.fileInfo)
  238. }
  239. // if the tab does not have an enabled method, we assume it's always available
  240. return tab.enabled ? tab.enabled(this.fileInfo) : true
  241. },
  242. onClose() {
  243. this.resetData()
  244. OCA.Files.Sidebar.file = ''
  245. },
  246. resetData() {
  247. this.error = null
  248. this.fileInfo = null
  249. this.$nextTick(() => {
  250. if (this.$refs.sidebar) {
  251. this.$refs.sidebar.updateTabs()
  252. }
  253. })
  254. },
  255. getPreviewIfAny(fileInfo) {
  256. if (fileInfo.hasPreview) {
  257. return OC.generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true`)
  258. }
  259. return OCA.Files.App.fileList._getIconUrl(fileInfo)
  260. },
  261. tabComponent(tab) {
  262. if (tab.isLegacyTab) {
  263. return {
  264. is: LegacyTab,
  265. component: tab.component
  266. }
  267. }
  268. return {
  269. is: tab.component
  270. }
  271. },
  272. /**
  273. * Toggle favourite state
  274. * TODO: better implementation
  275. *
  276. * @param {Boolean} state favourited or not
  277. */
  278. async toggleStarred(state) {
  279. try {
  280. this.starLoading = true
  281. await axios({
  282. method: 'PROPPATCH',
  283. url: this.davPath,
  284. data: `<?xml version="1.0"?>
  285. <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  286. ${state ? '<d:set>' : '<d:remove>'}
  287. <d:prop>
  288. <oc:favorite>1</oc:favorite>
  289. </d:prop>
  290. ${state ? '</d:set>' : '</d:remove>'}
  291. </d:propertyupdate>`
  292. })
  293. } catch (error) {
  294. OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file'))
  295. console.error('Unable to change favourite state', error)
  296. }
  297. this.starLoading = false
  298. },
  299. onDefaultAction() {
  300. if (this.defaultAction) {
  301. // generate fake context
  302. this.defaultAction.action(this.fileInfo.name, {
  303. fileInfo: this.fileInfo,
  304. dir: this.fileInfo.dir,
  305. fileList: OCA.Files.App.fileList,
  306. $file: $('body')
  307. })
  308. }
  309. }
  310. }
  311. }
  312. </script>
  313. <style lang="scss" scoped>
  314. #app-sidebar {
  315. &.has-preview::v-deep .app-sidebar-header__figure {
  316. background-size: cover;
  317. }
  318. }
  319. </style>