/** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ /* global dragOptions, folderDropOptions, OC */ (function() { if (!OCA.Files) { /** * Namespace for the files app * @namespace OCA.Files */ OCA.Files = {}; } /** * @namespace OCA.Files.App */ OCA.Files.App = { /** * Navigation instance * * @member {OCP.Files.Navigation} */ navigation: null, /** * File list for the "All files" section. * * @member {OCA.Files.FileList} */ fileList: null, currentFileList: null, /** * Backbone model for storing files preferences */ _filesConfig: null, /** * Initializes the files app */ initialize: function() { this.$showHiddenFiles = $('input#showhiddenfilesToggle'); var showHidden = $('#showHiddenFiles').val() === "1"; this.$showHiddenFiles.prop('checked', showHidden); // Toggle for grid view this.$showGridView = $('input#showgridview'); this.$showGridView.on('change', _.bind(this._onGridviewChange, this)); if ($('#fileNotFound').val() === "1") { OC.Notification.show(t('files', 'File could not be found'), {type: 'error'}); } this._filesConfig = OCP.InitialState.loadState('files', 'config', {}) var { fileid, scrollto, openfile } = OC.Util.History.parseUrlQuery(); var fileActions = new OCA.Files.FileActions(); // default actions fileActions.registerDefaultActions(); // regular actions fileActions.merge(OCA.Files.fileActions); this._onActionsUpdated = _.bind(this._onActionsUpdated, this); OCA.Files.fileActions.on('setDefault.app-files', this._onActionsUpdated); OCA.Files.fileActions.on('registerAction.app-files', this._onActionsUpdated); this.files = OCA.Files.Files; // TODO: ideally these should be in a separate class / app (the embedded "all files" app) this.fileList = new OCA.Files.FileList( $('#app-content-files'), { dragOptions: dragOptions, folderDropOptions: folderDropOptions, fileActions: fileActions, allowLegacyActions: true, scrollTo: scrollto, openFile: openfile, filesClient: OC.Files.getClient(), multiSelectMenu: [ { name: 'copyMove', displayName: t('files', 'Move or copy'), iconClass: 'icon-external', order: 10, }, { name: 'download', displayName: t('files', 'Download'), iconClass: 'icon-download', order: 10, }, OCA.Files.FileList.MultiSelectMenuActions.ToggleSelectionModeAction, { name: 'delete', displayName: t('files', 'Delete'), iconClass: 'icon-delete', order: 99, }, ...( OCA?.SystemTags === undefined ? [] : ([{ name: 'tags', displayName: t('files', 'Tags'), iconClass: 'icon-tag', order: 100, }]) ), ], sorting: { mode: $('#defaultFileSorting').val() === 'basename' ? 'name' : $('#defaultFileSorting').val(), direction: $('#defaultFileSortingDirection').val() }, config: this._filesConfig, enableUpload: true, maxChunkSize: OC.appConfig.files && OC.appConfig.files.max_chunk_size } ); this.updateCurrentFileList(this.fileList) this.files.initialize(); // for backward compatibility, the global FileList will // refer to the one of the "files" view window.FileList = this.fileList; OC.Plugins.attach('OCA.Files.App', this); this._setupEvents(); if (sessionStorage.getItem('WhatsNewServerCheck') < (Date.now() - 3600*1000)) { OCP.WhatsNew.query(); // for Nextcloud server sessionStorage.setItem('WhatsNewServerCheck', Date.now()); } window._nc_event_bus.emit('files:legacy-view:initialized', this); this.navigation = OCP.Files.Navigation }, /** * Destroy the app */ destroy: function() { this.fileList.destroy(); this.fileList = null; this.files = null; OCA.Files.fileActions.off('setDefault.app-files', this._onActionsUpdated); OCA.Files.fileActions.off('registerAction.app-files', this._onActionsUpdated); }, _onActionsUpdated: function(ev) { // forward new action to the file list if (ev.action) { this.fileList.fileActions.registerAction(ev.action); } else if (ev.defaultAction) { this.fileList.fileActions.setDefault( ev.defaultAction.mime, ev.defaultAction.name ); } }, /** * Set the currently active file list * * Due to the file list implementations being registered after clicking the * navigation item for the first time, OCA.Files.App is not aware of those until * they have initialized themselves. Therefore the files list needs to call this * method manually * * @param {OCA.Files.FileList} newFileList - */ updateCurrentFileList: function(newFileList) { if (this.currentFileList === newFileList) { return } this.currentFileList = newFileList; if (this.currentFileList !== null) { // update grid view to the current value const isGridView = this.$showGridView.is(':checked'); this.currentFileList.setGridView(isGridView); } }, /** * Return the currently active file list * @return {?OCA.Files.FileList} */ getCurrentFileList: function () { return this.currentFileList; }, /** * Returns the container of the currently visible app. * * @return app container */ getCurrentAppContainer: function() { var viewId = this.getActiveView(); return $('#app-content-' + viewId); }, /** * Sets the currently active view * @param viewId view id */ setActiveView: function(viewId) { // The Navigation API will handle the final event window._nc_event_bus.emit('files:legacy-navigation:changed', { id: viewId }) }, /** * Returns the view id of the currently active view * @return view id */ getActiveView: function() { return this.navigation && this.navigation.active && this.navigation.active.id; }, /** * * @returns {Backbone.Model} */ getFilesConfig: function() { return this._filesConfig; }, /** * Setup events based on URL changes */ _setupEvents: function() { OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this)); // detect when app changed their current directory $('#app-content').delegate('>div', 'changeDirectory', _.bind(this._onDirectoryChanged, this)); $('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this)); $('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this)); }, /** * Event handler for when the current navigation item has changed */ _onNavigationChanged: function(view) { var params; if (view && (view.itemId || view.id)) { if (view.id) { params = { view: view.id, dir: '/', } } else { // Legacy handling params = { view: typeof view.view === 'string' && view.view !== '' ? view.view : view.itemId, dir: view.dir ? view.dir : '/' } } this._changeUrl(params.view, params.dir); OCA.Files.Sidebar.close(); this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params)); window._nc_event_bus.emit('files:navigation:changed') } }, /** * Event handler for when an app notified that its directory changed */ _onDirectoryChanged: function(e) { if (e.dir && !e.changedThroughUrl) { this._changeUrl(this.getActiveView(), e.dir, e.fileId); } }, /** * Event handler for when an app notified that its directory changed */ _onAfterDirectoryChanged: function(e) { if (e.dir && e.fileId) { this._changeUrl(this.getActiveView(), e.dir, e.fileId); } }, /** * Event handler for when an app notifies that it needs space * for viewer mode. */ _onChangeViewerMode: function(e) { var state = !!e.viewerModeEnabled; if (e.viewerModeEnabled) { OCA.Files.Sidebar.close(); } $('#app-navigation').toggleClass('hidden', state); $('.app-files').toggleClass('viewer-mode no-sidebar', state); }, /** * Event handler for when the URL changed */ _onPopState: function(params) { params = _.extend({ dir: '/', view: 'files' }, params); var lastId = this.getActiveView(); if (!this.navigation.views.find(view => view.id === params.view)) { params.view = 'files'; } this.setActiveView(params.view, {silent: true}); if (lastId !== this.getActiveView()) { this.getCurrentAppContainer().trigger(new $.Event('show', params)); window._nc_event_bus.emit('files:navigation:changed') } this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params)); }, /** * Encode URL params into a string, except for the "dir" attribute * that gets encoded as path where "/" is not encoded * * @param {Object.} params * @return {string} encoded params */ _makeUrlParams: function(params) { var dir = params.dir; delete params.dir; return 'dir=' + OC.encodePath(dir) + '&' + OC.buildQueryString(params); }, /** * Change the URL to point to the given dir and view */ _changeUrl: function(view, dir, fileId) { var params = { dir: dir }; if (view !== 'files') { params.view = view; } else if (fileId) { params.fileid = fileId; } var currentParams = OC.Util.History.parseUrlQuery(); if (currentParams.dir === params.dir && currentParams.view === params.view) { if (currentParams.fileid !== params.fileid) { // if only fileid changed or was added, replace instead of push OC.Util.History.replaceState(this._makeUrlParams(params)); return } } else { OC.Util.History.pushState(this._makeUrlParams(params)); return } }, /** * Toggle showing gridview by default or not * * @returns {undefined} */ _onGridviewChange: function() { const isGridView = this.$showGridView.is(':checked'); // only save state if user is logged in if (OC.currentUser) { $.post(OC.generateUrl('/apps/files/api/v1/showgridview'), { show: isGridView, }); } this.$showGridView.next('#view-toggle') .removeClass('icon-toggle-filelist icon-toggle-pictures') .addClass(isGridView ? 'icon-toggle-filelist' : 'icon-toggle-pictures') this.$showGridView.next('#view-toggle') .attr('title', isGridView ? t('files', 'Show list view') : t('files', 'Show grid view')) this.$showGridView.attr('aria-label', isGridView ? t('files', 'Show list view') : t('files', 'Show grid view')) if (this.currentFileList) { this.currentFileList.setGridView(isGridView); } }, }; })(); window.addEventListener('DOMContentLoaded', function() { // wait for other apps/extensions to register their event handlers and file actions // in the "ready" clause _.defer(function() { OCA.Files.App.initialize(); }); });