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.

app.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. /*
  2. * Copyright (c) 2014
  3. *
  4. * @author Vincent Petry
  5. * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
  6. *
  7. * This file is licensed under the Affero General Public License version 3
  8. * or later.
  9. *
  10. * See the COPYING-README file.
  11. *
  12. */
  13. /* global dragOptions, folderDropOptions, OC */
  14. (function() {
  15. if (!OCA.Files) {
  16. /**
  17. * Namespace for the files app
  18. * @namespace OCA.Files
  19. */
  20. OCA.Files = {};
  21. }
  22. /**
  23. * @namespace OCA.Files.App
  24. */
  25. OCA.Files.App = {
  26. /**
  27. * Navigation instance
  28. *
  29. * @member {OCP.Files.Navigation}
  30. */
  31. navigation: null,
  32. /**
  33. * File list for the "All files" section.
  34. *
  35. * @member {OCA.Files.FileList}
  36. */
  37. fileList: null,
  38. currentFileList: null,
  39. /**
  40. * Backbone model for storing files preferences
  41. */
  42. _filesConfig: null,
  43. /**
  44. * Initializes the files app
  45. */
  46. initialize: function() {
  47. this.navigation = OCP.Files.Navigation;
  48. this.$showHiddenFiles = $('input#showhiddenfilesToggle');
  49. var showHidden = $('#showHiddenFiles').val() === "1";
  50. this.$showHiddenFiles.prop('checked', showHidden);
  51. // crop image previews
  52. this.$cropImagePreviews = $('input#cropimagepreviewsToggle');
  53. var cropImagePreviews = $('#cropImagePreviews').val() === "1";
  54. this.$cropImagePreviews.prop('checked', cropImagePreviews);
  55. // Toggle for grid view
  56. this.$showGridView = $('input#showgridview');
  57. this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
  58. if ($('#fileNotFound').val() === "1") {
  59. OC.Notification.show(t('files', 'File could not be found'), {type: 'error'});
  60. }
  61. this._filesConfig = new OC.Backbone.Model({
  62. showhidden: showHidden,
  63. cropimagepreviews: cropImagePreviews,
  64. });
  65. var urlParams = OC.Util.History.parseUrlQuery();
  66. var fileActions = new OCA.Files.FileActions();
  67. // default actions
  68. fileActions.registerDefaultActions();
  69. // regular actions
  70. fileActions.merge(OCA.Files.fileActions);
  71. this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
  72. OCA.Files.fileActions.on('setDefault.app-files', this._onActionsUpdated);
  73. OCA.Files.fileActions.on('registerAction.app-files', this._onActionsUpdated);
  74. this.files = OCA.Files.Files;
  75. // TODO: ideally these should be in a separate class / app (the embedded "all files" app)
  76. this.fileList = new OCA.Files.FileList(
  77. $('#app-content-files'), {
  78. dragOptions: dragOptions,
  79. folderDropOptions: folderDropOptions,
  80. fileActions: fileActions,
  81. allowLegacyActions: true,
  82. scrollTo: urlParams.scrollto,
  83. openFile: urlParams.openfile,
  84. filesClient: OC.Files.getClient(),
  85. multiSelectMenu: [
  86. {
  87. name: 'copyMove',
  88. displayName: t('files', 'Move or copy'),
  89. iconClass: 'icon-external',
  90. order: 10,
  91. },
  92. {
  93. name: 'download',
  94. displayName: t('files', 'Download'),
  95. iconClass: 'icon-download',
  96. order: 10,
  97. },
  98. OCA.Files.FileList.MultiSelectMenuActions.ToggleSelectionModeAction,
  99. {
  100. name: 'delete',
  101. displayName: t('files', 'Delete'),
  102. iconClass: 'icon-delete',
  103. order: 99,
  104. },
  105. {
  106. name: 'tags',
  107. displayName: t('files', 'Tags'),
  108. iconClass: 'icon-tag',
  109. order: 100,
  110. },
  111. ],
  112. sorting: {
  113. mode: $('#defaultFileSorting').val(),
  114. direction: $('#defaultFileSortingDirection').val()
  115. },
  116. config: this._filesConfig,
  117. enableUpload: true,
  118. maxChunkSize: OC.appConfig.files && OC.appConfig.files.max_chunk_size
  119. }
  120. );
  121. this.updateCurrentFileList(this.fileList)
  122. this.files.initialize();
  123. // for backward compatibility, the global FileList will
  124. // refer to the one of the "files" view
  125. window.FileList = this.fileList;
  126. OC.Plugins.attach('OCA.Files.App', this);
  127. this._setupEvents();
  128. // trigger URL change event handlers
  129. this._onPopState(urlParams);
  130. this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200);
  131. this._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200);
  132. if (sessionStorage.getItem('WhatsNewServerCheck') < (Date.now() - 3600*1000)) {
  133. OCP.WhatsNew.query(); // for Nextcloud server
  134. sessionStorage.setItem('WhatsNewServerCheck', Date.now());
  135. }
  136. },
  137. /**
  138. * Destroy the app
  139. */
  140. destroy: function() {
  141. this.fileList.destroy();
  142. this.fileList = null;
  143. this.files = null;
  144. OCA.Files.fileActions.off('setDefault.app-files', this._onActionsUpdated);
  145. OCA.Files.fileActions.off('registerAction.app-files', this._onActionsUpdated);
  146. },
  147. _onActionsUpdated: function(ev) {
  148. // forward new action to the file list
  149. if (ev.action) {
  150. this.fileList.fileActions.registerAction(ev.action);
  151. } else if (ev.defaultAction) {
  152. this.fileList.fileActions.setDefault(
  153. ev.defaultAction.mime,
  154. ev.defaultAction.name
  155. );
  156. }
  157. },
  158. /**
  159. * Set the currently active file list
  160. *
  161. * Due to the file list implementations being registered after clicking the
  162. * navigation item for the first time, OCA.Files.App is not aware of those until
  163. * they have initialized themselves. Therefore the files list needs to call this
  164. * method manually
  165. *
  166. * @param {OCA.Files.FileList} newFileList -
  167. */
  168. updateCurrentFileList: function(newFileList) {
  169. if (this.currentFileList === newFileList) {
  170. return
  171. }
  172. this.currentFileList = newFileList;
  173. if (this.currentFileList !== null) {
  174. // update grid view to the current value
  175. const isGridView = this.$showGridView.is(':checked');
  176. this.currentFileList.setGridView(isGridView);
  177. }
  178. },
  179. /**
  180. * Return the currently active file list
  181. * @return {?OCA.Files.FileList}
  182. */
  183. getCurrentFileList: function () {
  184. return this.currentFileList;
  185. },
  186. /**
  187. * Returns the container of the currently visible app.
  188. *
  189. * @return app container
  190. */
  191. getCurrentAppContainer: function() {
  192. var viewId = this.getActiveView();
  193. return $('#app-content-' + viewId);
  194. },
  195. /**
  196. * Sets the currently active view
  197. * @param viewId view id
  198. */
  199. setActiveView: function(viewId, options) {
  200. window._nc_event_bus.emit('files:view:changed', { id: viewId })
  201. },
  202. /**
  203. * Returns the view id of the currently active view
  204. * @return view id
  205. */
  206. getActiveView: function() {
  207. return this.navigation.active
  208. && this.navigation.active.id;
  209. },
  210. /**
  211. *
  212. * @returns {Backbone.Model}
  213. */
  214. getFilesConfig: function() {
  215. return this._filesConfig;
  216. },
  217. /**
  218. * Setup events based on URL changes
  219. */
  220. _setupEvents: function() {
  221. OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this));
  222. // detect when app changed their current directory
  223. $('#app-content').delegate('>div', 'changeDirectory', _.bind(this._onDirectoryChanged, this));
  224. $('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this));
  225. $('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this));
  226. window._nc_event_bus.subscribe('files:view:changed', _.bind(this._onNavigationChanged, this))
  227. $('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this));
  228. this.$showHiddenFiles.on('change', _.bind(this._onShowHiddenFilesChange, this));
  229. this.$cropImagePreviews.on('change', _.bind(this._onCropImagePreviewsChange, this));
  230. },
  231. /**
  232. * Toggle showing hidden files according to the settings checkbox
  233. *
  234. * @returns {undefined}
  235. */
  236. _onShowHiddenFilesChange: function() {
  237. var show = this.$showHiddenFiles.is(':checked');
  238. this._filesConfig.set('showhidden', show);
  239. this._debouncedPersistShowHiddenFilesState();
  240. },
  241. /**
  242. * Persist show hidden preference on the server
  243. *
  244. * @returns {undefined}
  245. */
  246. _persistShowHiddenFilesState: function() {
  247. var show = this._filesConfig.get('showhidden');
  248. $.post(OC.generateUrl('/apps/files/api/v1/showhidden'), {
  249. show: show
  250. });
  251. },
  252. /**
  253. * Toggle cropping image previews according to the settings checkbox
  254. *
  255. * @returns void
  256. */
  257. _onCropImagePreviewsChange: function() {
  258. var crop = this.$cropImagePreviews.is(':checked');
  259. this._filesConfig.set('cropimagepreviews', crop);
  260. this._debouncedPersistCropImagePreviewsState();
  261. },
  262. /**
  263. * Persist crop image previews preference on the server
  264. *
  265. * @returns void
  266. */
  267. _persistCropImagePreviewsState: function() {
  268. var crop = this._filesConfig.get('cropimagepreviews');
  269. $.post(OC.generateUrl('/apps/files/api/v1/cropimagepreviews'), {
  270. crop: crop
  271. });
  272. },
  273. /**
  274. * Event handler for when the current navigation item has changed
  275. */
  276. _onNavigationChanged: function(view) {
  277. var params;
  278. if (view && (view.itemId || view.id)) {
  279. if (view.id) {
  280. params = {
  281. view: view.id,
  282. dir: '/',
  283. }
  284. } else {
  285. // Legacy handling
  286. params = {
  287. view: typeof view.view === 'string' && view.view !== '' ? view.view : view.itemId,
  288. dir: view.dir ? view.dir : '/'
  289. }
  290. }
  291. this._changeUrl(params.view, params.dir);
  292. OCA.Files.Sidebar.close();
  293. this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params));
  294. window._nc_event_bus.emit('files:navigation:changed')
  295. }
  296. },
  297. /**
  298. * Event handler for when an app notified that its directory changed
  299. */
  300. _onDirectoryChanged: function(e) {
  301. if (e.dir && !e.changedThroughUrl) {
  302. this._changeUrl(this.getActiveView(), e.dir, e.fileId);
  303. }
  304. },
  305. /**
  306. * Event handler for when an app notified that its directory changed
  307. */
  308. _onAfterDirectoryChanged: function(e) {
  309. if (e.dir && e.fileId) {
  310. this._changeUrl(this.getActiveView(), e.dir, e.fileId);
  311. }
  312. },
  313. /**
  314. * Event handler for when an app notifies that it needs space
  315. * for viewer mode.
  316. */
  317. _onChangeViewerMode: function(e) {
  318. var state = !!e.viewerModeEnabled;
  319. if (e.viewerModeEnabled) {
  320. OCA.Files.Sidebar.close();
  321. }
  322. $('#app-navigation').toggleClass('hidden', state);
  323. $('.app-files').toggleClass('viewer-mode no-sidebar', state);
  324. },
  325. /**
  326. * Event handler for when the URL changed
  327. */
  328. _onPopState: function(params) {
  329. params = _.extend({
  330. dir: '/',
  331. view: 'files'
  332. }, params);
  333. var lastId = this.navigation.active;
  334. if (!this.navigation.views.find(view => view.id === params.view)) {
  335. params.view = 'files';
  336. }
  337. this.setActiveView(params.view, {silent: true});
  338. if (lastId !== this.getActiveView()) {
  339. this.getCurrentAppContainer().trigger(new $.Event('show'));
  340. }
  341. this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params));
  342. window._nc_event_bus.emit('files:navigation:changed')
  343. },
  344. /**
  345. * Encode URL params into a string, except for the "dir" attribute
  346. * that gets encoded as path where "/" is not encoded
  347. *
  348. * @param {Object.<string>} params
  349. * @return {string} encoded params
  350. */
  351. _makeUrlParams: function(params) {
  352. var dir = params.dir;
  353. delete params.dir;
  354. return 'dir=' + OC.encodePath(dir) + '&' + OC.buildQueryString(params);
  355. },
  356. /**
  357. * Change the URL to point to the given dir and view
  358. */
  359. _changeUrl: function(view, dir, fileId) {
  360. var params = {dir: dir};
  361. if (view !== 'files') {
  362. params.view = view;
  363. } else if (fileId) {
  364. params.fileid = fileId;
  365. }
  366. var currentParams = OC.Util.History.parseUrlQuery();
  367. if (currentParams.dir === params.dir && currentParams.view === params.view) {
  368. if (currentParams.fileid !== params.fileid) {
  369. // if only fileid changed or was added, replace instead of push
  370. OC.Util.History.replaceState(this._makeUrlParams(params));
  371. }
  372. } else {
  373. OC.Util.History.pushState(this._makeUrlParams(params));
  374. }
  375. },
  376. /**
  377. * Toggle showing gridview by default or not
  378. *
  379. * @returns {undefined}
  380. */
  381. _onGridviewChange: function() {
  382. const isGridView = this.$showGridView.is(':checked');
  383. // only save state if user is logged in
  384. if (OC.currentUser) {
  385. $.post(OC.generateUrl('/apps/files/api/v1/showgridview'), {
  386. show: isGridView,
  387. });
  388. }
  389. this.$showGridView.next('#view-toggle')
  390. .removeClass('icon-toggle-filelist icon-toggle-pictures')
  391. .addClass(isGridView ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
  392. this.$showGridView.next('#view-toggle')
  393. .attr('title', isGridView ? t('files', 'Show list view') : t('files', 'Show grid view'))
  394. this.$showGridView.attr('aria-label', isGridView ? t('files', 'Show list view') : t('files', 'Show grid view'))
  395. if (this.currentFileList) {
  396. this.currentFileList.setGridView(isGridView);
  397. }
  398. },
  399. };
  400. })();
  401. window.addEventListener('DOMContentLoaded', function() {
  402. // wait for other apps/extensions to register their event handlers and file actions
  403. // in the "ready" clause
  404. _.defer(function() {
  405. OCA.Files.App.initialize();
  406. });
  407. });