diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2024-12-13 12:00:28 +0100 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2024-12-17 09:59:56 +0100 |
commit | e7001022c75b3a818356378bb53bbfe5129a10fe (patch) | |
tree | 5cb23bd1fb1ff92f6ee99aed94629525de42ab45 /apps | |
parent | f16d0478084ca17551a0bc3242e48b633ff23a24 (diff) | |
download | nextcloud-server-e7001022c75b3a818356378bb53bbfe5129a10fe.tar.gz nextcloud-server-e7001022c75b3a818356378bb53bbfe5129a10fe.zip |
feat(files): add opendetails param and file list up/down keyboard shortcut
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/actions/sidebarAction.spec.ts | 4 | ||||
-rw-r--r-- | apps/files/src/actions/sidebarAction.ts | 9 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 172 | ||||
-rw-r--r-- | apps/files/src/composables/useRouteParameters.ts | 8 | ||||
-rw-r--r-- | apps/files/src/store/active.ts | 77 | ||||
-rw-r--r-- | apps/files/src/store/dragging.ts | 3 | ||||
-rw-r--r-- | apps/files/src/types.ts | 10 |
7 files changed, 234 insertions, 49 deletions
diff --git a/apps/files/src/actions/sidebarAction.spec.ts b/apps/files/src/actions/sidebarAction.spec.ts index 1f1e81dbeaf..75ed8c97b47 100644 --- a/apps/files/src/actions/sidebarAction.spec.ts +++ b/apps/files/src/actions/sidebarAction.spec.ts @@ -130,7 +130,7 @@ describe('Open sidebar action exec tests', () => { expect(goToRouteMock).toBeCalledWith( null, { view: view.id, fileid: '1' }, - { dir: '/' }, + { dir: '/', opendetails: 'true' }, true, ) }) @@ -159,7 +159,7 @@ describe('Open sidebar action exec tests', () => { expect(goToRouteMock).toBeCalledWith( null, { view: view.id, fileid: '1' }, - { dir: '/' }, + { dir: '/', opendetails: 'true' }, true, ) }) diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts index a951de1db97..0b8ad91741e 100644 --- a/apps/files/src/actions/sidebarAction.ts +++ b/apps/files/src/actions/sidebarAction.ts @@ -44,6 +44,11 @@ export const action = new FileAction({ async exec(node: Node, view: View, dir: string) { try { + // If the sidebar is already open for the current file, do nothing + if (window.OCA.Files.Sidebar.file === node.path) { + logger.debug('Sidebar already open for this file', { node }) + return null + } // Open sidebar and set active tab to sharing by default window.OCA.Files.Sidebar.setActiveTab('sharing') @@ -51,10 +56,10 @@ export const action = new FileAction({ await window.OCA.Files.Sidebar.open(node.path) // Silently update current fileid - window.OCP.Files.Router.goToRoute( + window.OCP?.Files?.Router?.goToRoute( null, { view: view.id, fileid: String(node.fileid) }, - { ...window.OCP.Files.Router.query, dir }, + { ...window.OCP.Files.Router.query, dir, opendetails: 'true' }, true, ) diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 81c4c5ac666..6df059f6143 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -12,7 +12,6 @@ isMtimeAvailable, isSizeAvailable, nodes, - fileListWidth, }" :scroll-to-index="scrollToIndex" :caption="caption"> @@ -58,32 +57,34 @@ </template> <script lang="ts"> -import type { Node as NcNode } from '@nextcloud/files' import type { ComponentPublicInstance, PropType } from 'vue' +import type { Node as NcNode } from '@nextcloud/files' import type { UserConfig } from '../types' +import { defineComponent } from 'vue' import { getFileListHeaders, Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files' import { showError } from '@nextcloud/dialogs' -import { translate as t } from '@nextcloud/l10n' import { subscribe, unsubscribe } from '@nextcloud/event-bus' -import { defineComponent } from 'vue' +import { translate as t } from '@nextcloud/l10n' +import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js' import { action as sidebarAction } from '../actions/sidebarAction.ts' +import { getSummaryFor } from '../utils/fileUtils' +import { useActiveStore } from '../store/active.ts' import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useRouteParameters } from '../composables/useRouteParameters.ts' -import { getSummaryFor } from '../utils/fileUtils' import { useSelectionStore } from '../store/selection.js' import { useUserConfigStore } from '../store/userconfig.ts' import FileEntry from './FileEntry.vue' import FileEntryGrid from './FileEntryGrid.vue' +import FileListFilters from './FileListFilters.vue' import FilesListHeader from './FilesListHeader.vue' import FilesListTableFooter from './FilesListTableFooter.vue' import FilesListTableHeader from './FilesListTableHeader.vue' -import VirtualList from './VirtualList.vue' -import logger from '../logger.ts' import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue' -import FileListFilters from './FileListFilters.vue' +import logger from '../logger.ts' +import VirtualList from './VirtualList.vue' export default defineComponent({ name: 'FilesListVirtual', @@ -113,18 +114,24 @@ export default defineComponent({ }, setup() { - const userConfigStore = useUserConfigStore() + const activeStore = useActiveStore() const selectionStore = useSelectionStore() + const userConfigStore = useUserConfigStore() + const fileListWidth = useFileListWidth() - const { fileId, openFile } = useRouteParameters() + const { fileId, openDetails, openFile } = useRouteParameters() return { fileId, fileListWidth, + openDetails, openFile, - userConfigStore, + activeStore, selectionStore, + userConfigStore, + + t, } }, @@ -215,12 +222,20 @@ export default defineComponent({ handler() { // wait for scrolling and updating the actions to settle this.$nextTick(() => { - if (this.fileId) { - if (this.openFile) { - this.handleOpenFile(this.fileId) - } else { - this.unselectFile() - } + if (this.fileId && this.openFile) { + this.handleOpenFile(this.fileId) + } + }) + }, + immediate: true, + }, + + openDetails: { + handler() { + // wait for scrolling and updating the actions to settle + this.$nextTick(() => { + if (this.fileId && this.openDetails) { + this.openSidebarForFile(this.fileId) } }) }, @@ -228,39 +243,39 @@ export default defineComponent({ }, }, + created() { + useHotKey('Escape', this.unselectFile, { + stop: true, + prevent: true, + }) + + useHotKey(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'], this.onKeyDown, { + stop: true, + prevent: true, + }) + }, + mounted() { // Add events on parent to cover both the table and DragAndDrop notice const mainContent = window.document.querySelector('main.app-content') as HTMLElement mainContent.addEventListener('dragover', this.onDragOver) - - subscribe('files:sidebar:closed', this.unselectFile) - - // If the file list is mounted with a fileId specified - // then we need to open the sidebar initially - if (this.fileId) { - this.openSidebarForFile(this.fileId) - } + subscribe('files:sidebar:closed', this.onSidebarClosed) }, beforeDestroy() { const mainContent = window.document.querySelector('main.app-content') as HTMLElement mainContent.removeEventListener('dragover', this.onDragOver) - - unsubscribe('files:sidebar:closed', this.unselectFile) + unsubscribe('files:sidebar:closed', this.onSidebarClosed) }, methods: { - // Open the file sidebar if we have the room for it - // but don't open the sidebar for the current folder openSidebarForFile(fileId) { - if (document.documentElement.clientWidth > 1024 && this.currentFolder.fileid !== fileId) { - // Open the sidebar for the given URL fileid - // iif we just loaded the app. - const node = this.nodes.find(n => n.fileid === fileId) as NcNode - if (node && sidebarAction?.enabled?.([node], this.currentView)) { - logger.debug('Opening sidebar on file ' + node.path, { node }) - sidebarAction.exec(node, this.currentView, this.currentFolder.path) - } + // Open the sidebar for the given URL fileid + // iif we just loaded the app. + const node = this.nodes.find(n => n.fileid === fileId) as NcNode + if (node && sidebarAction?.enabled?.([node], this.currentView)) { + logger.debug('Opening sidebar on file ' + node.path, { node }) + sidebarAction.exec(node, this.currentView, this.currentFolder.path) } }, @@ -273,19 +288,39 @@ export default defineComponent({ const index = this.nodes.findIndex(node => node.fileid === fileId) if (warn && index === -1 && fileId !== this.currentFolder.fileid) { - showError(this.t('files', 'File not found')) + showError(t('files', 'File not found')) } + this.scrollToIndex = Math.max(0, index) } }, + /** + * Unselect the current file and clear open parameters from the URL + */ unselectFile() { - // If the Sidebar is closed and if openFile is false, remove the file id from the URL - if (!this.openFile && OCA.Files.Sidebar.file === '') { + const query = { ...this.$route.query } + delete query.openfile + delete query.opendetails + + this.activeStore.clearActiveNode() + window.OCP.Files.Router.goToRoute( + null, + { ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') }, + query, + true, + ) + }, + + // When sidebar is closed, we remove the openDetails parameter from the URL + onSidebarClosed() { + if (this.openDetails) { + const query = { ...this.$route.query } + delete query.opendetails window.OCP.Files.Router.goToRoute( null, - { ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') }, - this.$route.query, + this.$route.params, + query, ) } }, @@ -348,7 +383,58 @@ export default defineComponent({ } }, - t, + onKeyDown(event: KeyboardEvent) { + // Up and down arrow keys + if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + const columnCount = this.$refs.table?.columnCount ?? 1 + const index = this.nodes.findIndex(node => node.fileid === this.fileId) ?? 0 + const nextIndex = event.key === 'ArrowUp' ? index - columnCount : index + columnCount + if (nextIndex < 0 || nextIndex >= this.nodes.length) { + return + } + + const nextNode = this.nodes[nextIndex] + + if (nextNode && nextNode?.fileid) { + this.setActiveNode(nextNode) + } + } + + // if grid mode, left and right arrow keys + if (this.userConfig.grid_view && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { + const index = this.nodes.findIndex(node => node.fileid === this.fileId) ?? 0 + const nextIndex = event.key === 'ArrowLeft' ? index - 1 : index + 1 + if (nextIndex < 0 || nextIndex >= this.nodes.length) { + return + } + + const nextNode = this.nodes[nextIndex] + + if (nextNode && nextNode?.fileid) { + this.setActiveNode(nextNode) + } + } + }, + + setActiveNode(node: NcNode & { fileid: number }) { + logger.debug('Navigating to file ' + node.path, { node, fileid: node.fileid }) + this.scrollToFile(node.fileid) + + // Remove openfile and opendetails from the URL + const query = { ...this.$route.query } + delete query.openfile + delete query.opendetails + + this.activeStore.setActiveNode(node) + + // Silent update of the URL + window.OCP.Files.Router.goToRoute( + null, + { ...this.$route.params, fileid: String(node.fileid) }, + query, + true, + ) + }, }, }) </script> diff --git a/apps/files/src/composables/useRouteParameters.ts b/apps/files/src/composables/useRouteParameters.ts index abf14614fb7..dbb8ca7f081 100644 --- a/apps/files/src/composables/useRouteParameters.ts +++ b/apps/files/src/composables/useRouteParameters.ts @@ -37,6 +37,11 @@ export function useRouteParameters() { () => 'openfile' in route.query && (typeof route.query.openfile !== 'string' || route.query.openfile.toLocaleLowerCase() !== 'false'), ) + const openDetails = computed<boolean>( + // if `opendetails` is set it is considered truthy, but allow to explicitly set it to 'false' + () => 'opendetails' in route.query && (typeof route.query.opendetails !== 'string' || route.query.opendetails.toLocaleLowerCase() !== 'false'), + ) + return { /** Path of currently open directory */ directory, @@ -46,5 +51,8 @@ export function useRouteParameters() { /** Should the active node should be opened (`openFile` route param) */ openFile, + + /** Should the details sidebar be shown (`openDetails` route param) */ + openDetails, } } diff --git a/apps/files/src/store/active.ts b/apps/files/src/store/active.ts new file mode 100644 index 00000000000..2efb823b232 --- /dev/null +++ b/apps/files/src/store/active.ts @@ -0,0 +1,77 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ActiveStore } from '../types.ts' +import type { FileAction, Node, View } from '@nextcloud/files' + +import { defineStore } from 'pinia' +import { getNavigation } from '@nextcloud/files' +import { subscribe } from '@nextcloud/event-bus' + +import logger from '../logger.ts' +import type { set } from 'lodash' + +export const useActiveStore = function(...args) { + const store = defineStore('active', { + state: () => ({ + _initialized: false, + activeNode: null, + activeView: null, + activeAction: null, + } as ActiveStore), + + actions: { + setActiveNode(node: Node) { + if (!node) { + throw new Error('Use clearActiveNode to clear the active node') + } + logger.debug('Setting active node', { node }) + this.activeNode = node + }, + + clearActiveNode() { + this.activeNode = null + }, + + onDeletedNode(node: Node) { + if (this.activeNode && this.activeNode.source === node.source) { + this.clearActiveNode() + } + }, + + setActiveAction(action: FileAction) { + this.activeAction = action + }, + + clearActiveAction() { + this.activeAction = null + }, + + onChangedView(view: View|null = null) { + logger.debug('Setting active view', { view }) + this.activeView = view + this.clearActiveNode() + }, + }, + }) + + const activeStore = store(...args) + const navigation = getNavigation() + + // Make sure we only register the listeners once + if (!activeStore._initialized) { + subscribe('files:node:deleted', activeStore.onDeletedNode) + + activeStore._initialized = true + activeStore.onChangedView(navigation.active) + + // Or you can react to changes of the current active view + navigation.addEventListener('updateActive', (event) => { + activeStore.onChangedView(event.detail) + }) + } + + return activeStore +} diff --git a/apps/files/src/store/dragging.ts b/apps/files/src/store/dragging.ts index 667c6fe67a7..f5c20095cca 100644 --- a/apps/files/src/store/dragging.ts +++ b/apps/files/src/store/dragging.ts @@ -2,9 +2,10 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { DragAndDropStore, FileSource } from '../types' + import { defineStore } from 'pinia' import Vue from 'vue' -import type { DragAndDropStore, FileSource } from '../types' export const useDragAndDropStore = defineStore('dragging', { state: () => ({ diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 39f0ed0865f..673cb06e182 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Folder, Node } from '@nextcloud/files' +import type { FileAction, Folder, Node, View } from '@nextcloud/files' import type { Upload } from '@nextcloud/upload' // Global definitions @@ -95,6 +95,14 @@ export interface DragAndDropStore { dragging: FileSource[] } +// Active node store +export interface ActiveStore { + _initialized: boolean + activeNode: Node|null + activeView: View|null + activeAction: FileAction|null +} + export interface TemplateFile { app: string label: string |