Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v26.0.0beta1
$this, | $this, | ||||
[ | [ | ||||
'routes' => [ | 'routes' => [ | ||||
[ | |||||
'name' => 'view#index', | |||||
'url' => '/', | |||||
'verb' => 'GET', | |||||
], | |||||
[ | |||||
'name' => 'view#index', | |||||
'url' => '/{view}', | |||||
'verb' => 'GET', | |||||
'postfix' => 'view', | |||||
], | |||||
[ | |||||
'name' => 'view#index', | |||||
'url' => '/{view}/{fileid}', | |||||
'verb' => 'GET', | |||||
'postfix' => 'fileid', | |||||
], | |||||
[ | [ | ||||
'name' => 'View#showFile', | 'name' => 'View#showFile', | ||||
'url' => '/f/{fileid}', | 'url' => '/f/{fileid}', | ||||
'verb' => 'GET', | 'verb' => 'GET', | ||||
'root' => '', | 'root' => '', | ||||
], | ], | ||||
[ | |||||
'name' => 'ajax#getStorageStats', | |||||
'url' => '/ajax/getstoragestats', | |||||
'verb' => 'GET', | |||||
], | |||||
[ | [ | ||||
'name' => 'API#getThumbnail', | 'name' => 'API#getThumbnail', | ||||
'url' => '/api/v1/thumbnail/{x}/{y}/{file}', | 'url' => '/api/v1/thumbnail/{x}/{y}/{file}', | ||||
'url' => '/api/v1/showgridview', | 'url' => '/api/v1/showgridview', | ||||
'verb' => 'GET' | 'verb' => 'GET' | ||||
], | ], | ||||
[ | |||||
'name' => 'view#index', | |||||
'url' => '/', | |||||
'verb' => 'GET', | |||||
], | |||||
[ | |||||
'name' => 'ajax#getStorageStats', | |||||
'url' => '/ajax/getstoragestats', | |||||
'verb' => 'GET', | |||||
], | |||||
[ | [ | ||||
'name' => 'API#toggleShowFolder', | 'name' => 'API#toggleShowFolder', | ||||
'url' => '/api/v1/toggleShowFolder/{key}', | 'url' => '/api/v1/toggleShowFolder/{key}', |
*/ | */ | ||||
OCA.Files.App = { | OCA.Files.App = { | ||||
/** | /** | ||||
* Navigation control | |||||
* Navigation instance | |||||
* | * | ||||
* @member {OCA.Files.Navigation} | |||||
* @member {OCP.Files.Navigation} | |||||
*/ | */ | ||||
navigation: null, | navigation: null, | ||||
* Initializes the files app | * Initializes the files app | ||||
*/ | */ | ||||
initialize: function() { | initialize: function() { | ||||
this.navigation = new OCA.Files.Navigation($('#app-navigation')); | |||||
this.navigation = OCP.Files.Navigation; | |||||
this.$showHiddenFiles = $('input#showhiddenfilesToggle'); | this.$showHiddenFiles = $('input#showhiddenfilesToggle'); | ||||
var showHidden = $('#showHiddenFiles').val() === "1"; | var showHidden = $('#showHiddenFiles').val() === "1"; | ||||
this.$showHiddenFiles.prop('checked', showHidden); | this.$showHiddenFiles.prop('checked', showHidden); | ||||
* Destroy the app | * Destroy the app | ||||
*/ | */ | ||||
destroy: function() { | destroy: function() { | ||||
this.navigation = null; | |||||
this.fileList.destroy(); | this.fileList.destroy(); | ||||
this.fileList = null; | this.fileList = null; | ||||
this.files = null; | this.files = null; | ||||
* @return app container | * @return app container | ||||
*/ | */ | ||||
getCurrentAppContainer: function() { | getCurrentAppContainer: function() { | ||||
return this.navigation.getActiveContainer(); | |||||
var viewId = this.getActiveView(); | |||||
return $('#app-content-' + viewId); | |||||
}, | }, | ||||
/** | /** | ||||
* @param viewId view id | * @param viewId view id | ||||
*/ | */ | ||||
setActiveView: function(viewId, options) { | setActiveView: function(viewId, options) { | ||||
this.navigation.setActiveItem(viewId, options); | |||||
window._nc_event_bus.emit('files:view:changed', { id: viewId }) | |||||
}, | }, | ||||
/** | /** | ||||
* @return view id | * @return view id | ||||
*/ | */ | ||||
getActiveView: function() { | getActiveView: function() { | ||||
return this.navigation.getActiveItem(); | |||||
return this.navigation.active | |||||
&& this.navigation.active.id; | |||||
}, | }, | ||||
/** | /** | ||||
$('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this)); | $('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this)); | ||||
$('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this)); | $('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this)); | ||||
window._nc_event_bus.subscribe('files:view:changed', _.bind(this._onNavigationChanged, this)) | |||||
$('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this)); | $('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this)); | ||||
this.$showHiddenFiles.on('change', _.bind(this._onShowHiddenFilesChange, this)); | this.$showHiddenFiles.on('change', _.bind(this._onShowHiddenFilesChange, this)); | ||||
this.$cropImagePreviews.on('change', _.bind(this._onCropImagePreviewsChange, this)); | this.$cropImagePreviews.on('change', _.bind(this._onCropImagePreviewsChange, this)); | ||||
/** | /** | ||||
* Event handler for when the current navigation item has changed | * Event handler for when the current navigation item has changed | ||||
*/ | */ | ||||
_onNavigationChanged: function(e) { | |||||
_onNavigationChanged: function(view) { | |||||
var params; | var params; | ||||
if (e && e.itemId) { | |||||
params = { | |||||
view: typeof e.view === 'string' && e.view !== '' ? e.view : e.itemId, | |||||
dir: e.dir ? e.dir : '/' | |||||
}; | |||||
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); | this._changeUrl(params.view, params.dir); | ||||
OCA.Files.Sidebar.close(); | OCA.Files.Sidebar.close(); | ||||
this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params)); | |||||
this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params)); | |||||
window._nc_event_bus.emit('files:navigation:changed') | window._nc_event_bus.emit('files:navigation:changed') | ||||
} | } | ||||
}, | }, | ||||
*/ | */ | ||||
_onDirectoryChanged: function(e) { | _onDirectoryChanged: function(e) { | ||||
if (e.dir && !e.changedThroughUrl) { | if (e.dir && !e.changedThroughUrl) { | ||||
this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId); | |||||
this._changeUrl(this.getActiveView(), e.dir, e.fileId); | |||||
} | } | ||||
}, | }, | ||||
*/ | */ | ||||
_onAfterDirectoryChanged: function(e) { | _onAfterDirectoryChanged: function(e) { | ||||
if (e.dir && e.fileId) { | if (e.dir && e.fileId) { | ||||
this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId); | |||||
this._changeUrl(this.getActiveView(), e.dir, e.fileId); | |||||
} | } | ||||
}, | }, | ||||
dir: '/', | dir: '/', | ||||
view: 'files' | view: 'files' | ||||
}, params); | }, params); | ||||
var lastId = this.navigation.getActiveItem(); | |||||
if (!this.navigation.itemExists(params.view)) { | |||||
var lastId = this.navigation.active; | |||||
if (!this.navigation.views.find(view => view.id === params.view)) { | |||||
params.view = 'files'; | params.view = 'files'; | ||||
} | } | ||||
this.navigation.setActiveItem(params.view, {silent: true}); | |||||
if (lastId !== this.navigation.getActiveItem()) { | |||||
this.navigation.getActiveContainer().trigger(new $.Event('show')); | |||||
this.setActiveView(params.view, {silent: true}); | |||||
if (lastId !== this.getActiveView()) { | |||||
this.getCurrentAppContainer().trigger(new $.Event('show')); | |||||
} | } | ||||
this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params)); | |||||
this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params)); | |||||
window._nc_event_bus.emit('files:navigation:changed') | window._nc_event_bus.emit('files:navigation:changed') | ||||
}, | }, | ||||
"jquery.fileupload.js", | "jquery.fileupload.js", | ||||
"keyboardshortcuts.js", | "keyboardshortcuts.js", | ||||
"mainfileinfodetailview.js", | "mainfileinfodetailview.js", | ||||
"navigation.js", | |||||
"newfilemenu.js", | "newfilemenu.js", | ||||
"operationprogressbar.js", | "operationprogressbar.js", | ||||
"recentfilelist.js", | "recentfilelist.js", |
'script' => 'simplelist.php', | 'script' => 'simplelist.php', | ||||
'order' => 5, | 'order' => 5, | ||||
'name' => $l10n->t('Favorites'), | 'name' => $l10n->t('Favorites'), | ||||
'expandedState' => 'show_Quick_Access' | |||||
]; | ]; | ||||
}); | }); | ||||
} | } |
* @throws \OCP\PreConditionNotMetException | * @throws \OCP\PreConditionNotMetException | ||||
*/ | */ | ||||
public function toggleShowFolder(int $show, string $key): Response { | public function toggleShowFolder(int $show, string $key): Response { | ||||
// ensure the edited key exists | |||||
$navItems = \OCA\Files\App::getNavigationManager()->getAll(); | |||||
foreach ($navItems as $item) { | |||||
// check if data is valid | |||||
if (($show === 0 || $show === 1) && isset($item['expandedState']) && $key === $item['expandedState']) { | |||||
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', $key, (string)$show); | |||||
return new Response(); | |||||
} | |||||
if ($show !== 0 && $show !== 1) { | |||||
return new DataResponse([ | |||||
'message' => 'Invalid show value. Only 0 and 1 are allowed.' | |||||
], Http::STATUS_BAD_REQUEST); | |||||
} | } | ||||
$response = new Response(); | |||||
$response->setStatus(Http::STATUS_FORBIDDEN); | |||||
return $response; | |||||
$userId = $this->userSession->getUser()->getUID(); | |||||
// Set the new value and return it | |||||
// Using a prefix prevents the user from setting arbitrary keys | |||||
$this->config->setUserValue($userId, 'files', 'show_' . $key, (string)$show); | |||||
return new JSONResponse([$key => $show]); | |||||
} | } | ||||
/** | /** |
* @throws NotFoundException | * @throws NotFoundException | ||||
*/ | */ | ||||
public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false, $openfile = null) { | public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false, $openfile = null) { | ||||
if ($fileid !== null && $dir === '') { | |||||
try { | |||||
return $this->redirectToFile($fileid); | |||||
} catch (NotFoundException $e) { | |||||
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true])); | |||||
} | |||||
} | |||||
// if ($fileid !== null && $dir === '') { | |||||
// try { | |||||
// return $this->redirectToFile($fileid); | |||||
// } catch (NotFoundException $e) { | |||||
// return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true])); | |||||
// } | |||||
// } | |||||
$nav = new \OCP\Template('files', 'appnavigation', ''); | $nav = new \OCP\Template('files', 'appnavigation', ''); | ||||
// FIXME: Make non static | // FIXME: Make non static | ||||
$storageInfo = $this->getStorageInfo(); | $storageInfo = $this->getStorageInfo(); | ||||
$user = $this->userSession->getUser()->getUID(); | |||||
$userId = $this->userSession->getUser()->getUID(); | |||||
// Get all the user favorites to create a submenu | // Get all the user favorites to create a submenu | ||||
try { | try { | ||||
$favElements = $this->activityHelper->getFavoriteFilePaths($this->userSession->getUser()->getUID()); | |||||
$favElements = $this->activityHelper->getFavoriteFilePaths($userId); | |||||
} catch (\RuntimeException $e) { | } catch (\RuntimeException $e) { | ||||
$favElements['folders'] = []; | $favElements['folders'] = []; | ||||
} | } | ||||
'order' => $navBarPositionPosition, | 'order' => $navBarPositionPosition, | ||||
'folderPosition' => $sortingValue, | 'folderPosition' => $sortingValue, | ||||
'name' => basename($favElement), | 'name' => basename($favElement), | ||||
'icon' => 'files', | |||||
'icon' => 'folder', | |||||
'quickaccesselement' => 'true' | 'quickaccesselement' => 'true' | ||||
]; | ]; | ||||
$navItems['favorites']['sublist'] = $favoritesSublistArray; | $navItems['favorites']['sublist'] = $favoritesSublistArray; | ||||
$navItems['favorites']['classes'] = $collapseClasses; | $navItems['favorites']['classes'] = $collapseClasses; | ||||
// parse every menu and add the expandedState user value | |||||
// parse every menu and add the expanded user value | |||||
foreach ($navItems as $key => $item) { | foreach ($navItems as $key => $item) { | ||||
if (isset($item['expandedState'])) { | |||||
$navItems[$key]['defaultExpandedState'] = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', $item['expandedState'], '0') === '1'; | |||||
} | |||||
$navItems[$key]['expanded'] = $this->config->getUserValue($userId, 'files', 'show_' . $item['id'], '0') === '1'; | |||||
} | } | ||||
$nav->assign('navigationItems', $navItems); | $nav->assign('navigationItems', $navItems); | ||||
$nav->assign('quota', $storageInfo['quota']); | $nav->assign('quota', $storageInfo['quota']); | ||||
$nav->assign('usage_relative', $storageInfo['relative']); | $nav->assign('usage_relative', $storageInfo['relative']); | ||||
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . rawurlencode($user))); | |||||
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . rawurlencode($userId))); | |||||
$contentItems = []; | $contentItems = []; | ||||
$this->initialState->provideInitialState('navigation', $navItems); | |||||
// render the container content for every navigation item | // render the container content for every navigation item | ||||
foreach ($navItems as $item) { | foreach ($navItems as $item) { | ||||
$content = ''; | $content = ''; | ||||
$params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? ''; | $params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? ''; | ||||
$params['isPublic'] = false; | $params['isPublic'] = false; | ||||
$params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no'; | $params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no'; | ||||
$params['defaultFileSorting'] = $this->config->getUserValue($user, 'files', 'file_sorting', 'name'); | |||||
$params['defaultFileSortingDirection'] = $this->config->getUserValue($user, 'files', 'file_sorting_direction', 'asc'); | |||||
$params['showgridview'] = $this->config->getUserValue($user, 'files', 'show_grid', false); | |||||
$showHidden = (bool) $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', false); | |||||
$params['defaultFileSorting'] = $this->config->getUserValue($userId, 'files', 'file_sorting', 'name'); | |||||
$params['defaultFileSortingDirection'] = $this->config->getUserValue($userId, 'files', 'file_sorting_direction', 'asc'); | |||||
$params['showgridview'] = $this->config->getUserValue($userId, 'files', 'show_grid', false); | |||||
$showHidden = (bool) $this->config->getUserValue($userId, 'files', 'show_hidden', false); | |||||
$params['showHiddenFiles'] = $showHidden ? 1 : 0; | $params['showHiddenFiles'] = $showHidden ? 1 : 0; | ||||
$cropImagePreviews = (bool) $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', true); | |||||
$cropImagePreviews = (bool) $this->config->getUserValue($userId, 'files', 'crop_image_previews', true); | |||||
$params['cropImagePreviews'] = $cropImagePreviews ? 1 : 0; | $params['cropImagePreviews'] = $cropImagePreviews ? 1 : 0; | ||||
$params['fileNotFound'] = $fileNotFound ? 1 : 0; | $params['fileNotFound'] = $fileNotFound ? 1 : 0; | ||||
$params['appNavigation'] = $nav; | $params['appNavigation'] = $nav; |
/** | |||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @license AGPL-3.0-or-later | |||||
* | |||||
* 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/>. | |||||
* | |||||
*/ | |||||
import { loadState } from '@nextcloud/initial-state' | |||||
import logger from '../logger.js' | |||||
/** | |||||
* Fetch and register the legacy files views | |||||
*/ | |||||
export default function() { | |||||
const legacyViews = Object.values(loadState('files', 'navigation', {})) | |||||
if (legacyViews.length > 0) { | |||||
logger.debug('Legacy files views detected. Processing...', legacyViews) | |||||
legacyViews.forEach(view => { | |||||
registerLegacyView(view) | |||||
if (view.sublist) { | |||||
view.sublist.forEach(subview => registerLegacyView({ ...subview, parent: view.id })) | |||||
} | |||||
}) | |||||
} | |||||
} | |||||
const registerLegacyView = function({ id, name, order, icon, parent, classes = '', expanded }) { | |||||
OCP.Files.Navigation.register({ | |||||
id, | |||||
name, | |||||
iconClass: icon ? `icon-${icon}` : 'nav-icon-' + id, | |||||
order, | |||||
parent, | |||||
legacy: true, | |||||
sticky: classes.includes('pinned'), | |||||
expanded: expanded === true, | |||||
}) | |||||
} |
/** | /** | ||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | * | ||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at> | |||||
* @author Roeland Jago Douma <roeland@famdouma.nl> | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | * | ||||
* @license AGPL-3.0-or-later | * @license AGPL-3.0-or-later | ||||
* | * | ||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
* | * | ||||
*/ | */ | ||||
import { getCurrentUser } from '@nextcloud/auth' | |||||
import { getLoggerBuilder } from '@nextcloud/logger' | import { getLoggerBuilder } from '@nextcloud/logger' | ||||
const getLogger = user => { | |||||
if (user === null) { | |||||
return getLoggerBuilder() | |||||
.setApp('files') | |||||
.build() | |||||
} | |||||
return getLoggerBuilder() | |||||
.setApp('files') | |||||
.setUid(user.uid) | |||||
.build() | |||||
} | |||||
export default getLogger(getCurrentUser()) | |||||
export default getLoggerBuilder() | |||||
.setApp('files') | |||||
.detectUser() | |||||
.build() |
import './files-app-settings' | |||||
import './templates' | |||||
import './legacy/filelistSearch' | |||||
import './files-app-settings.js' | |||||
import './templates.js' | |||||
import './legacy/filelistSearch.js' | |||||
import processLegacyFilesViews from './legacy/navigationMapper.js' | |||||
import Vue from 'vue' | |||||
import NavigationService from './services/Navigation.ts' | |||||
import NavigationView from './views/Navigation.vue' | |||||
import router from './router/router.js' | |||||
// Init Files App Navigation Service | |||||
const Navigation = new NavigationService() | |||||
// Assign Navigation Service to the global OCP.Files | |||||
window.OCP.Files = window.OCP.Files ?? {} | |||||
Object.assign(window.OCP.Files, { Navigation }) | |||||
// Init Navigation View | |||||
const View = Vue.extend(NavigationView) | |||||
const FilesNavigationRoot = new View({ | |||||
name: 'FilesNavigationRoot', | |||||
propsData: { | |||||
Navigation, | |||||
}, | |||||
router, | |||||
}) | |||||
FilesNavigationRoot.$mount('#app-navigation-files') | |||||
// Init legacy files views | |||||
processLegacyFilesViews() |
/** | |||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @license AGPL-3.0-or-later | |||||
* | |||||
* 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/>. | |||||
* | |||||
*/ | |||||
import Vue from 'vue' | |||||
import Router from 'vue-router' | |||||
import { generateUrl } from '@nextcloud/router' | |||||
Vue.use(Router) | |||||
export default new Router({ | |||||
mode: 'history', | |||||
// if index.php is in the url AND we got this far, then it's working: | |||||
// let's keep using index.php in the url | |||||
base: generateUrl('/apps/files', ''), | |||||
linkActiveClass: 'active', | |||||
routes: [ | |||||
{ | |||||
path: '/', | |||||
// Pretending we're using the default view | |||||
alias: '/files', | |||||
}, | |||||
{ | |||||
path: '/:view/:fileId?', | |||||
name: 'filelist', | |||||
props: true, | |||||
}, | |||||
{ | |||||
path: '/not-found', | |||||
name: 'notfound', | |||||
}, | |||||
], | |||||
}) |
/** | |||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @license AGPL-3.0-or-later | |||||
* | |||||
* 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/>. | |||||
* | |||||
*/ | |||||
import type Node from '@nextcloud/files/dist/files/node' | |||||
import isSvg from 'is-svg' | |||||
import logger from '../logger' | |||||
export interface Column { | |||||
/** Unique column ID */ | |||||
id: string | |||||
/** Translated column title */ | |||||
title: string | |||||
/** Property key from Node main or additional attributes. | |||||
Will be used if no custom sort function is provided. | |||||
Sorting will be done by localCompare */ | |||||
property: string | |||||
/** Special function used to sort Nodes between them */ | |||||
sortFunction?: (nodeA: Node, nodeB: Node) => number; | |||||
/** Custom summary of the column to display at the end of the list. | |||||
Will not be displayed if nothing is provided */ | |||||
summary?: (node: Node[]) => string | |||||
} | |||||
export interface Navigation { | |||||
/** Unique view ID */ | |||||
id: string | |||||
/** Translated view name */ | |||||
name: string | |||||
/** Method return the content of the provided path */ | |||||
getFiles: (path: string) => Node[] | |||||
/** The view icon as an inline svg */ | |||||
icon: string | |||||
/** The view order */ | |||||
order: number | |||||
/** This view column(s). Name and actions are | |||||
by default always included */ | |||||
columns?: Column[] | |||||
/** The empty view element to render your empty content into */ | |||||
emptyView?: (div: HTMLDivElement) => void | |||||
/** The parent unique ID */ | |||||
parent?: string | |||||
/** This view is sticky (sent at the bottom) */ | |||||
sticky?: boolean | |||||
/** This view has children and is expanded or not */ | |||||
expanded?: boolean | |||||
/** | |||||
* This view is sticky a legacy view. | |||||
* Here until all the views are migrated to Vue. | |||||
* @deprecated It will be removed in a near future | |||||
*/ | |||||
legacy?: boolean | |||||
/** | |||||
* An icon class. | |||||
* @deprecated It will be removed in a near future | |||||
*/ | |||||
iconClass?: string | |||||
} | |||||
export default class { | |||||
private _views: Navigation[] = [] | |||||
private _currentView: Navigation | null = null | |||||
constructor() { | |||||
logger.debug('Navigation service initialized') | |||||
} | |||||
register(view: Navigation) { | |||||
try { | |||||
isValidNavigation(view) | |||||
isUniqueNavigation(view, this._views) | |||||
} catch (e) { | |||||
if (e instanceof Error) { | |||||
logger.error(e.message, { view }) | |||||
} | |||||
throw e | |||||
} | |||||
if (view.legacy) { | |||||
logger.warn('Legacy view detected, please migrate to Vue') | |||||
} | |||||
if (view.iconClass) { | |||||
view.legacy = true | |||||
} | |||||
this._views.push(view) | |||||
} | |||||
get views(): Navigation[] { | |||||
return this._views | |||||
} | |||||
setActive(view: Navigation | null) { | |||||
this._currentView = view | |||||
} | |||||
get active(): Navigation | null { | |||||
return this._currentView | |||||
} | |||||
} | |||||
/** | |||||
* Make sure the given view is unique | |||||
* and not already registered. | |||||
*/ | |||||
const isUniqueNavigation = function(view: Navigation, views: Navigation[]): boolean { | |||||
if (views.find(search => search.id === view.id)) { | |||||
throw new Error(`Navigation id ${view.id} is already registered`) | |||||
} | |||||
return true | |||||
} | |||||
/** | |||||
* Typescript cannot validate an interface. | |||||
* Please keep in sync with the Navigation interface requirements. | |||||
*/ | |||||
const isValidNavigation = function(view: Navigation): boolean { | |||||
if (!view.id || typeof view.id !== 'string') { | |||||
throw new Error('Navigation id is required and must be a string') | |||||
} | |||||
if (!view.name || typeof view.name !== 'string') { | |||||
throw new Error('Navigation name is required and must be a string') | |||||
} | |||||
/** | |||||
* Legacy handle their content and icon differently | |||||
* TODO: remove when support for legacy views is removed | |||||
*/ | |||||
if (!view.legacy) { | |||||
if (!view.getFiles || typeof view.getFiles !== 'function') { | |||||
throw new Error('Navigation getFiles is required and must be a function') | |||||
} | |||||
if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) { | |||||
throw new Error('Navigation icon is required and must be a valid svg string') | |||||
} | |||||
} | |||||
if (!('order' in view) || typeof view.order !== 'number') { | |||||
throw new Error('Navigation order is required and must be a number') | |||||
} | |||||
// Optional properties | |||||
if (view.columns) { | |||||
view.columns.forEach(isValidColumn) | |||||
} | |||||
if (view.emptyView && typeof view.emptyView !== 'function') { | |||||
throw new Error('Navigation emptyView must be a function') | |||||
} | |||||
if (view.parent && typeof view.parent !== 'string') { | |||||
throw new Error('Navigation parent must be a string') | |||||
} | |||||
if ('sticky' in view && typeof view.sticky !== 'boolean') { | |||||
throw new Error('Navigation sticky must be a boolean') | |||||
} | |||||
if ('expanded' in view && typeof view.expanded !== 'boolean') { | |||||
throw new Error('Navigation expanded must be a boolean') | |||||
} | |||||
return true | |||||
} | |||||
/** | |||||
* Typescript cannot validate an interface. | |||||
* Please keep in sync with the Column interface requirements. | |||||
*/ | |||||
const isValidColumn = function(column: Column): boolean { | |||||
if (!column.id || typeof column.id !== 'string') { | |||||
throw new Error('Column id is required') | |||||
} | |||||
if (!column.title || typeof column.title !== 'string') { | |||||
throw new Error('Column title is required') | |||||
} | |||||
if (!column.property || typeof column.property !== 'string') { | |||||
throw new Error('Column property is required') | |||||
} | |||||
// Optional properties | |||||
if (column.sortFunction && typeof column.sortFunction !== 'function') { | |||||
throw new Error('Column sortFunction must be a function') | |||||
} | |||||
if (column.summary && typeof column.summary !== 'function') { | |||||
throw new Error('Column summary must be a function') | |||||
} | |||||
return true | |||||
} |
<!-- | |||||
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @author Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @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> | |||||
<NcAppNavigation> | |||||
<NcAppNavigationItem v-for="view in parentViews" | |||||
:key="view.id" | |||||
:allow-collapse="true" | |||||
:to="{name: 'filelist', params: { view: view.id }}" | |||||
:icon="view.iconClass" | |||||
:open="view.expanded" | |||||
:pinned="view.sticky" | |||||
:title="view.name" | |||||
@update:open="onToggleExpand(view)"> | |||||
<NcAppNavigationItem v-for="child in childViews[view.id]" | |||||
:key="child.id" | |||||
:to="{name: 'filelist', params: { view: child.id }}" | |||||
:icon="child.iconClass" | |||||
:title="child.name" /> | |||||
</NcAppNavigationItem> | |||||
</NcAppNavigation> | |||||
</template> | |||||
<script> | |||||
import { emit } from '@nextcloud/event-bus' | |||||
import { generateUrl } from '@nextcloud/router' | |||||
import axios from '@nextcloud/axios' | |||||
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' | |||||
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' | |||||
import Navigation from '../services/Navigation.ts' | |||||
import logger from '../logger.js' | |||||
export default { | |||||
name: 'Navigation', | |||||
components: { | |||||
NcAppNavigation, | |||||
NcAppNavigationItem, | |||||
}, | |||||
props: { | |||||
// eslint-disable-next-line vue/prop-name-casing | |||||
Navigation: { | |||||
type: Navigation, | |||||
required: true, | |||||
}, | |||||
}, | |||||
data() { | |||||
return { | |||||
key: 'value', | |||||
} | |||||
}, | |||||
computed: { | |||||
currentViewId() { | |||||
return this.$route.params.view || 'files' | |||||
}, | |||||
currentView() { | |||||
return this.views.find(view => view.id === this.currentViewId) | |||||
}, | |||||
/** @return {Navigation[]} */ | |||||
views() { | |||||
return this.Navigation.views | |||||
}, | |||||
parentViews() { | |||||
return this.views | |||||
// filter child views | |||||
.filter(view => !view.parent) | |||||
// sort views by order | |||||
.sort((a, b) => { | |||||
return a.order - b.order | |||||
}) | |||||
}, | |||||
childViews() { | |||||
return this.views | |||||
// filter parent views | |||||
.filter(view => !!view.parent) | |||||
// create a map of parents and their children | |||||
.reduce((list, view) => { | |||||
list[view.parent] = [...(list[view.parent] || []), view] | |||||
// Sort children by order | |||||
list[view.parent].sort((a, b) => { | |||||
return a.order - b.order | |||||
}) | |||||
return list | |||||
}, {}) | |||||
}, | |||||
}, | |||||
watch: { | |||||
currentView(view, oldView) { | |||||
logger.debug('View changed', { view }) | |||||
this.showView(view, oldView) | |||||
}, | |||||
}, | |||||
beforeMount() { | |||||
if (this.currentView) { | |||||
logger.debug('Navigation mounted. Showing requested view', { view: this.currentView }) | |||||
this.showView(this.currentView) | |||||
} | |||||
}, | |||||
methods: { | |||||
/** | |||||
* @param {Navigation} view the new active view | |||||
* @param {Navigation} oldView the old active view | |||||
*/ | |||||
showView(view, oldView) { | |||||
if (view.legacy) { | |||||
document.querySelectorAll('#app-content .viewcontainer').forEach(el => { | |||||
el.classList.add('hidden') | |||||
}) | |||||
document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer').classList.remove('hidden') | |||||
} | |||||
this.Navigation.setActive(view) | |||||
emit('files:view:changed', view) | |||||
}, | |||||
onToggleExpand(view) { | |||||
// Invert state | |||||
view.expanded = !view.expanded | |||||
axios.post(generateUrl(`/apps/files/api/v1/toggleShowFolder/${view.id}`), { show: view.expanded }) | |||||
}, | |||||
}, | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
// TODO: remove when https://github.com/nextcloud/nextcloud-vue/pull/3539 is in | |||||
.app-navigation::v-deep .app-navigation-entry-icon { | |||||
background-repeat: no-repeat; | |||||
background-position: center; | |||||
} | |||||
</style> |
<div id="app-navigation" role="navigation"> | |||||
<div id="app-navigation-files" role="navigation"></div> | |||||
<div class="hidden"> | |||||
<ul class="with-icon" tabindex="0"> | <ul class="with-icon" tabindex="0"> | ||||
<?php | <?php |
<label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>" | <label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>" | ||||
title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label> | title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label> | ||||
<!-- New files vue container --> | |||||
<div id="app-content-vue" class="hidden"></div> | |||||
<!-- Legacy views --> | |||||
<?php foreach ($_['appContents'] as $content) { ?> | <?php foreach ($_['appContents'] as $content) { ?> | ||||
<div id="app-content-<?php p($content['id']) ?>" class="hidden viewcontainer"> | <div id="app-content-<?php p($content['id']) ?>" class="hidden viewcontainer"> | ||||
<?php print_unescaped($content['content']) ?> | <?php print_unescaped($content['content']) ?> |
'quickaccesselement' => 'true', | 'quickaccesselement' => 'true', | ||||
], | ], | ||||
], | ], | ||||
'defaultExpandedState' => false, | |||||
'expandedState' => 'show_Quick_Access', | |||||
'expanded' => false, | |||||
'unread' => 0, | 'unread' => 0, | ||||
], | ], | ||||
'systemtagsfilter' => [ | 'systemtagsfilter' => [ | ||||
'active' => false, | 'active' => false, | ||||
'icon' => '', | 'icon' => '', | ||||
'type' => 'link', | 'type' => 'link', | ||||
'expandedState' => 'show_sharing_menu', | |||||
'defaultExpandedState' => false, | |||||
'expanded' => false, | |||||
'unread' => 0, | 'unread' => 0, | ||||
] | ] | ||||
]); | ]); |
'name' => $l->t('Shares'), | 'name' => $l->t('Shares'), | ||||
'classes' => 'collapsible', | 'classes' => 'collapsible', | ||||
'sublist' => $sharingSublistArray, | 'sublist' => $sharingSublistArray, | ||||
'expandedState' => 'show_sharing_menu' | |||||
]; | ]; | ||||
}); | }); | ||||
} | } |
'@babel/plugin-proposal-class-properties', | '@babel/plugin-proposal-class-properties', | ||||
], | ], | ||||
presets: [ | presets: [ | ||||
// https://babeljs.io/docs/en/babel-preset-typescript | |||||
'@babel/preset-typescript', | |||||
[ | [ | ||||
'@babel/preset-env', | '@babel/preset-env', | ||||
{ | { |
"dependencies": { | "dependencies": { | ||||
"@chenfengyuan/vue-qrcode": "^1.0.2", | "@chenfengyuan/vue-qrcode": "^1.0.2", | ||||
"@mdi/svg": "^7.0.96", | "@mdi/svg": "^7.0.96", | ||||
"@nextcloud/auth": "^1.3.0", | |||||
"@nextcloud/auth": "^2.0.0", | |||||
"@nextcloud/axios": "^1.10.0", | "@nextcloud/axios": "^1.10.0", | ||||
"@nextcloud/browser-storage": "^0.1.1", | "@nextcloud/browser-storage": "^0.1.1", | ||||
"@nextcloud/browserslist-config": "^2.3.0", | "@nextcloud/browserslist-config": "^2.3.0", | ||||
"@nextcloud/capabilities": "^1.0.4", | "@nextcloud/capabilities": "^1.0.4", | ||||
"@nextcloud/dialogs": "^4.0.0-beta.2", | "@nextcloud/dialogs": "^4.0.0-beta.2", | ||||
"@nextcloud/event-bus": "^3.0.2", | "@nextcloud/event-bus": "^3.0.2", | ||||
"@nextcloud/files": "^2.1.0", | |||||
"@nextcloud/initial-state": "^1.2.1", | |||||
"@nextcloud/files": "^3.0.0-beta.5", | |||||
"@nextcloud/initial-state": "^2.0.0", | |||||
"@nextcloud/l10n": "^1.6.0", | "@nextcloud/l10n": "^1.6.0", | ||||
"@nextcloud/logger": "^2.1.0", | |||||
"@nextcloud/logger": "^2.4.0", | |||||
"@nextcloud/moment": "^1.2.1", | "@nextcloud/moment": "^1.2.1", | ||||
"@nextcloud/password-confirmation": "^4.0.2", | "@nextcloud/password-confirmation": "^4.0.2", | ||||
"@nextcloud/paths": "^2.1.0", | "@nextcloud/paths": "^2.1.0", | ||||
"focus-visible": "^5.2.0", | "focus-visible": "^5.2.0", | ||||
"handlebars": "^4.7.7", | "handlebars": "^4.7.7", | ||||
"ical.js": "^1.4.0", | "ical.js": "^1.4.0", | ||||
"is-svg": "^4.3.2", | |||||
"jquery": "~3.6", | "jquery": "~3.6", | ||||
"jquery-migrate": "~3.4", | "jquery-migrate": "~3.4", | ||||
"jquery-ui": "^1.13.2", | "jquery-ui": "^1.13.2", | ||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@babel/node": "^7.20.0", | "@babel/node": "^7.20.0", | ||||
"@babel/preset-typescript": "^7.18.6", | |||||
"@cypress/browserify-preprocessor": "^3.0.2", | "@cypress/browserify-preprocessor": "^3.0.2", | ||||
"@nextcloud/babel-config": "^1.0.0", | "@nextcloud/babel-config": "^1.0.0", | ||||
"@nextcloud/cypress": "^1.0.0-beta.1", | "@nextcloud/cypress": "^1.0.0-beta.1", |
{ | { | ||||
"extends": "@vue/tsconfig/tsconfig.json", | "extends": "@vue/tsconfig/tsconfig.json", | ||||
"include": ["./**/*.ts"], | |||||
"include": ["./apps/**/*.ts", "./core/**/*.ts"], | |||||
"compilerOptions": { | "compilerOptions": { | ||||
"types": ["node"], | "types": ["node"], | ||||
"allowSyntheticDefaultImports": true, | |||||
"moduleResolution": "node", | |||||
"outDir": "./dist/", | |||||
"target": "ESNext", | "target": "ESNext", | ||||
"module": "esnext", | "module": "esnext", | ||||
"declaration": true, | |||||
"strict": true, | |||||
"moduleResolution": "node", | |||||
// Allow ts to import js files | |||||
"allowJs": true, | |||||
"allowSyntheticDefaultImports": true, | |||||
"declaration": false, | |||||
"noImplicitAny": false, | "noImplicitAny": false, | ||||
"resolveJsonModule": true | |||||
"resolveJsonModule": true, | |||||
"strict": true, | |||||
}, | }, | ||||
"ts-node": { | "ts-node": { | ||||
// these options are overrides used only by ts-node | // these options are overrides used only by ts-node |
'emoji-mart-vue-fast', | 'emoji-mart-vue-fast', | ||||
]), | ]), | ||||
}, | }, | ||||
{ | |||||
test: /\.tsx?$/, | |||||
use: 'babel-loader', | |||||
exclude: BabelLoaderExcludeNodeModulesExcept([]), | |||||
}, | |||||
{ | { | ||||
test: /\.js$/, | test: /\.js$/, | ||||
loader: 'babel-loader', | loader: 'babel-loader', | ||||
// make sure to use the handlebar runtime when importing | // make sure to use the handlebar runtime when importing | ||||
handlebars: 'handlebars/runtime', | handlebars: 'handlebars/runtime', | ||||
}, | }, | ||||
extensions: ['*', '.js', '.vue'], | |||||
extensions: ['*', '.ts', '.js', '.vue'], | |||||
symlinks: true, | symlinks: true, | ||||
fallback: { | fallback: { | ||||
buffer: require.resolve('buffer'), | buffer: require.resolve('buffer'), |