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..338ed7eb0 --- /dev/null +++ b/src/components/header/GoToDateMenuItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 36e3a10db..4f7a3a18c 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -54,6 +54,14 @@ + + @@ -67,6 +75,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'; @@ -86,6 +95,7 @@ export default defineComponent({ NcBreadcrumb, NcActions, NcActionButton, + NcDateTimePicker, PublicUploadHandler, HomeIcon, ShareIcon, @@ -161,6 +171,11 @@ export default defineComponent({ uploadHandler(): InstanceType | null { return (this.$refs.uploadHandler as InstanceType) || null; }, + + onDateSelected(date: Date) { + if (!date) return; + utils.bus.emit('memories:timeline:scrollToDate', date); + }, }, }); 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[];