feat: set creation_time on file creation and render recently created icon

Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
pull/58562/head
Cristian Scheid 2 months ago
parent 34484b591a
commit 7f89490cef
  1. 5
      apps/dav/lib/Files/FileSearchBackend.php
  2. 30
      apps/files/src/components/FileEntry/FileEntryPreview.vue
  3. 78
      apps/files/src/components/FileEntry/RecentlyCreatedIcon.vue
  4. 10
      apps/files/src/components/FilesListVirtual.vue
  5. 6
      lib/private/Files/Cache/Cache.php
  6. 2
      lib/private/Files/Cache/QuerySearchHelper.php
  7. 2
      lib/private/Files/Cache/SearchBuilder.php
  8. 1
      lib/private/Files/Node/Folder.php

@ -86,6 +86,7 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{http://nextcloud.org/ns}creation_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{http://nextcloud.org/ns}upload_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
@ -299,6 +300,8 @@ class FileSearchBackend implements ISearchBackend {
return $node->getName();
case '{DAV:}getlastmodified':
return $node->getLastModified();
case '{http://nextcloud.org/ns}creation_time':
return $node->getNode()->getCreationTime();
case '{http://nextcloud.org/ns}upload_time':
return $node->getNode()->getUploadTime();
case FilesPlugin::SIZE_PROPERTYNAME:
@ -461,6 +464,8 @@ class FileSearchBackend implements ISearchBackend {
return 'mimetype';
case '{DAV:}getlastmodified':
return 'mtime';
case '{http://nextcloud.org/ns}creation_time':
return 'creation_time';
case '{http://nextcloud.org/ns}upload_time':
return 'upload_time';
case FilesPlugin::SIZE_PROPERTYNAME:

@ -42,6 +42,11 @@
<FavoriteIcon v-once />
</span>
<!-- Recently created icon -->
<span v-else-if="isRecentView && isRecentlyCreated" class="files-list__row-icon-recently-created">
<RecentlyCreatedIcon v-once />
</span>
<component
:is="fileOverlay"
v-if="fileOverlay"
@ -71,6 +76,7 @@ import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import CollectivesIcon from './CollectivesIcon.vue'
import FavoriteIcon from './FavoriteIcon.vue'
import RecentlyCreatedIcon from './RecentlyCreatedIcon.vue'
import { usePreviewImage } from '../../composables/usePreviewImage.ts'
import logger from '../../logger.ts'
import { isLivePhoto } from '../../services/LivePhotos.ts'
@ -91,6 +97,7 @@ export default defineComponent({
LinkIcon,
NetworkIcon,
TagIcon,
RecentlyCreatedIcon,
},
props: {
@ -138,6 +145,29 @@ export default defineComponent({
return this.source.attributes.favorite === 1
},
isRecentlyCreated(): boolean {
if (this.source.attributes.upload_time) {
return false
}
const creationDate = this.source.attributes.creationdate
? new Date(this.source.attributes.creationdate)
: null
if (!creationDate) {
return false
}
const oneDayAgo = new Date()
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
return creationDate > oneDayAgo
},
isRecentView(): boolean {
return this.$route?.params?.view === 'recent'
},
userConfig(): UserConfig {
return this.userConfigStore.userConfig
},

@ -0,0 +1,78 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcIconSvgWrapper class="recently-created-marker-icon" :name="t('files', 'Recently created')" :svg="PlusSvg" />
</template>
<script lang="ts">
import PlusSvg from '@mdi/svg/svg/plus.svg?raw'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
/**
* A recently created icon to be used for overlaying recently created entries like the file preview / icon
* It has a stroke around the icon to ensure enough contrast for accessibility.
*
* If the background has a hover state you might want to also apply it to the stroke like this:
* ```scss
* .parent:hover :deep(.favorite-marker-icon svg path) {
* stroke: var(--color-background-hover);
* }
* ```
*/
export default defineComponent({
name: 'RecentlyCreatedIcon',
components: {
NcIconSvgWrapper,
},
data() {
return {
PlusSvg,
}
},
async mounted() {
await this.$nextTick()
// MDI default viewBox is "0 0 24 24" but we add a stroke of 10px so we must adjust it
const el = this.$el.querySelector('svg')
el?.setAttribute?.('viewBox', '-4 -4 30 30')
},
methods: {
t,
},
})
</script>
<style lang="scss" scoped>
.recently-created-marker-icon {
color: var(--color-element-success);
// Override NcIconSvgWrapper defaults (clickable area)
min-width: unset !important;
min-height: unset !important;
:deep() {
svg {
// We added a stroke for a11y so we must increase the size to include the stroke
width: 20px !important;
height: 20px !important;
// Override NcIconSvgWrapper defaults of 20px
max-width: unset !important;
max-height: unset !important;
// Show a border around the icon for better contrast
path {
stroke: var(--color-main-background);
stroke-width: 8px;
stroke-linejoin: round;
paint-order: stroke;
}
}
}
}
</style>

@ -743,7 +743,7 @@ export default defineComponent({
& > span {
justify-content: flex-start;
&:not(.files-list__row-icon-favorite) svg {
&:not(.files-list__row-icon-favorite):not(.files-list__row-icon-recently-created) svg {
width: var(--icon-preview-size);
height: var(--icon-preview-size);
}
@ -791,7 +791,8 @@ export default defineComponent({
}
}
&-favorite {
&-favorite,
&-recently-created {
position: absolute;
top: 0px;
inset-inline-end: -10px;
@ -993,8 +994,9 @@ export default defineComponent({
}
}
// Star icon in the top right
.files-list__row-icon-favorite {
// Icon in the top right
.files-list__row-icon-favorite,
.files-list__row-icon-recently-created {
position: absolute;
top: 0;
inset-inline-end: 0;

@ -250,6 +250,12 @@ class Cache implements ICache {
* @throws \RuntimeException
*/
public function put($file, array $data) {
// do not carry over creation_time to file versions, as each new version would otherwise
// create a filecache_extended entry with the same creation_time as the original file
if (str_starts_with($file, 'files_versions/')) {
unset($data['creation_time']);
}
if (($id = $this->getId($file)) > -1) {
$this->update($id, $data);
return $id;

@ -153,7 +153,7 @@ class QuerySearchHelper {
$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
$joinExtendedCache = in_array('upload_time', $requestedFields);
$joinExtendedCache = in_array('creation_time', $requestedFields) || in_array('upload_time', $requestedFields);
$query = $builder->selectFileCache('file', $joinExtendedCache);

@ -64,6 +64,7 @@ class SearchBuilder {
'share_with' => 'string',
'share_type' => 'integer',
'owner' => 'string',
'creation_time' => 'integer',
'upload_time' => 'integer',
];
@ -258,6 +259,7 @@ class SearchBuilder {
'share_with' => ['eq'],
'share_type' => ['eq'],
'owner' => ['eq'],
'creation_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
];

@ -175,6 +175,7 @@ class Folder extends Node implements IFolder {
throw new NotPermittedException('Could not create path "' . $fullPath . '"');
}
$node = new File($this->root, $this->view, $fullPath, null, $this);
$this->view->putFileInfo($fullPath, ['creation_time' => time()]);
$this->sendHooks(['postWrite', 'postCreate'], [$node]);
return $node;
}

Loading…
Cancel
Save