diff options
Diffstat (limited to 'apps/files/src/components/FilesListVirtual.vue')
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 618 |
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> |