From d1911fb2d0804c83ae86364e8fb3344eb6dcbf7b Mon Sep 17 00:00:00 2001 From: Philip Gouverneur Date: Sat, 14 Feb 2026 22:40:56 +0100 Subject: [PATCH 1/5] Add "go to date" button Signed-off-by: Philip Gouverneur --- src/components/MobileHeader.vue | 3 + src/components/Timeline.vue | 47 +++++++++++ src/components/header/GoToDateMenuItem.vue | 81 +++++++++++++++++++ src/components/top-matter/FolderTopMatter.vue | 57 +++++++++++++ src/components/top-matter/TopMatter.vue | 16 +++- src/services/utils/event-bus.ts | 4 + 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/components/header/GoToDateMenuItem.vue diff --git a/src/components/MobileHeader.vue b/src/components/MobileHeader.vue index 16868e1f7..163c5deda 100644 --- a/src/components/MobileHeader.vue +++ b/src/components/MobileHeader.vue @@ -11,6 +11,7 @@
+
@@ -21,6 +22,7 @@ import { defineComponent } from 'vue'; import { generateUrl } from '@nextcloud/router'; +import GoToDateMenuItem from '@components/header/GoToDateMenuItem.vue'; import UploadMenuItem from '@components/header/UploadMenuItem.vue'; import SearchbarMenuItem from '@components/header/SearchbarMenuItem.vue'; @@ -31,6 +33,7 @@ import banner from '@assets/banner.svg'; export default defineComponent({ name: 'MobileHeader', components: { + GoToDateMenuItem, UploadMenuItem, SearchbarMenuItem, }, diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 9e4811c4b..0ba79c5fe 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -236,6 +236,8 @@ export default defineComponent({ utils.bus.on('memories:timeline:deleted', this.deleteFromViewWithAnimation); utils.bus.on('memories:timeline:soft-refresh', this.softRefresh); utils.bus.on('memories:timeline:hard-refresh', this.refresh); + utils.bus.on('memories:timeline:scrollToDate', this.scrollToDate); + utils.bus.on('memories:timeline:getDateRange', this.getDateRange); }, beforeDestroy() { @@ -246,6 +248,8 @@ export default defineComponent({ utils.bus.off('memories:timeline:deleted', this.deleteFromViewWithAnimation); utils.bus.off('memories:timeline:soft-refresh', this.softRefresh); utils.bus.off('memories:timeline:hard-refresh', this.refresh); + utils.bus.off('memories:timeline:scrollToDate', this.scrollToDate); + utils.bus.off('memories:timeline:getDateRange', this.getDateRange); this.resetState(); this.state = 0; }, @@ -347,6 +351,49 @@ export default defineComponent({ return this.containerSize[0] <= 768; }, + /** Scroll to the nearest day matching the given date */ + scrollToDate(date: Date) { + const targetDayId = utils.dateToDayId(date); + + // Find the nearest dayId <= target (closest earlier or exact date) + let bestDayId: number | null = null; + for (const dayId of this.heads.keys()) { + if (dayId <= targetDayId) { + if (bestDayId === null || dayId > bestDayId) { + bestDayId = dayId; + } + } + } + + // If no earlier date found, use the closest one overall + if (bestDayId === null) { + for (const dayId of this.heads.keys()) { + if (bestDayId === null || Math.abs(dayId - targetDayId) < Math.abs(bestDayId - targetDayId)) { + bestDayId = dayId; + } + } + } + + if (bestDayId === null) return; + + const index = this.list.findIndex((r) => r.type === 0 && r.dayId === bestDayId); + if (index !== -1) { + this.refs.recycler?.scrollToItem(index); + } + }, + + /** Provide the date range of the current view */ + getDateRange(event: { result: { min: Date; max: Date } | null }) { + const dayIds = Array.from(this.heads.keys()); + if (dayIds.length === 0) return; + const min = Math.min(...dayIds); + const max = Math.max(...dayIds); + event.result = { + min: utils.dayIdToDate(min), + max: utils.dayIdToDate(max), + }; + }, + isMobileLayout() { return this.containerSize[0] <= 600; }, diff --git a/src/components/header/GoToDateMenuItem.vue b/src/components/header/GoToDateMenuItem.vue new file mode 100644 index 000000000..6d7f6c550 --- /dev/null +++ b/src/components/header/GoToDateMenuItem.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 36e3a10db..1ca729fc2 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -53,7 +53,23 @@ + + + {{ t('memories', 'Go to date') }} + + + + @@ -77,6 +93,7 @@ import ShareIcon from 'vue-material-design-icons/ShareVariant.vue'; import TimelineIcon from 'vue-material-design-icons/ImageMultiple.vue'; import FoldersIcon from 'vue-material-design-icons/FolderMultiple.vue'; import UploadIcon from 'vue-material-design-icons/Upload.vue'; +import CalendarSearchIcon from 'vue-material-design-icons/CalendarSearch.vue'; export default defineComponent({ name: 'FolderTopMatter', @@ -92,6 +109,7 @@ export default defineComponent({ TimelineIcon, FoldersIcon, UploadIcon, + CalendarSearchIcon, }, mixins: [UserConfig], @@ -161,6 +179,35 @@ export default defineComponent({ uploadHandler(): InstanceType | null { return (this.$refs.uploadHandler as InstanceType) || null; }, + + openDatePicker() { + const input = this.$refs.dateInput as HTMLInputElement; + const event = { result: null as { min: Date; max: Date } | null }; + utils.bus.emit('memories:timeline:getDateRange', event); + if (event.result) { + input.min = event.result.min.toISOString().split('T')[0]; + input.max = event.result.max.toISOString().split('T')[0]; + } + + // Temporarily make visible for showPicker to work + input.style.width = '1px'; + input.style.height = '1px'; + try { + input.showPicker(); + } catch { + input.click(); + } + input.style.width = ''; + input.style.height = ''; + }, + + onDateSelected(event: Event) { + const input = event.target as HTMLInputElement; + if (!input.value) return; + const date = new Date(input.value + 'T00:00:00Z'); + utils.bus.emit('memories:timeline:scrollToDate', date); + input.value = ''; + }, }, }); @@ -180,5 +227,15 @@ export default defineComponent({ align-items: center; gap: 10px; // Add spacing between actions and progress bar } + + .date-input-hidden { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + border: 0; + padding: 0; + margin: 0; + } } diff --git a/src/components/top-matter/TopMatter.vue b/src/components/top-matter/TopMatter.vue index e3ecde649..7b169db78 100644 --- a/src/components/top-matter/TopMatter.vue +++ b/src/components/top-matter/TopMatter.vue @@ -4,9 +4,14 @@ :class="{ 'dynamic-visible': dynamicVisible, }" - v-if="currentmatter" > - + + +
+
+ +
+
@@ -19,6 +24,8 @@ import FaceTopMatter from './FaceTopMatter.vue'; import AlbumTopMatter from './AlbumTopMatter.vue'; import PlacesTopMatter from './PlacesTopMatter.vue'; +import GoToDateMenuItem from '@components/header/GoToDateMenuItem.vue'; + import * as utils from '@services/utils'; export default defineComponent({ @@ -28,6 +35,7 @@ export default defineComponent({ ClusterTopMatter, FaceTopMatter, AlbumTopMatter, + GoToDateMenuItem, }, data: () => ({ @@ -134,6 +142,10 @@ export default defineComponent({ } } + .top-matter-date-only { + justify-content: flex-end; + } + :deep button { display: inline-block; } diff --git a/src/services/utils/event-bus.ts b/src/services/utils/event-bus.ts index 7c26adfa8..d8e8fff5a 100644 --- a/src/services/utils/event-bus.ts +++ b/src/services/utils/event-bus.ts @@ -44,6 +44,10 @@ export type BusEvent = { previous: number; dynTopMatterVisible: boolean; }; + /** Scroll timeline to a specific date */ + 'memories:timeline:scrollToDate': Date; + /** Request the date range of the current view (handler fills in result) */ + 'memories:timeline:getDateRange': { result: { min: Date; max: Date } | null }; /** Albums were updated for these photos */ 'memories:albums:update': IPhoto[]; From e91a035c91ad0e1219c0f907650012c6a7819c8b Mon Sep 17 00:00:00 2001 From: Philip Gouverneur Date: Sat, 14 Feb 2026 22:53:27 +0100 Subject: [PATCH 2/5] Prettier Signed-off-by: Philip Gouverneur --- src/components/header/GoToDateMenuItem.vue | 8 +------- src/components/top-matter/FolderTopMatter.vue | 13 ++----------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/components/header/GoToDateMenuItem.vue b/src/components/header/GoToDateMenuItem.vue index 6d7f6c550..8db3d4613 100644 --- a/src/components/header/GoToDateMenuItem.vue +++ b/src/components/header/GoToDateMenuItem.vue @@ -10,13 +10,7 @@ - + diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 1ca729fc2..1a80bad26 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -54,22 +54,13 @@ - + {{ t('memories', 'Go to date') }} - + From bae22e3c420b68bac697b744bc380a74a336cb09 Mon Sep 17 00:00:00 2001 From: Philip Gouverneur Date: Fri, 6 Mar 2026 19:54:58 +0100 Subject: [PATCH 3/5] Seitch to nextclouds own date picker Signed-off-by: Philip Gouverneur --- src/components/header/GoToDateMenuItem.vue | 72 +++++-------------- src/components/top-matter/FolderTopMatter.vue | 59 +++++---------- 2 files changed, 34 insertions(+), 97 deletions(-) diff --git a/src/components/header/GoToDateMenuItem.vue b/src/components/header/GoToDateMenuItem.vue index 8db3d4613..702d945e3 100644 --- a/src/components/header/GoToDateMenuItem.vue +++ b/src/components/header/GoToDateMenuItem.vue @@ -1,75 +1,37 @@ - - diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 1a80bad26..2c8500ec9 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -54,13 +54,15 @@ - - {{ t('memories', 'Go to date') }} - - - + @@ -74,6 +76,7 @@ const NcBreadcrumbs = () => import('@nextcloud/vue/dist/Components/NcBreadcrumbs const NcBreadcrumb = () => import('@nextcloud/vue/dist/Components/NcBreadcrumb.js'); import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'; +import NcDateTimePicker from '@nextcloud/vue/dist/Components/NcDateTimePicker.js'; import PublicUploadHandler from '@components/upload/PublicUploadHandler.vue'; import * as utils from '@services/utils'; @@ -84,7 +87,6 @@ import ShareIcon from 'vue-material-design-icons/ShareVariant.vue'; import TimelineIcon from 'vue-material-design-icons/ImageMultiple.vue'; import FoldersIcon from 'vue-material-design-icons/FolderMultiple.vue'; import UploadIcon from 'vue-material-design-icons/Upload.vue'; -import CalendarSearchIcon from 'vue-material-design-icons/CalendarSearch.vue'; export default defineComponent({ name: 'FolderTopMatter', @@ -94,17 +96,23 @@ export default defineComponent({ NcBreadcrumb, NcActions, NcActionButton, + NcDateTimePicker, PublicUploadHandler, HomeIcon, ShareIcon, TimelineIcon, FoldersIcon, UploadIcon, - CalendarSearchIcon, }, mixins: [UserConfig], + data() { + return { + goToDate: new Date(), + }; + }, + computed: { list(): { text: string; @@ -171,33 +179,9 @@ export default defineComponent({ return (this.$refs.uploadHandler as InstanceType) || null; }, - openDatePicker() { - const input = this.$refs.dateInput as HTMLInputElement; - const event = { result: null as { min: Date; max: Date } | null }; - utils.bus.emit('memories:timeline:getDateRange', event); - if (event.result) { - input.min = event.result.min.toISOString().split('T')[0]; - input.max = event.result.max.toISOString().split('T')[0]; - } - - // Temporarily make visible for showPicker to work - input.style.width = '1px'; - input.style.height = '1px'; - try { - input.showPicker(); - } catch { - input.click(); - } - input.style.width = ''; - input.style.height = ''; - }, - - onDateSelected(event: Event) { - const input = event.target as HTMLInputElement; - if (!input.value) return; - const date = new Date(input.value + 'T00:00:00Z'); + onDateSelected(date: Date) { + if (!date) return; utils.bus.emit('memories:timeline:scrollToDate', date); - input.value = ''; }, }, }); @@ -219,14 +203,5 @@ export default defineComponent({ gap: 10px; // Add spacing between actions and progress bar } - .date-input-hidden { - position: absolute; - width: 0; - height: 0; - overflow: hidden; - border: 0; - padding: 0; - margin: 0; - } } From 5af2cec1e6243927f2fa461e47183a2637f7a455 Mon Sep 17 00:00:00 2001 From: Philip Gouverneur Date: Fri, 6 Mar 2026 20:01:18 +0100 Subject: [PATCH 4/5] Fix error messages Signed-off-by: Philip Gouverneur --- src/components/header/GoToDateMenuItem.vue | 10 ++-------- src/components/top-matter/FolderTopMatter.vue | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/components/header/GoToDateMenuItem.vue b/src/components/header/GoToDateMenuItem.vue index 702d945e3..338ed7eb0 100644 --- a/src/components/header/GoToDateMenuItem.vue +++ b/src/components/header/GoToDateMenuItem.vue @@ -1,10 +1,10 @@ @@ -21,12 +21,6 @@ export default defineComponent({ NcDateTimePicker, }, - data() { - return { - selectedDate: new Date(), - }; - }, - methods: { onDateSelected(date: Date) { if (!date) return; diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 2c8500ec9..0a7330b1c 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -57,11 +57,11 @@ @@ -107,12 +107,6 @@ export default defineComponent({ mixins: [UserConfig], - data() { - return { - goToDate: new Date(), - }; - }, - computed: { list(): { text: string; From 365e69921e336dd24e1b1dff4e3d1fcea206d67a Mon Sep 17 00:00:00 2001 From: Philip Gouverneur Date: Fri, 6 Mar 2026 20:03:23 +0100 Subject: [PATCH 5/5] Prettier Signed-off-by: Philip Gouverneur --- src/components/top-matter/FolderTopMatter.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 0a7330b1c..4f7a3a18c 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -53,7 +53,6 @@ -