diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php
index c39719ae8ed..be32dce0d63 100644
--- a/apps/files/lib/Service/UserConfig.php
+++ b/apps/files/lib/Service/UserConfig.php
@@ -47,6 +47,12 @@ class UserConfig {
'default' => true,
'allowed' => [true, false],
],
+ [
+ // Whether to show the files list in grid view or not
+ 'key' => 'grid_view',
+ 'default' => false,
+ 'allowed' => [true, false],
+ ],
];
protected IConfig $config;
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index f1606d218c2..adfaab8cc9a 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -37,124 +37,41 @@
-
-
-
- |
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
|
-
-
-
-
-
-
-
-
-
-
-
- {{ actionDisplayName(action) }}
-
-
- |
+ :files-list-width="filesListWidth"
+ :loading.sync="loading"
+ :opened.sync="openedMenu"
+ :source="source"
+ :visible="visible" />
-
- |
import type { PropType } from 'vue'
-import { emit, subscribe } from '@nextcloud/event-bus'
import { extname, join } from 'path'
-import { generateUrl } from '@nextcloud/router'
-import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File as NcFile, FileAction, NodeStatus, Node } from '@nextcloud/files'
+import { FileType, formatFileSize, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files'
import { getUploader } from '@nextcloud/upload'
-import { showError, showSuccess } from '@nextcloud/dialogs'
+import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
-import { Type as ShareType } from '@nextcloud/sharing'
import { vOnClickOutside } from '@vueuse/components'
-import axios from '@nextcloud/axios'
import moment from '@nextcloud/moment'
import Vue from 'vue'
-import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue'
-import FileIcon from 'vue-material-design-icons/File.vue'
-import FolderIcon from 'vue-material-design-icons/Folder.vue'
-import FolderOpenIcon from 'vue-material-design-icons/FolderOpen.vue'
-import KeyIcon from 'vue-material-design-icons/Key.vue'
-import TagIcon from 'vue-material-design-icons/Tag.vue'
-import LinkIcon from 'vue-material-design-icons/Link.vue'
-import NetworkIcon from 'vue-material-design-icons/Network.vue'
-import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
-
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { getDragAndDropPreview } from '../utils/dragUtils.ts'
import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction.ts'
-import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts'
import { hashCode } from '../utils/hashUtils.ts'
+import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
-import { useKeyboardStore } from '../store/keyboard.ts'
import { useRenamingStore } from '../store/renaming.ts'
import { useSelectionStore } from '../store/selection.ts'
-import { useUserConfigStore } from '../store/userconfig.ts'
import CustomElementRender from './CustomElementRender.vue'
-import FavoriteIcon from './FavoriteIcon.vue'
+import FileEntryActions from './FileEntry/FileEntryActions.vue'
+import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
+import FileEntryName from './FileEntry/FileEntryName.vue'
+import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
import logger from '../logger.js'
-import { loadState } from '@nextcloud/initial-state'
-
-// The registered actions list
-const actions = getFileActions()
Vue.directive('onClickOutside', vOnClickOutside)
-const forbiddenCharacters = loadState('files', 'forbiddenCharacters', '') as string
-
export default Vue.extend({
name: 'FileEntry',
components: {
- AccountGroupIcon,
- AccountPlusIcon,
CustomElementRender,
- FavoriteIcon,
- FileIcon,
- FolderIcon,
- FolderOpenIcon,
- KeyIcon,
- LinkIcon,
- NcActionButton,
- NcActions,
- NcCheckboxRadioSwitch,
- NcIconSvgWrapper,
- NcLoadingIcon,
- NcTextField,
- NetworkIcon,
- TagIcon,
+ FileEntryActions,
+ FileEntryCheckbox,
+ FileEntryName,
+ FileEntryPreview,
},
props: {
@@ -282,10 +162,6 @@ export default Vue.extend({
type: [Folder, NcFile, Node] as PropType,
required: true,
},
- index: {
- type: Number,
- required: true,
- },
nodes: {
type: Array as PropType,
required: true,
@@ -294,48 +170,41 @@ export default Vue.extend({
type: Number,
default: 0,
},
+ compact: {
+ type: Boolean,
+ default: false,
+ },
},
setup() {
const actionsMenuStore = useActionsMenuStore()
const draggingStore = useDragAndDropStore()
const filesStore = useFilesStore()
- const keyboardStore = useKeyboardStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
- const userConfigStore = useUserConfigStore()
return {
actionsMenuStore,
draggingStore,
filesStore,
- keyboardStore,
renamingStore,
selectionStore,
- userConfigStore,
}
},
data() {
return {
- backgroundFailed: undefined,
loading: '',
dragover: false,
-
- NodeStatus,
}
},
computed: {
- userConfig() {
- return this.userConfigStore.userConfig
- },
-
- currentView() {
- return this.$navigation.active
+ currentView(): View {
+ return this.$navigation.active as View
},
columns() {
// Hide columns if the list is too small
- if (this.filesListWidth < 512) {
+ if (this.filesListWidth < 512 || this.compact) {
return []
}
return this.currentView?.columns || []
@@ -351,6 +220,12 @@ export default Vue.extend({
fileid() {
return this.source?.fileid?.toString?.()
},
+ uniqueId() {
+ return hashCode(this.source.source)
+ },
+ isLoading() {
+ return this.source.status === NodeStatus.LOADING
+ },
extension() {
if (this.source.attributes?.displayName) {
@@ -418,73 +293,6 @@ export default Vue.extend({
return ''
},
- folderOverlay() {
- if (this.source.type !== FileType.Folder) {
- return null
- }
-
- // Encrypted folders
- if (this.source?.attributes?.['is-encrypted'] === 1) {
- return KeyIcon
- }
-
- // System tags
- if (this.source?.attributes?.['is-tag']) {
- return TagIcon
- }
-
- // Link and mail shared folders
- const shareTypes = Object.values(this.source?.attributes?.['share-types'] || {}).flat() as number[]
- if (shareTypes.some(type => type === ShareType.SHARE_TYPE_LINK || type === ShareType.SHARE_TYPE_EMAIL)) {
- return LinkIcon
- }
-
- // Shared folders
- if (shareTypes.length > 0) {
- return AccountPlusIcon
- }
-
- switch (this.source?.attributes?.['mount-type']) {
- case 'external':
- case 'external-session':
- return NetworkIcon
- case 'group':
- return AccountGroupIcon
- }
-
- return null
- },
-
- linkTo() {
- if (this.source.attributes.failed) {
- return {
- title: t('files', 'This node is unavailable'),
- is: 'span',
- }
- }
-
- if (this.enabledDefaultActions.length > 0) {
- const action = this.enabledDefaultActions[0]
- const displayName = action.displayName([this.source], this.currentView)
- return {
- title: displayName,
- role: 'button',
- }
- }
-
- if (this.source?.permissions & Permission.READ) {
- return {
- download: this.source.basename,
- href: this.source.source,
- title: t('files', 'Download file {name}', { name: this.displayName }),
- }
- }
-
- return {
- is: 'span',
- }
- },
-
draggingFiles() {
return this.draggingStore.dragging
},
@@ -495,124 +303,12 @@ export default Vue.extend({
return this.selectedFiles.includes(this.fileid)
},
- cropPreviews() {
- return this.userConfig.crop_image_previews
- },
- previewUrl() {
- if (this.source.type === FileType.Folder) {
- return null
- }
-
- if (this.backgroundFailed === true) {
- return null
- }
-
- try {
- const previewUrl = this.source.attributes.previewUrl
- || generateUrl('/core/preview?fileId={fileid}', {
- fileid: this.fileid,
- })
- const url = new URL(window.location.origin + previewUrl)
-
- // Request tiny previews
- url.searchParams.set('x', '32')
- url.searchParams.set('y', '32')
- url.searchParams.set('mimeFallback', 'true')
-
- // Handle cropping
- url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
- return url.href
- } catch (e) {
- return null
- }
- },
-
- // Sorted actions that are enabled for this node
- enabledActions() {
- if (this.source.attributes.failed) {
- return []
- }
-
- return actions
- .filter(action => !action.enabled || action.enabled([this.source], this.currentView))
- .sort((a, b) => (a.order || 0) - (b.order || 0))
- },
-
- // Enabled action that are displayed inline
- enabledInlineActions() {
- if (this.filesListWidth < 768) {
- return []
- }
- return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
- },
-
- // Enabled action that are displayed inline with a custom render function
- enabledRenderActions() {
- if (!this.visible) {
- return []
- }
- return this.enabledActions.filter(action => typeof action.renderInline === 'function')
- },
-
- // Default actions
- enabledDefaultActions() {
- return this.enabledActions.filter(action => !!action?.default)
- },
-
- // Actions shown in the menu
- enabledMenuActions() {
- return [
- // Showing inline first for the NcActions inline prop
- ...this.enabledInlineActions,
- // Then the rest
- ...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'),
- ].filter((value, index, self) => {
- // Then we filter duplicates to prevent inline actions to be shown twice
- return index === self.findIndex(action => action.id === value.id)
- })
- },
- openedMenu: {
- get() {
- return this.actionsMenuStore.opened === this.uniqueId
- },
- set(opened) {
- this.actionsMenuStore.opened = opened ? this.uniqueId : null
- },
- },
-
- uniqueId() {
- return hashCode(this.source.source)
- },
-
- isFavorite() {
- return this.source.attributes.favorite === 1
- },
- isLoading() {
- return this.source.status === NodeStatus.LOADING
- },
-
- renameLabel() {
- const matchLabel: Record = {
- [FileType.File]: t('files', 'File name'),
- [FileType.Folder]: t('files', 'Folder name'),
- }
- return matchLabel[this.source.type]
- },
-
isRenaming() {
return this.renamingStore.renamingNode === this.source
},
isRenamingSmallScreen() {
return this.isRenaming && this.filesListWidth < 512
},
- newName: {
- get() {
- return this.renamingStore.newName
- },
- set(newName) {
- this.renamingStore.newName = newName
- },
- },
isActive() {
return this.fileid === this.currentFileId?.toString?.()
@@ -643,6 +339,15 @@ export default Vue.extend({
return (this.source.permissions & Permission.CREATE) !== 0
},
+
+ openedMenu: {
+ get() {
+ return this.actionsMenuStore.opened === this.uniqueId
+ },
+ set(opened) {
+ this.actionsMenuStore.opened = opened ? this.uniqueId : null
+ },
+ },
},
watch: {
@@ -653,17 +358,6 @@ export default Vue.extend({
source() {
this.resetState()
},
-
- /**
- * If renaming starts, select the file name
- * in the input, without the extension.
- * @param renaming
- */
- isRenaming(renaming) {
- if (renaming) {
- this.startRenaming()
- }
- },
},
beforeDestroy() {
@@ -675,96 +369,12 @@ export default Vue.extend({
// Reset loading state
this.loading = ''
- // Reset background state
- this.backgroundFailed = undefined
- if (this.$refs.previewImg) {
- this.$refs.previewImg.src = ''
- }
+ this.$refs.preview.reset()
// Close menu
this.openedMenu = false
},
- async onActionClick(action) {
- const displayName = action.displayName([this.source], this.currentView)
- try {
- // Set the loading marker
- this.loading = action.id
- Vue.set(this.source, 'status', NodeStatus.LOADING)
-
- const success = await action.exec(this.source, this.currentView, this.currentDir)
-
- // If the action returns null, we stay silent
- if (success === null) {
- return
- }
-
- if (success) {
- showSuccess(t('files', '"{displayName}" action executed successfully', { displayName }))
- return
- }
- showError(t('files', '"{displayName}" action failed', { displayName }))
- } catch (e) {
- logger.error('Error while executing action', { action, e })
- showError(t('files', '"{displayName}" action failed', { displayName }))
- } finally {
- // Reset the loading marker
- this.loading = ''
- Vue.set(this.source, 'status', undefined)
- }
- },
- execDefaultAction(event) {
- if (this.enabledDefaultActions.length > 0) {
- event.preventDefault()
- event.stopPropagation()
- // Execute the first default action if any
- this.enabledDefaultActions[0].exec(this.source, this.currentView, this.currentDir)
- }
- },
-
- openDetailsIfAvailable(event) {
- event.preventDefault()
- event.stopPropagation()
- if (sidebarAction?.enabled?.([this.source], this.currentView)) {
- sidebarAction.exec(this.source, this.currentView, this.currentDir)
- }
- },
-
- onSelectionChange(selected: boolean) {
- const newSelectedIndex = this.index
- const lastSelectedIndex = this.selectionStore.lastSelectedIndex
-
- // Get the last selected and select all files in between
- if (this.keyboardStore?.shiftKey && lastSelectedIndex !== null) {
- const isAlreadySelected = this.selectedFiles.includes(this.fileid)
-
- const start = Math.min(newSelectedIndex, lastSelectedIndex)
- const end = Math.max(lastSelectedIndex, newSelectedIndex)
-
- const lastSelection = this.selectionStore.lastSelection
- const filesToSelect = this.nodes
- .map(file => file.fileid?.toString?.())
- .slice(start, end + 1)
-
- // If already selected, update the new selection _without_ the current file
- const selection = [...lastSelection, ...filesToSelect]
- .filter(fileid => !isAlreadySelected || fileid !== this.fileid)
-
- logger.debug('Shift key pressed, selecting all files in between', { start, end, filesToSelect, isAlreadySelected })
- // Keep previous lastSelectedIndex to be use for further shift selections
- this.selectionStore.set(selection)
- return
- }
-
- const selection = selected
- ? [...this.selectedFiles, this.fileid]
- : this.selectedFiles.filter(fileid => fileid !== this.fileid)
-
- logger.debug('Updating selection', { selection })
- this.selectionStore.set(selection)
- this.selectionStore.setLastIndex(newSelectedIndex)
- },
-
// Open the actions menu on right click
onRightClick(event) {
// If already opened, fallback to default browser
@@ -781,166 +391,16 @@ export default Vue.extend({
event.stopPropagation()
},
- /**
- * Check if the file name is valid and update the
- * input validity using browser's native validation.
- * @param event the keyup event
- */
- checkInputValidity(event?: KeyboardEvent) {
- const input = event.target as HTMLInputElement
- const newName = this.newName.trim?.() || ''
- logger.debug('Checking input validity', { newName })
- try {
- this.isFileNameValid(newName)
- input.setCustomValidity('')
- input.title = ''
- } catch (e) {
- input.setCustomValidity(e.message)
- input.title = e.message
- } finally {
- input.reportValidity()
- }
- },
- isFileNameValid(name) {
- const trimmedName = name.trim()
- if (trimmedName === '.' || trimmedName === '..') {
- throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
- } else if (trimmedName.length === 0) {
- throw new Error(t('files', 'File name cannot be empty.'))
- } else if (trimmedName.indexOf('/') !== -1) {
- throw new Error(t('files', '"/" is not allowed inside a file name.'))
- } else if (trimmedName.match(OC.config.blacklist_files_regex)) {
- throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
- } else if (this.checkIfNodeExists(name)) {
- throw new Error(t('files', '{newName} already exists.', { newName: name }))
- }
-
- const toCheck = trimmedName.split('')
- toCheck.forEach(char => {
- if (forbiddenCharacters.indexOf(char) !== -1) {
- throw new Error(this.t('files', '"{char}" is not allowed inside a file name.', { char }))
- }
- })
-
- return true
- },
- checkIfNodeExists(name) {
- return this.nodes.find(node => node.basename === name && node !== this.source)
+ execDefaultAction(...args) {
+ this.$refs.actions.execDefaultAction(...args)
},
- startRenaming() {
- this.$nextTick(() => {
- // Using split to get the true string length
- const extLength = (this.source.extension || '').split('').length
- const length = this.source.basename.split('').length - extLength
- const input = this.$refs.renameInput?.$refs?.inputField?.$refs?.input
- if (!input) {
- logger.error('Could not find the rename input')
- return
- }
- input.setSelectionRange(0, length)
- input.focus()
-
- // Trigger a keyup event to update the input validity
- input.dispatchEvent(new Event('keyup'))
- })
- },
- stopRenaming() {
- if (!this.isRenaming) {
- return
+ openDetailsIfAvailable(event) {
+ event.preventDefault()
+ event.stopPropagation()
+ if (sidebarAction?.enabled?.([this.source], this.currentView)) {
+ sidebarAction.exec(this.source, this.currentView, this.currentDir)
}
-
- // Reset the renaming store
- this.renamingStore.$reset()
- },
-
- // Rename and move the file
- async onRename() {
- const oldName = this.source.basename
- const oldEncodedSource = this.source.encodedSource
- const newName = this.newName.trim?.() || ''
- if (newName === '') {
- showError(t('files', 'Name cannot be empty'))
- return
- }
-
- if (oldName === newName) {
- this.stopRenaming()
- return
- }
-
- // Checking if already exists
- if (this.checkIfNodeExists(newName)) {
- showError(t('files', 'Another entry with the same name already exists'))
- return
- }
-
- // Set loading state
- this.loading = 'renaming'
- Vue.set(this.source, 'status', NodeStatus.LOADING)
-
- // Update node
- this.source.rename(newName)
-
- logger.debug('Moving file to', { destination: this.source.encodedSource, oldEncodedSource })
- try {
- await axios({
- method: 'MOVE',
- url: oldEncodedSource,
- headers: {
- Destination: this.source.encodedSource,
- },
- })
-
- // Success š
- emit('files:node:updated', this.source)
- emit('files:node:renamed', this.source)
- showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
-
- // Reset the renaming store
- this.stopRenaming()
- this.$nextTick(() => {
- this.$refs.basename.focus()
- })
- } catch (error) {
- logger.error('Error while renaming file', { error })
- this.source.rename(oldName)
- this.$refs.renameInput.focus()
-
- // TODO: 409 means current folder does not exist, redirect ?
- if (error?.response?.status === 404) {
- showError(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
- return
- } else if (error?.response?.status === 412) {
- showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
- return
- }
-
- // Unknown error
- showError(t('files', 'Could not rename "{oldName}"', { oldName }))
- } finally {
- this.loading = false
- Vue.set(this.source, 'status', undefined)
- }
- },
-
- /**
- * Making this a function in case the files-list
- * reference changes in the future. That way we're
- * sure there is one at the time we call it.
- */
- getBoundariesElement() {
- return document.querySelector('.app-content > .files-list')
- },
-
- actionDisplayName(action: FileAction) {
- if (this.filesListWidth < 768 && action.inline && typeof action.title === 'function') {
- // if an inline action is rendered in the menu for
- // lack of space we use the title first if defined
- const title = action.title([this.source], this.currentView)
- if (title) return title
- }
- return action.displayName([this.source], this.currentView)
},
onDragOver(event: DragEvent) {
@@ -1057,43 +517,3 @@ export default Vue.extend({
},
})
-
-
-
-
diff --git a/apps/files/src/components/FavoriteIcon.vue b/apps/files/src/components/FileEntry/FavoriteIcon.vue
similarity index 100%
rename from apps/files/src/components/FavoriteIcon.vue
rename to apps/files/src/components/FileEntry/FavoriteIcon.vue
diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue
new file mode 100644
index 00000000000..bcb4abef10f
--- /dev/null
+++ b/apps/files/src/components/FileEntry/FileEntryActions.vue
@@ -0,0 +1,247 @@
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ {{ actionDisplayName(action) }}
+
+
+ |
+
+
+
diff --git a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
new file mode 100644
index 00000000000..961e4bf2266
--- /dev/null
+++ b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+ |
+
+
+
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue
new file mode 100644
index 00000000000..e54eacdbe9e
--- /dev/null
+++ b/apps/files/src/components/FileEntry/FileEntryName.vue
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/files/src/components/FileEntry/FileEntryPreview.vue b/apps/files/src/components/FileEntry/FileEntryPreview.vue
new file mode 100644
index 00000000000..076319428e5
--- /dev/null
+++ b/apps/files/src/components/FileEntry/FileEntryPreview.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/files/src/components/FileEntryGrid.vue b/apps/files/src/components/FileEntryGrid.vue
new file mode 100644
index 00000000000..d8c45cb2ce8
--- /dev/null
+++ b/apps/files/src/components/FileEntryGrid.vue
@@ -0,0 +1,414 @@
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/apps/files/src/components/FilesListTableFooter.vue b/apps/files/src/components/FilesListTableFooter.vue
index 3e8f49deace..bca4604d57d 100644
--- a/apps/files/src/components/FilesListTableFooter.vue
+++ b/apps/files/src/components/FilesListTableFooter.vue
@@ -159,17 +159,16 @@ export default Vue.extend({
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index c5ff9e663a3..ad8e67b0d4f 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -28,10 +28,10 @@
:style="{ height: dndNoticeHeight }" />
-import type { PropType } from 'vue'
import type { Node as NcNode } from '@nextcloud/files'
+import type { PropType } from 'vue'
+import type { UserConfig } from '../types.ts'
import { Fragment } from 'vue-frag'
import { getFileListHeaders, Folder, View, Permission } from '@nextcloud/files'
@@ -89,8 +90,10 @@ import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Vue from 'vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
+import { useUserConfigStore } from '../store/userconfig.ts'
import DragAndDropNotice from './DragAndDropNotice.vue'
import FileEntry from './FileEntry.vue'
+import FileEntryGrid from './FileEntryGrid.vue'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
@@ -129,9 +132,17 @@ export default Vue.extend({
},
},
+ setup() {
+ const userConfigStore = useUserConfigStore()
+ return {
+ userConfigStore,
+ }
+ },
+
data() {
return {
FileEntry,
+ FileEntryGrid,
headers: getFileListHeaders(),
scrollToIndex: 0,
dragover: false,
@@ -140,6 +151,10 @@ export default Vue.extend({
},
computed: {
+ userConfig(): UserConfig {
+ return this.userConfigStore.userConfig
+ },
+
files() {
return this.nodes.filter(node => node.type === 'file')
},
@@ -302,6 +317,14 @@ export default Vue.extend({
width: 100%;
// Necessary for virtual scrolling absolute
position: relative;
+
+ /* Hover effect on tbody lines only */
+ tr {
+ &:hover,
+ &:focus {
+ background-color: var(--color-background-dark);
+ }
+ }
}
// Before table and thead
@@ -340,6 +363,7 @@ export default Vue.extend({
user-select: none;
border-bottom: 1px solid var(--color-border);
user-select: none;
+ height: var(--row-height);
}
td, th {
@@ -465,10 +489,15 @@ export default Vue.extend({
width: var(--icon-preview-size);
height: var(--icon-preview-size);
border-radius: var(--border-radius);
- background-repeat: no-repeat;
// Center and contain the preview
- background-position: center;
- background-size: contain;
+ object-fit: contain;
+ object-position: center;
+
+ /* Preview not loaded animation effect */
+ &:not(.files-list__row-icon-preview--loaded) {
+ background: var(--color-loading-dark);
+ // animation: preview-gradient-fade 1.2s ease-in-out infinite;
+ }
}
&-favorite {
@@ -476,6 +505,16 @@ export default Vue.extend({
top: 0px;
right: -10px;
}
+
+ // Folder overlay
+ &-overlay {
+ position: absolute;
+ max-height: calc(var(--icon-preview-size) * 0.5);
+ max-width: calc(var(--icon-preview-size) * 0.5);
+ color: var(--color-main-background);
+ // better alignment with the folder icon
+ margin-top: 2px;
+ }
}
// Entry link
@@ -518,6 +557,8 @@ export default Vue.extend({
.files-list__row-name-ext {
color: var(--color-text-maxcontrast);
+ // always show the extension
+ overflow: visible;
}
}
@@ -541,6 +582,7 @@ export default Vue.extend({
}
.files-list__row-actions {
+ // take as much space as necessary
width: auto;
// Add margin to all cells after the actions
@@ -581,3 +623,91 @@ export default Vue.extend({
}
}
+
+
diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue
index ef824d7ba91..0dde37923a1 100644
--- a/apps/files/src/components/VirtualList.vue
+++ b/apps/files/src/components/VirtualList.vue
@@ -11,11 +11,14 @@
-
+
@@ -23,7 +26,6 @@
|