aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FilesListVirtual.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/components/FilesListVirtual.vue')
-rw-r--r--apps/files/src/components/FilesListVirtual.vue618
1 files changed, 453 insertions, 165 deletions
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index ed0096e9792..47b8ef19b19 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2023 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/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<VirtualList ref="table"
:data-component="userConfig.grid_view ? FileEntryGrid : FileEntry"
@@ -26,21 +9,28 @@
:data-sources="nodes"
:grid-mode="userConfig.grid_view"
:extra-props="{
+ isMimeAvailable,
isMtimeAvailable,
isSizeAvailable,
nodes,
- filesListWidth,
}"
:scroll-to-index="scrollToIndex"
:caption="caption">
+ <template #filters>
+ <FileListFilters />
+ </template>
+
<template v-if="!isNoneSelected" #header-overlay>
+ <span class="files-list__selected">
+ {{ n('files', '{count} selected', '{count} selected', selectedNodes.length, { count: selectedNodes.length }) }}
+ </span>
<FilesListTableHeaderActions :current-view="currentView"
:selected-nodes="selectedNodes" />
</template>
<template #before>
<!-- Headers -->
- <FilesListHeader v-for="header in sortedHeaders"
+ <FilesListHeader v-for="header in headers"
:key="header.id"
:current-folder="currentFolder"
:current-view="currentView"
@@ -51,15 +41,23 @@
<template #header>
<!-- Table header and sort buttons -->
<FilesListTableHeader ref="thead"
- :files-list-width="filesListWidth"
+ :files-list-width="fileListWidth"
+ :is-mime-available="isMimeAvailable"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes" />
</template>
+ <!-- Body replacement if no files are available -->
+ <template #empty>
+ <slot name="empty" />
+ </template>
+
<!-- Tfoot-->
<template #footer>
- <FilesListTableFooter :files-list-width="filesListWidth"
+ <FilesListTableFooter :current-view="currentView"
+ :files-list-width="fileListWidth"
+ :is-mime-available="isMimeAvailable"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes"
@@ -69,35 +67,40 @@
</template>
<script lang="ts">
-import type { Node as NcNode } from '@nextcloud/files'
-import type { PropType } from 'vue'
import type { UserConfig } from '../types'
+import type { Node as NcNode } from '@nextcloud/files'
+import type { ComponentPublicInstance, PropType } from 'vue'
-import { getFileListHeaders, Folder, View, getFileActions } from '@nextcloud/files'
+import { Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
-import { loadState } from '@nextcloud/initial-state'
-import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { n, t } from '@nextcloud/l10n'
+import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { defineComponent } from 'vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
-import { getSummaryFor } from '../utils/fileUtils'
+import { useActiveStore } from '../store/active.ts'
+import { useFileListHeaders } from '../composables/useFileListHeaders.ts'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
+import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useSelectionStore } from '../store/selection.js'
import { useUserConfigStore } from '../store/userconfig.ts'
+import logger from '../logger.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 filesListWidthMixin from '../mixins/filesListWidth.ts'
-import VirtualList from './VirtualList.vue'
-import logger from '../logger.js'
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
+import VirtualList from './VirtualList.vue'
export default defineComponent({
name: 'FilesListVirtual',
components: {
+ FileListFilters,
FilesListHeader,
FilesListTableFooter,
FilesListTableHeader,
@@ -105,10 +108,6 @@ export default defineComponent({
FilesListTableHeaderActions,
},
- mixins: [
- filesListWidthMixin,
- ],
-
props: {
currentView: {
type: View,
@@ -122,14 +121,33 @@ export default defineComponent({
type: Array as PropType<NcNode[]>,
required: true,
},
+ summary: {
+ type: String,
+ required: true,
+ },
},
setup() {
- const userConfigStore = useUserConfigStore()
+ const activeStore = useActiveStore()
const selectionStore = useSelectionStore()
+ const userConfigStore = useUserConfigStore()
+
+ const fileListWidth = useFileListWidth()
+ const { fileId, openDetails, openFile } = useRouteParameters()
+
return {
- userConfigStore,
+ fileId,
+ fileListWidth,
+ headers: useFileListHeaders(),
+ openDetails,
+ openFile,
+
+ activeStore,
selectionStore,
+ userConfigStore,
+
+ n,
+ t,
}
},
@@ -137,7 +155,6 @@ export default defineComponent({
return {
FileEntry,
FileEntryGrid,
- headers: getFileListHeaders(),
scrollToIndex: 0,
}
},
@@ -147,43 +164,53 @@ export default defineComponent({
return this.userConfigStore.userConfig
},
- fileId() {
- return parseInt(this.$route.params.fileid) || null
- },
-
- summary() {
- return getSummaryFor(this.nodes)
+ isMimeAvailable() {
+ if (!this.userConfig.show_mime_column) {
+ return false
+ }
+ // Hide mime column on narrow screens
+ if (this.fileListWidth < 1024) {
+ return false
+ }
+ return this.nodes.some(node => node.mime !== undefined || node.mime !== 'application/octet-stream')
},
-
isMtimeAvailable() {
// Hide mtime column on narrow screens
- if (this.filesListWidth < 768) {
+ if (this.fileListWidth < 768) {
return false
}
return this.nodes.some(node => node.mtime !== undefined)
},
isSizeAvailable() {
// Hide size column on narrow screens
- if (this.filesListWidth < 768) {
+ if (this.fileListWidth < 768) {
return false
}
- return this.nodes.some(node => node.attributes.size !== undefined)
+ return this.nodes.some(node => node.size !== undefined)
},
- sortedHeaders() {
- if (!this.currentFolder || !this.currentView) {
- return []
- }
+ cantUpload() {
+ return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) === 0
+ },
- return [...this.headers].sort((a, b) => a.order - b.order)
+ isQuotaExceeded() {
+ return this.currentFolder?.attributes?.['quota-available-bytes'] === 0
},
caption() {
const defaultCaption = t('files', 'List of files and folders.')
const viewCaption = this.currentView.caption || defaultCaption
+ const cantUploadCaption = this.cantUpload ? t('files', 'You do not have permission to upload or create files here.') : null
+ const quotaExceededCaption = this.isQuotaExceeded ? t('files', 'You have used your space quota and cannot upload files anymore.') : null
const sortableCaption = t('files', 'Column headers with buttons are sortable.')
const virtualListNote = t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.')
- return `${viewCaption}\n${sortableCaption}\n${virtualListNote}`
+ return [
+ viewCaption,
+ cantUploadCaption,
+ quotaExceededCaption,
+ sortableCaption,
+ virtualListNote,
+ ].filter(Boolean).join('\n')
},
selectedNodes() {
@@ -193,74 +220,179 @@ export default defineComponent({
isNoneSelected() {
return this.selectedNodes.length === 0
},
+
+ isEmpty() {
+ return this.nodes.length === 0
+ },
},
watch: {
- fileId(fileId) {
- this.scrollToFile(fileId, false)
+ // If nodes gets populated and we have a fileId,
+ // an openFile or openDetails, we fire the appropriate actions.
+ isEmpty() {
+ this.handleOpenQueries()
+ },
+ fileId() {
+ this.handleOpenQueries()
+ },
+ openFile() {
+ this.handleOpenQueries()
+ },
+ openDetails() {
+ this.handleOpenQueries()
},
},
+ 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)
-
- this.scrollToFile(this.fileId)
- this.openSidebarForFile(this.fileId)
- this.handleOpenFile()
+ 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.onSidebarClosed)
},
methods: {
- // Open the file sidebar if we have the room for it
- // but don't open the sidebar for the current folder
+ handleOpenQueries() {
+ // If the list is empty, or we don't have a fileId,
+ // there's nothing to be done.
+ if (this.isEmpty || !this.fileId) {
+ return
+ }
+
+ logger.debug('FilesListVirtual: checking for requested fileId, openFile or openDetails', {
+ nodes: this.nodes,
+ fileId: this.fileId,
+ openFile: this.openFile,
+ openDetails: this.openDetails,
+ })
+
+ if (this.openFile) {
+ this.handleOpenFile(this.fileId)
+ }
+
+ if (this.openDetails) {
+ this.openSidebarForFile(this.fileId)
+ }
+
+ if (this.fileId) {
+ this.scrollToFile(this.fileId, false)
+ }
+ },
+
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)
+ return
}
+ logger.warn(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node })
},
scrollToFile(fileId: number|null, warn = true) {
if (fileId) {
+ // Do not uselessly scroll to the top of the list.
+ if (fileId === this.currentFolder.fileid) {
+ return
+ }
+
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)
+ logger.debug('Scrolling to file ' + fileId, { fileId, index })
}
},
- handleOpenFile() {
- const openFileInfo = loadState('files', 'openFileInfo', {}) as ({ id?: number })
- if (openFileInfo === undefined) {
- return
+ /**
+ * Unselect the current file and clear open parameters from the URL
+ */
+ unselectFile() {
+ const query = { ...this.$route.query }
+ delete query.openfile
+ delete query.opendetails
+
+ this.activeStore.activeNode = undefined
+ 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,
+ query,
+ )
}
+ },
- const node = this.nodes.find(n => n.fileid === openFileInfo.id) as NcNode
+ /**
+ * Handle opening a file (e.g. by ?openfile=true)
+ * @param fileId File to open
+ */
+ async handleOpenFile(fileId: number) {
+ const node = this.nodes.find(n => n.fileid === fileId) as NcNode
if (node === undefined) {
return
}
- logger.debug('Opening file ' + node.path, { node })
- getFileActions()
- .filter(action => !action.enabled || action.enabled([node], this.currentView))
- .sort((a, b) => (a.order || 0) - (b.order || 0))
- .filter(action => !!action?.default)[0].exec(node, this.currentView, this.currentFolder.path)
- },
-
- getFileId(node) {
- return node.fileid
+ if (node.type === FileType.File) {
+ const defaultAction = getFileActions()
+ // Get only default actions (visible and hidden)
+ .filter((action) => !!action?.default)
+ // Find actions that are either always enabled or enabled for the current node
+ .filter((action) => !action.enabled || action.enabled([node], this.currentView))
+ .filter((action) => action.id !== 'download')
+ // Sort enabled default actions by order
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
+ // Get the first one
+ .at(0)
+
+ // Some file types do not have a default action (e.g. they can only be downloaded)
+ // So if there is an enabled default action, so execute it
+ if (defaultAction) {
+ logger.debug('Opening file ' + node.path, { node })
+ return await defaultAction.exec(node, this.currentView, this.currentFolder.path)
+ }
+ }
+ // The file is either a folder or has no default action other than downloading
+ // in this case we need to open the details instead and remove the route from the history
+ logger.debug('Ignore `openfile` query and replacing with `opendetails` for ' + node.path, { node })
+ window.OCP.Files.Router.goToRoute(
+ null,
+ this.$route.params,
+ { ...this.$route.query, openfile: undefined, opendetails: '' },
+ true, // silent update of the URL
+ )
},
onDragOver(event: DragEvent) {
@@ -275,41 +407,99 @@ export default defineComponent({
event.preventDefault()
event.stopPropagation()
- const tableTop = this.$refs.table.$el.getBoundingClientRect().top
- const tableBottom = tableTop + this.$refs.table.$el.getBoundingClientRect().height
+ const tableElement = (this.$refs.table as ComponentPublicInstance<typeof VirtualList>).$el
+ const tableTop = tableElement.getBoundingClientRect().top
+ const tableBottom = tableTop + tableElement.getBoundingClientRect().height
// If reaching top, scroll up. Using 100 because of the floating header
if (event.clientY < tableTop + 100) {
- this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop - 25
+ tableElement.scrollTop = tableElement.scrollTop - 25
return
}
// If reaching bottom, scroll down
if (event.clientY > tableBottom - 50) {
- this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop + 25
+ tableElement.scrollTop = tableElement.scrollTop + 25
}
},
- 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.activeNode = node
+
+ // Silent update of the URL
+ window.OCP.Files.Router.goToRoute(
+ null,
+ { ...this.$route.params, fileid: String(node.fileid) },
+ query,
+ true,
+ )
+ },
},
})
</script>
<style scoped lang="scss">
.files-list {
- --row-height: 55px;
+ --row-height: 44px;
--cell-margin: 14px;
--checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2);
--checkbox-size: 24px;
- --clickable-area: 44px;
- --icon-preview-size: 32px;
+ --clickable-area: var(--default-clickable-area);
+ --icon-preview-size: 24px;
- position: relative;
+ --fixed-block-start-position: var(--default-clickable-area);
+ display: flex;
+ flex-direction: column;
overflow: auto;
height: 100%;
will-change: scroll-position;
+ &:has(.file-list-filters__active) {
+ --fixed-block-start-position: calc(var(--default-clickable-area) + var(--default-grid-baseline) + var(--clickable-area-small));
+ }
+
& :deep() {
// Table head, body and footer
tbody {
@@ -337,24 +527,57 @@ export default defineComponent({
flex-direction: column;
}
+ .files-list__selected {
+ padding-inline-end: 12px;
+ white-space: nowrap;
+ }
+
.files-list__table {
display: block;
+
+ &.files-list__table--with-thead-overlay {
+ // Hide the table header below the overlay
+ margin-block-start: calc(-1 * var(--row-height));
+ }
+
+ // Visually hide the table when there are no files
+ &--hidden {
+ visibility: hidden;
+ z-index: -1;
+ opacity: 0;
+ }
}
- .files-list__thead-overlay {
- position: absolute;
+ .files-list__filters {
+ // Pinned on top when scrolling above table header
+ position: sticky;
top: 0;
- left: var(--row-height); // Save space for a row checkbox
- right: 0;
- z-index: 1000;
+ // ensure there is a background to hide the file list on scroll
+ background-color: var(--color-main-background);
+ z-index: 10;
+ // fixed the size
+ padding-inline: var(--row-height) var(--default-grid-baseline, 4px);
+ height: var(--fixed-block-start-position);
+ width: 100%;
+ }
+
+ .files-list__thead-overlay {
+ // Pinned on top when scrolling
+ position: sticky;
+ top: var(--fixed-block-start-position);
+ // Save space for a row checkbox
+ margin-inline-start: var(--row-height);
+ // More than .files-list__thead
+ z-index: 20;
display: flex;
align-items: center;
// Reuse row styles
background-color: var(--color-main-background);
- border-bottom: 1px solid var(--color-border);
+ border-block-end: 1px solid var(--color-border);
height: var(--row-height);
+ flex: 0 0 var(--row-height);
}
.files-list__thead,
@@ -363,7 +586,6 @@ export default defineComponent({
flex-direction: column;
width: 100%;
background-color: var(--color-main-background);
-
}
// Table header
@@ -371,12 +593,17 @@ export default defineComponent({
// Pinned on top when scrolling
position: sticky;
z-index: 10;
- top: 0;
+ top: var(--fixed-block-start-position);
}
- // Table footer
- .files-list__tfoot {
- min-height: 300px;
+ // Empty content
+ .files-list__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
}
tr {
@@ -384,8 +611,7 @@ export default defineComponent({
display: flex;
align-items: center;
width: 100%;
- user-select: none;
- border-bottom: 1px solid var(--color-border);
+ border-block-end: 1px solid var(--color-border);
box-sizing: border-box;
user-select: none;
height: var(--row-height);
@@ -395,7 +621,7 @@ export default defineComponent({
display: flex;
align-items: center;
flex: 0 0 auto;
- justify-content: left;
+ justify-content: start;
width: var(--row-height);
height: var(--row-height);
margin: 0;
@@ -417,8 +643,7 @@ export default defineComponent({
position: absolute;
display: block;
top: 0;
- left: 0;
- right: 0;
+ inset-inline: 0;
bottom: 0;
opacity: .1;
z-index: -1;
@@ -482,7 +707,7 @@ export default defineComponent({
width: var(--icon-preview-size);
height: 100%;
// Show same padding as the checkbox right padding for visual balance
- margin-right: var(--checkbox-padding);
+ margin-inline-end: var(--checkbox-padding);
color: var(--color-primary-element);
// Icon is also clickable
@@ -509,15 +734,31 @@ export default defineComponent({
}
}
- &-preview {
+ &-preview-container {
+ position: relative; // Needed for the blurshash to be positioned correctly
overflow: hidden;
width: var(--icon-preview-size);
height: var(--icon-preview-size);
border-radius: var(--border-radius);
+ }
+
+ &-blurhash {
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ height: 100%;
+ width: 100%;
+ object-fit: cover;
+ }
+
+ &-preview {
// Center and contain the preview
object-fit: contain;
object-position: center;
+ height: 100%;
+ width: 100%;
+
/* Preview not loaded animation effect */
&:not(.files-list__row-icon-preview--loaded) {
background: var(--color-loading-dark);
@@ -528,17 +769,17 @@ export default defineComponent({
&-favorite {
position: absolute;
top: 0px;
- right: -10px;
+ inset-inline-end: -10px;
}
// File and folder overlay
&-overlay {
position: absolute;
- max-height: calc(var(--icon-preview-size) * 0.5);
- max-width: calc(var(--icon-preview-size) * 0.5);
+ max-height: calc(var(--icon-preview-size) * 0.6);
+ max-width: calc(var(--icon-preview-size) * 0.6);
color: var(--color-primary-element-text);
// better alignment with the folder icon
- margin-top: 2px;
+ margin-block-start: 2px;
// Improve icon contrast with a background for files
&--file {
@@ -556,24 +797,27 @@ export default defineComponent({
// Take as much space as possible
flex: 1 1 auto;
- a {
+ button.files-list__row-name-link {
display: flex;
align-items: center;
+ text-align: start;
// Fill cell height and width
width: 100%;
height: 100%;
// Necessary for flex grow to work
min-width: 0;
+ margin: 0;
+ padding: 0;
// Already added to the inner text, see rule below
&:focus-visible {
- outline: none;
+ outline: none !important;
}
// Keyboard indicator a11y
&:focus .files-list__row-name-text {
- outline: 2px solid var(--color-main-text) !important;
- border-radius: 20px;
+ outline: var(--border-width-input-focused) solid var(--color-main-text) !important;
+ border-radius: var(--border-radius-element);
}
&:focus:not(:focus-visible) .files-list__row-name-text {
outline: none !important;
@@ -583,8 +827,8 @@ export default defineComponent({
.files-list__row-name-text {
color: var(--color-main-text);
// Make some space for the outline
- padding: 5px 10px;
- margin-left: -10px;
+ padding: var(--default-grid-baseline) calc(2 * var(--default-grid-baseline));
+ padding-inline-start: -10px;
// Align two name and ext
display: inline-flex;
}
@@ -603,7 +847,7 @@ export default defineComponent({
input {
width: 100%;
// Align with text, 0 - padding - border
- margin-left: -8px;
+ margin-inline-start: -8px;
padding: 2px 6px;
border-width: 2px;
@@ -634,44 +878,56 @@ export default defineComponent({
}
.files-list__row-action--inline {
- margin-right: 7px;
+ margin-inline-end: 7px;
}
+ .files-list__row-mime,
.files-list__row-mtime,
.files-list__row-size {
color: var(--color-text-maxcontrast);
}
+
.files-list__row-size {
- width: calc(var(--row-height) * 1.5);
+ width: calc(var(--row-height) * 2);
// Right align content/text
justify-content: flex-end;
}
.files-list__row-mtime {
- width: calc(var(--row-height) * 2);
+ width: calc(var(--row-height) * 2.5);
+ }
+
+ .files-list__row-mime {
+ width: calc(var(--row-height) * 3.5);
}
.files-list__row-column-custom {
- width: calc(var(--row-height) * 2);
+ width: calc(var(--row-height) * 2.5);
}
}
}
+
+@media screen and (max-width: 512px) {
+ .files-list :deep(.files-list__filters) {
+ // Reduce padding on mobile
+ padding-inline: var(--default-grid-baseline, 4px);
+ }
+}
+
</style>
<style lang="scss">
// Grid mode
-tbody.files-list__tbody.files-list__tbody--grid {
- --half-clickable-area: calc(var(--clickable-area) / 2);
- --row-width: 160px;
- // We use half of the clickable area as visual balance margin
- --row-height: calc(var(--row-width) - var(--half-clickable-area));
- --icon-preview-size: calc(var(--row-width) - var(--clickable-area));
+.files-list--grid tbody.files-list__tbody {
+ --item-padding: 16px;
+ --icon-preview-size: 166px;
+ --name-height: var(--default-clickable-area);
+ --mtime-height: calc(var(--font-size-small) + var(--default-grid-baseline));
+ --row-width: calc(var(--icon-preview-size) + var(--item-padding) * 2);
+ --row-height: calc(var(--icon-preview-size) + var(--name-height) + var(--mtime-height) + var(--item-padding) * 2);
--checkbox-padding: 0px;
-
display: grid;
grid-template-columns: repeat(auto-fill, var(--row-width));
- grid-gap: 15px;
- row-gap: 15px;
align-content: center;
align-items: center;
@@ -679,29 +935,44 @@ tbody.files-list__tbody.files-list__tbody--grid {
justify-items: center;
tr {
+ display: flex;
+ flex-direction: column;
width: var(--row-width);
- height: calc(var(--row-height) + var(--clickable-area));
+ height: var(--row-height);
border: none;
- border-radius: var(--border-radius);
+ border-radius: var(--border-radius-large);
+ padding: var(--item-padding);
}
// Checkbox in the top left
.files-list__row-checkbox {
position: absolute;
z-index: 9;
- top: 0;
- left: 0;
+ top: calc(var(--item-padding) / 2);
+ inset-inline-start: calc(var(--item-padding) / 2);
overflow: hidden;
- width: var(--clickable-area);
- height: var(--clickable-area);
- border-radius: var(--half-clickable-area);
+ --checkbox-container-size: 44px;
+ width: var(--checkbox-container-size);
+ height: var(--checkbox-container-size);
+
+ // Add a background to the checkbox so we do not see the image through it.
+ .checkbox-radio-switch__content::after {
+ content: '';
+ width: 16px;
+ height: 16px;
+ position: absolute;
+ inset-inline-start: 50%;
+ margin-inline-start: -8px;
+ z-index: -1;
+ background: var(--color-main-background);
+ }
}
// Star icon in the top right
.files-list__row-icon-favorite {
position: absolute;
top: 0;
- right: 0;
+ inset-inline-end: 0;
display: flex;
align-items: center;
justify-content: center;
@@ -710,38 +981,55 @@ tbody.files-list__tbody.files-list__tbody--grid {
}
.files-list__row-name {
- display: grid;
- justify-content: stretch;
- width: 100%;
- height: 100%;
- grid-auto-rows: var(--row-height) var(--clickable-area);
+ display: flex;
+ flex-direction: column;
+ width: var(--icon-preview-size);
+ height: calc(var(--icon-preview-size) + var(--name-height));
+ // Ensure that the name outline is visible.
+ overflow: visible;
span.files-list__row-icon {
- width: 100%;
- height: 100%;
- // Visual balance, we use half of the clickable area
- // as a margin around the preview
- padding-top: var(--half-clickable-area);
- }
-
- a.files-list__row-name-link {
- // Minus action menu
- width: calc(100% - var(--clickable-area));
- height: var(--clickable-area);
+ width: var(--icon-preview-size);
+ height: var(--icon-preview-size);
}
.files-list__row-name-text {
margin: 0;
- padding-right: 0;
+ // Ensure that the outline is not too close to the text.
+ margin-inline-start: -4px;
+ padding: 0px 4px;
}
}
+ .files-list__row-mtime {
+ width: var(--icon-preview-size);
+ height: var(--mtime-height);
+ font-size: var(--font-size-small);
+ }
+
.files-list__row-actions {
position: absolute;
- right: 0;
- bottom: 0;
+ inset-inline-end: calc(var(--clickable-area) / 4);
+ inset-block-end: calc(var(--mtime-height) / 2);
width: var(--clickable-area);
height: var(--clickable-area);
}
}
+
+@media screen and (max-width: 768px) {
+ // there is no mtime
+ .files-list--grid tbody.files-list__tbody {
+ --mtime-height: 0px;
+
+ // so we move the action to the name
+ .files-list__row-actions {
+ inset-block-end: var(--item-padding);
+ }
+
+ // and we need to keep space on the name for the actions
+ .files-list__row-name-text {
+ padding-inline-end: var(--clickable-area) !important;
+ }
+ }
+}
</style>