-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat(recent-files): allow configuring image grouping #58908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5bb846e
f1ed770
95d7635
5888b1a
e4a01e0
2d9f283
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO it makes more sense to make this more generic, meaning just make this a group based on mime type. Of cause for now you can use image formats but then we can adjust this if needed anytime and dont create special cases :) The reason we have comparatively few issues in the files app nowadays is the results of the quite flexible way we changed if when migrated from jQuery. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| <!-- | ||
| - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| - SPDX-License-Identifier: AGPL-3.0-or-later | ||
| --> | ||
|
|
||
| <template> | ||
| <tr | ||
| class="files-list__row files-list__row--image-group" | ||
| :class="{ | ||
| 'files-list__row--image-group-expanded': source.expanded, | ||
| 'files-list__row--active': isSelected, | ||
| }"> | ||
| <td class="files-list__row-checkbox" @click.stop> | ||
| <NcCheckboxRadioSwitch | ||
| :aria-label="t('files', 'Toggle selection for image group')" | ||
| :modelValue="isSelected" | ||
| :indeterminate="isPartiallySelected" | ||
| @update:modelValue="onSelectionChange" /> | ||
| </td> | ||
|
|
||
| <td class="files-list__row-name" @click="emit('toggle', source.source)"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This must be changed this is not accessible. You will have to put in either a proper |
||
| <span class="files-list__row-icon"> | ||
| <ImageMultipleIcon :size="20" /> | ||
| </span> | ||
|
|
||
| <span class="files-list__row-image-group-chevron"> | ||
| <NcIconSvgWrapper | ||
| :path="mdiChevronDown" | ||
| :size="20" | ||
| :class="{ 'files-list__row-image-group-chevron--expanded': source.expanded }" /> | ||
| </span> | ||
|
|
||
| <span class="files-list__row-name-text"> | ||
| {{ n('files', '{count} image', '{count} images', source.images.length, { count: source.images.length }) }} | ||
| </span> | ||
| </td> | ||
|
|
||
| <td v-if="isMimeAvailable" class="files-list__row-mime" /> | ||
| <td v-if="isSizeAvailable" class="files-list__row-size" /> | ||
| <td v-if="isMtimeAvailable" class="files-list__row-mtime" /> | ||
| </tr> | ||
| </template> | ||
|
|
||
| <script lang="ts" setup> | ||
| import type { ImageGroupNode } from '../composables/useImageGrouping.ts' | ||
|
|
||
| import { mdiChevronDown } from '@mdi/js' | ||
| import { n, t } from '@nextcloud/l10n' | ||
| import { computed } from 'vue' | ||
| import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' | ||
| import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' | ||
| import ImageMultipleIcon from 'vue-material-design-icons/ImageMultiple.vue' | ||
| import { useSelectionStore } from '../store/selection.ts' | ||
|
|
||
| const props = defineProps<{ | ||
| source: ImageGroupNode | ||
| isMimeAvailable?: boolean | ||
| isSizeAvailable?: boolean | ||
| isMtimeAvailable?: boolean | ||
| }>() | ||
|
|
||
| const emit = defineEmits<{ | ||
| (e: 'toggle', key: string): void | ||
| }>() | ||
|
|
||
| const selectionStore = useSelectionStore() | ||
|
|
||
| const childSources = computed(() => props.source.images.map((img) => img.source)) | ||
|
|
||
| const isSelected = computed(() => childSources.value.every((src) => selectionStore.selected.includes(src))) | ||
|
|
||
| const isPartiallySelected = computed(() => !isSelected.value && childSources.value.some((src) => selectionStore.selected.includes(src))) | ||
|
|
||
| /** | ||
| * Handle selection change for the image group | ||
| * | ||
| * @param selected - Whether the group should be selected or deselected | ||
| */ | ||
| async function onSelectionChange(selected: boolean) { | ||
| const current = selectionStore.selected | ||
| if (selected) { | ||
| // select all children | ||
| selectionStore.set([...new Set([...current, ...childSources.value])]) | ||
| } else { | ||
| // unselect all children | ||
| selectionStore.set(current.filter((src) => !childSources.value.includes(src))) | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <style scoped lang="scss"> | ||
| .files-list__row--image-group { | ||
| .files-list__row-name { | ||
| cursor: pointer; | ||
| * { | ||
| cursor: pointer; | ||
| } | ||
| } | ||
|
|
||
| .files-list__row-image-group-chevron { | ||
| display: flex; | ||
| align-items: center; | ||
| color: var(--color-text-maxcontrast); | ||
| &--expanded { | ||
| transform: rotate(180deg); | ||
| } | ||
| } | ||
|
|
||
| .files-list__row-name-text { | ||
| color: var(--color-main-text); | ||
| } | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| <!-- | ||
| - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| - SPDX-License-Identifier: AGPL-3.0-or-later | ||
| --> | ||
|
|
||
| <template> | ||
| <FileEntryImageGroup | ||
| v-if="isGroup" | ||
| :source="source" | ||
| :isMimeAvailable="isMimeAvailable" | ||
| :isSizeAvailable="isSizeAvailable" | ||
| :isMtimeAvailable="isMtimeAvailable" | ||
| @toggle="onToggleGroup?.($event)" /> | ||
|
|
||
| <component | ||
| :is="entryComponent" | ||
| v-else | ||
| :source="source" | ||
| :isMimeAvailable="isMimeAvailable" | ||
| :isSizeAvailable="isSizeAvailable" | ||
| :isMtimeAvailable="isMtimeAvailable" | ||
| :class="{ 'files-list__row--group-child': isGroupChild }" | ||
| v-bind="$attrs" /> | ||
| </template> | ||
|
|
||
| <script lang="ts"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please directly use |
||
| import type { PropType } from 'vue' | ||
| import type { GroupedNode } from '../composables/useImageGrouping.ts' | ||
|
|
||
| import { defineComponent } from 'vue' | ||
| import FileEntry from './FileEntry.vue' | ||
| import FileEntryGrid from './FileEntryGrid.vue' | ||
| import FileEntryImageGroup from './FileEntryImageGroup.vue' | ||
| import { isImageGroup } from '../composables/useImageGrouping.ts' | ||
|
|
||
| export default defineComponent({ | ||
| name: 'FileEntryWrapper', | ||
|
|
||
| components: { | ||
| FileEntry, | ||
| FileEntryGrid, | ||
| FileEntryImageGroup, | ||
| }, | ||
|
|
||
| inheritAttrs: false, | ||
|
|
||
| props: { | ||
| source: { | ||
| type: Object as PropType<GroupedNode>, | ||
| required: true, | ||
| }, | ||
|
|
||
| gridMode: { | ||
| type: Boolean, | ||
| default: false, | ||
| }, | ||
|
|
||
| isMimeAvailable: { | ||
| type: Boolean, | ||
| default: false, | ||
| }, | ||
|
|
||
| isSizeAvailable: { | ||
| type: Boolean, | ||
| default: false, | ||
| }, | ||
|
|
||
| isMtimeAvailable: { | ||
| type: Boolean, | ||
| default: false, | ||
| }, | ||
|
|
||
| onToggleGroup: { | ||
| type: Function, | ||
| default: null, | ||
| }, | ||
| }, | ||
|
|
||
| emits: ['toggle-group'], | ||
|
|
||
| computed: { | ||
| isGroup(): boolean { | ||
| return isImageGroup(this.source) | ||
| }, | ||
|
|
||
| isGroupChild(): boolean { | ||
| return '_isGroupChild' in this.source | ||
| }, | ||
|
|
||
| entryComponent() { | ||
| return this.gridMode ? FileEntryGrid : FileEntry | ||
| }, | ||
| }, | ||
| }) | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| <!-- | ||
| - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| - SPDX-License-Identifier: AGPL-3.0-or-later | ||
| --> | ||
|
|
||
| <script lang="ts" setup> | ||
| import { t } from '@nextcloud/l10n' | ||
| import { NcFormBoxSwitch, NcInputField, NcSelect } from '@nextcloud/vue' | ||
| import debounce from 'debounce' | ||
| import { ref, watch } from 'vue' | ||
| import NcAppSettingsSection from '@nextcloud/vue/components/NcAppSettingsSection' | ||
| import NcFormBox from '@nextcloud/vue/components/NcFormBox' | ||
| import { useUserConfigStore } from '../../store/userconfig.ts' | ||
|
|
||
| const store = useUserConfigStore() | ||
|
|
||
| const availableMimetypes = [ | ||
| { id: 'image/png', label: 'PNG' }, | ||
| { id: 'image/jpeg', label: 'JPEG' }, | ||
| { id: 'image/gif', label: 'GIF' }, | ||
| { id: 'image/webp', label: 'WebP' }, | ||
| { id: 'image/avif', label: 'AVIF' }, | ||
| { id: 'image/heic', label: 'HEIC' }, | ||
| { id: 'image/heif', label: 'HEIF' }, | ||
| ] | ||
|
|
||
| const storedMimetypes = store.userConfig.recent_files_group_mimetypes | ||
| const initialMimetypes = Array.isArray(storedMimetypes) | ||
| ? availableMimetypes.filter((m) => storedMimetypes.includes(m.id)) | ||
| : [] | ||
|
|
||
| const selectedMimetypes = ref(initialMimetypes) | ||
|
|
||
| const debouncedUpdateMimetypes = debounce((value) => { | ||
| store.update('recent_files_group_mimetypes', JSON.stringify(value.map((v) => v.id))) | ||
| }, 500) | ||
|
|
||
| watch(selectedMimetypes, (value) => { | ||
| debouncedUpdateMimetypes(value) | ||
| }) | ||
|
|
||
| const debouncedUpdateTimespan = debounce((value: number) => { | ||
| store.update('recent_files_group_timespan_minutes', value) | ||
| }, 500) | ||
| </script> | ||
|
|
||
| <template> | ||
| <NcAppSettingsSection id="recent" :name="t('files', 'Recent view')"> | ||
| <NcFormBox> | ||
| <NcFormBoxSwitch | ||
| v-model="store.userConfig.group_recent_files_images" | ||
| :label="t('files', 'Group image files')" | ||
| @update:modelValue="store.update('group_recent_files_images', $event)" /> | ||
| <label>{{ t('files', 'Group these image types together') }}</label> | ||
| <NcSelect | ||
| v-model="selectedMimetypes" | ||
| :options="availableMimetypes" | ||
| labelOutside | ||
| multiple /> | ||
| <NcInputField | ||
| v-model="store.userConfig.recent_files_group_timespan_minutes" | ||
| type="number" | ||
| :min="1" | ||
| :max="999" | ||
| :label="t('files', 'Time window in minutes to group files uploaded close together')" | ||
| @update:modelValue="debouncedUpdateTimespan(Number($event))" /> | ||
| </NcFormBox> | ||
| </NcAppSettingsSection> | ||
| </template> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about tif or other image formats?
We could just always (if grouping is enabled) group by mime group, e.g.
image/*and / orvideo/*