diff --git a/mobile/lib/ui/viewer/gallery/component/group/group_header_widget.dart b/mobile/lib/ui/viewer/gallery/component/group/group_header_widget.dart index a946cd9d6..25ea5e7d6 100644 --- a/mobile/lib/ui/viewer/gallery/component/group/group_header_widget.dart +++ b/mobile/lib/ui/viewer/gallery/component/group/group_header_widget.dart @@ -1,16 +1,15 @@ import "package:flutter/cupertino.dart"; -import "package:intl/intl.dart"; import 'package:photos/core/constants.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; class GroupHeaderWidget extends StatelessWidget { - final int timestamp; + final String title; final int gridSize; const GroupHeaderWidget({ super.key, - required this.timestamp, + required this.title, required this.gridSize, }); @@ -22,7 +21,7 @@ class GroupHeaderWidget extends StatelessWidget { gridSize < photoGridSizeMax ? textTheme.body : textTheme.small; final double horizontalPadding = gridSize < photoGridSizeMax ? 12.0 : 8.0; final double verticalPadding = gridSize < photoGridSizeMax ? 12.0 : 14.0; - final String dayTitle = _getDayTitle(context, timestamp); + return Padding( padding: EdgeInsets.symmetric( horizontal: horizontalPadding, @@ -31,33 +30,12 @@ class GroupHeaderWidget extends StatelessWidget { child: Container( alignment: Alignment.centerLeft, child: Text( - dayTitle, - style: (dayTitle == S.of(context).dayToday) + title, + style: (title == S.of(context).dayToday) ? textStyle : textStyle.copyWith(color: colorScheme.textMuted), ), ), ); } - - String _getDayTitle(BuildContext context, int timestamp) { - final date = DateTime.fromMicrosecondsSinceEpoch(timestamp); - final now = DateTime.now(); - - if (date.year == now.year && date.month == now.month) { - if (date.day == now.day) { - return S.of(context).dayToday; - } else if (date.day == now.day - 1) { - return S.of(context).dayYesterday; - } - } - - if (date.year != DateTime.now().year) { - return DateFormat.yMMMEd(Localizations.localeOf(context).languageCode) - .format(date); - } else { - return DateFormat.MMMEd(Localizations.localeOf(context).languageCode) - .format(date); - } - } } diff --git a/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart b/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart index 8e81a4eb7..299d84d7d 100644 --- a/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart +++ b/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart @@ -11,6 +11,7 @@ import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart"; import "package:photos/ui/viewer/gallery/component/group/group_gallery.dart"; import "package:photos/ui/viewer/gallery/component/group/group_header_widget.dart"; +import "package:photos/ui/viewer/gallery/component/group/type.dart"; import 'package:photos/ui/viewer/gallery/gallery.dart'; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; @@ -104,32 +105,30 @@ class _LazyGroupGalleryState extends State { if (_filesInGroup.isEmpty) { return; } - final DateTime groupDate = - DateTime.fromMicrosecondsSinceEpoch(_filesInGroup[0].creationTime!); + final galleryState = context.findAncestorStateOfType(); + final groupType = GalleryContextState.of(context)!.type; + // iterate over files and check if any of the belongs to this group - final anyCandidateForGroup = event.updatedFiles.any((file) { - final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - return fileDate.year == groupDate.year && - fileDate.month == groupDate.month && - fileDate.day == groupDate.day; - }); + final anyCandidateForGroup = groupType.areModifiedFilesPartOfGroup( + event.updatedFiles, + _filesInGroup[0], + lastFile: _filesInGroup.last, + ); if (anyCandidateForGroup) { + late int startRange, endRange; + (startRange, endRange) = groupType.getGroupRange(_filesInGroup[0]); if (kDebugMode) { _logger.info( - " files were updated due to ${event.reason} on " + - DateTime.fromMicrosecondsSinceEpoch( - groupDate.microsecondsSinceEpoch, - ).toIso8601String(), + " files were updated due to ${event.reason} on type ${groupType.name} from ${DateTime.fromMicrosecondsSinceEpoch(startRange).toIso8601String()}" + " to ${DateTime.fromMicrosecondsSinceEpoch(endRange).toIso8601String()}", ); } if (event.type == EventType.addedOrUpdated || widget.removalEventTypes.contains(event.type)) { // We are reloading the whole group - final dayStartTime = - DateTime(groupDate.year, groupDate.month, groupDate.day); final result = await widget.asyncLoader( - dayStartTime.microsecondsSinceEpoch, - dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1, + startRange, + endRange, asc: GalleryContextState.of(context)!.sortOrderAsc, ); @@ -144,7 +143,7 @@ class _LazyGroupGalleryState extends State { //[galleryState] will never be null except when LazyLoadingGallery is //used without Gallery as an ancestor. - final galleryState = context.findAncestorStateOfType(); + if (galleryState?.mounted ?? false) { galleryState!.setState(() {}); _filesInGroup = result.files; @@ -178,6 +177,7 @@ class _LazyGroupGalleryState extends State { if (_filesInGroup.isEmpty) { return const SizedBox.shrink(); } + final groupType = GalleryContextState.of(context)!.type; return Column( children: [ Row( @@ -185,7 +185,11 @@ class _LazyGroupGalleryState extends State { children: [ if (widget.enableFileGrouping) GroupHeaderWidget( - timestamp: _filesInGroup[0].creationTime!, + title: groupType.getTitle( + context, + _filesInGroup[0], + lastFile: _filesInGroup.last, + ), gridSize: widget.photoGridSize, ), Expanded(child: Container()), diff --git a/mobile/lib/ui/viewer/gallery/component/group/type.dart b/mobile/lib/ui/viewer/gallery/component/group/type.dart new file mode 100644 index 000000000..f47156591 --- /dev/null +++ b/mobile/lib/ui/viewer/gallery/component/group/type.dart @@ -0,0 +1,175 @@ +import "package:flutter/widgets.dart"; +import "package:intl/intl.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/utils/date_time_util.dart"; + +enum GroupType { day, week, month, size, year } + +extension GroupTypeExtension on GroupType { + String get name { + switch (this) { + case GroupType.day: + return "day"; + case GroupType.week: + return "week"; + case GroupType.month: + return "month"; + case GroupType.size: + return "size"; + case GroupType.year: + return "year"; + } + } + + String getTitle(BuildContext context, EnteFile file, {EnteFile? lastFile}) { + if (this == GroupType.day) { + return _getDayTitle(context, file.creationTime!); + } else if (this == GroupType.week) { + // return weeks starting date to end date based on file + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final startOfWeek = date.subtract(Duration(days: date.weekday - 1)); + final endOfWeek = startOfWeek.add(const Duration(days: 6)); + return "${DateFormat.MMMd(Localizations.localeOf(context).languageCode).format(startOfWeek)} - ${DateFormat.MMMd(Localizations.localeOf(context).languageCode).format(endOfWeek)}, ${endOfWeek.year}"; + } else if (this == GroupType.year) { + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + return DateFormat.y(Localizations.localeOf(context).languageCode) + .format(date); + } else if (this == GroupType.month) { + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + return DateFormat.yMMM(Localizations.localeOf(context).languageCode) + .format(date); + } else { + throw UnimplementedError("not implemented for $this"); + } + } + + // returns true if the group should be refreshed. + // If groupType is day, it should return true if the list of modified files contains a file that was created on the same day as the first file. + // If groupType is week, it should return true if the list of modified files contains a file that was created in the same week as the first file. + // If groupType is month, it should return true if the list of modified files contains a file that was created in the same month as the first file. + // If groupType is year, it should return true if the list of modified files contains a file that was created in the same year as the first file. + bool areModifiedFilesPartOfGroup( + List modifiedFiles, + EnteFile fistFile, { + EnteFile? lastFile, + }) { + switch (this) { + case GroupType.day: + return modifiedFiles.any( + (file) => areFromSameDay(fistFile.creationTime!, file.creationTime!), + ); + case GroupType.week: + return modifiedFiles.any((file) { + final firstDate = + DateTime.fromMicrosecondsSinceEpoch(fistFile.creationTime!); + final fileDate = + DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + return areDatesInSameWeek(firstDate, fileDate); + }); + case GroupType.month: + return modifiedFiles.any((file) { + final firstDate = + DateTime.fromMicrosecondsSinceEpoch(fistFile.creationTime!); + final fileDate = + DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + return firstDate.year == fileDate.year && + firstDate.month == fileDate.month; + }); + case GroupType.year: + return modifiedFiles.any((file) { + final firstDate = + DateTime.fromMicrosecondsSinceEpoch(fistFile.creationTime!); + final fileDate = + DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + return firstDate.year == fileDate.year; + }); + default: + throw UnimplementedError("not implemented for $this"); + } + } + + // for day, year, month, year type, return the microsecond range of the group + (int, int) getGroupRange(EnteFile file) { + switch (this) { + case GroupType.day: + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final startOfDay = DateTime(date.year, date.month, date.day); + return ( + startOfDay.microsecondsSinceEpoch, + (startOfDay.microsecondsSinceEpoch + microSecondsInDay - 1), + ); + case GroupType.week: + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final startOfWeek = DateTime(date.year, date.month, date.day) + .subtract(Duration(days: date.weekday - 1)); + final endOfWeek = startOfWeek.add(const Duration(days: 7)); + return ( + startOfWeek.microsecondsSinceEpoch, + endOfWeek.microsecondsSinceEpoch - 1 + ); + case GroupType.month: + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final startOfMonth = DateTime(date.year, date.month); + final endOfMonth = DateTime(date.year, date.month + 1); + return ( + startOfMonth.microsecondsSinceEpoch, + endOfMonth.microsecondsSinceEpoch - 1 + ); + case GroupType.year: + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final startOfYear = DateTime(date.year); + final endOfYear = DateTime(date.year + 1); + return ( + startOfYear.microsecondsSinceEpoch, + endOfYear.microsecondsSinceEpoch - 1 + ); + default: + throw UnimplementedError("not implemented for $this"); + } + } + + bool areFromSameGroup(EnteFile first, EnteFile second) { + switch (this) { + case GroupType.day: + return areFromSameDay(first.creationTime!, second.creationTime!); + case GroupType.month: + return DateTime.fromMicrosecondsSinceEpoch(first.creationTime!).year == + DateTime.fromMicrosecondsSinceEpoch(second.creationTime!) + .year && + DateTime.fromMicrosecondsSinceEpoch(first.creationTime!).month == + DateTime.fromMicrosecondsSinceEpoch(second.creationTime!).month; + case GroupType.year: + return DateTime.fromMicrosecondsSinceEpoch(first.creationTime!).year == + DateTime.fromMicrosecondsSinceEpoch(second.creationTime!).year; + case GroupType.week: + final firstDate = + DateTime.fromMicrosecondsSinceEpoch(first.creationTime!); + final secondDate = + DateTime.fromMicrosecondsSinceEpoch(second.creationTime!); + return areDatesInSameWeek(firstDate, secondDate); + default: + throw UnimplementedError("not implemented for $this"); + } + } + + String _getDayTitle(BuildContext context, int timestamp) { + final date = DateTime.fromMicrosecondsSinceEpoch(timestamp); + final now = DateTime.now(); + if (date.year == now.year && date.month == now.month) { + if (date.day == now.day) { + return S.of(context).dayToday; + } else if (date.day == now.day - 1) { + return S.of(context).dayYesterday; + } + } + if (date.year != DateTime.now().year) { + return DateFormat.yMMMEd(Localizations.localeOf(context).languageCode) + .format(date); + } else { + return DateFormat.MMMEd(Localizations.localeOf(context).languageCode) + .format(date); + } + } +} diff --git a/mobile/lib/ui/viewer/gallery/gallery.dart b/mobile/lib/ui/viewer/gallery/gallery.dart index 8213158f9..45a732ad0 100644 --- a/mobile/lib/ui/viewer/gallery/gallery.dart +++ b/mobile/lib/ui/viewer/gallery/gallery.dart @@ -12,10 +12,10 @@ import 'package:photos/models/file/file.dart'; import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/ui/common/loading_widget.dart'; +import "package:photos/ui/viewer/gallery/component/group/type.dart"; import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.dart"; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; -import 'package:photos/utils/date_time_util.dart'; import "package:photos/utils/debouncer.dart"; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -59,6 +59,7 @@ class Gallery extends StatefulWidget { // add a Function variable to get sort value in bool final SortAscFn? sortAsyncFn; + final GroupType groupType; const Gallery({ required this.asyncLoader, @@ -73,6 +74,7 @@ class Gallery extends StatefulWidget { this.emptyState = const EmptyState(), this.scrollBottomSafeArea = 120.0, this.albumName = '', + this.groupType = GroupType.day, this.enableFileGrouping = true, this.loadingWidget = const EnteLoadingWidget(), this.disableScroll = false, @@ -248,6 +250,7 @@ class GalleryState extends State { return GalleryContextState( sortOrderAsc: _sortOrderAsc, inSelectionMode: widget.inSelectionMode, + type: widget.groupType, child: MultipleGroupsGalleryView( itemScroller: _itemScroller, groupedFiles: currentGroupedFiles, @@ -273,13 +276,11 @@ class GalleryState extends State { List> _groupFiles(List files) { List dailyFiles = []; + final List> resultGroupedFiles = []; for (int index = 0; index < files.length; index++) { if (index > 0 && - !areFromSameDay( - files[index - 1].creationTime!, - files[index].creationTime!, - )) { + !widget.groupType.areFromSameGroup(files[index - 1], files[index])) { resultGroupedFiles.add(dailyFiles); dailyFiles = []; } diff --git a/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart b/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart index ada72b6f3..4b202c39b 100644 --- a/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart +++ b/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart @@ -1,12 +1,15 @@ import "package:flutter/material.dart"; +import "package:photos/ui/viewer/gallery/component/group/type.dart"; class GalleryContextState extends InheritedWidget { ///Sorting by creation time final bool sortOrderAsc; final bool inSelectionMode; + final GroupType type; const GalleryContextState({ this.inSelectionMode = false, + this.type = GroupType.day, required this.sortOrderAsc, required Widget child, Key? key, @@ -19,6 +22,7 @@ class GalleryContextState extends InheritedWidget { @override bool updateShouldNotify(GalleryContextState oldWidget) { return sortOrderAsc != oldWidget.sortOrderAsc || - inSelectionMode != oldWidget.inSelectionMode; + inSelectionMode != oldWidget.inSelectionMode || + type != oldWidget.type; } } diff --git a/mobile/lib/utils/date_time_util.dart b/mobile/lib/utils/date_time_util.dart index 942896859..d60cd5d79 100644 --- a/mobile/lib/utils/date_time_util.dart +++ b/mobile/lib/utils/date_time_util.dart @@ -28,6 +28,27 @@ bool areFromSameDay(int firstCreationTime, int secondCreationTime) { firstDate.day == secondDate.day; } +bool areDatesInSameWeek(DateTime date1, DateTime date2) { + if (date1.year == date2.year && + date1.month == date2.month && + date1.day == date2.day) { + return true; + } + final int dayOfWeek1 = date1.weekday; + final int dayOfWeek2 = date2.weekday; + // Calculate the start and end dates of the week for both dates + final DateTime startOfWeek1 = date1.subtract(Duration(days: dayOfWeek1 - 1)); + final DateTime endOfWeek1 = startOfWeek1.add(const Duration(days: 6)); + final DateTime startOfWeek2 = date2.subtract(Duration(days: dayOfWeek2 - 1)); + final DateTime endOfWeek2 = startOfWeek2.add(const Duration(days: 6)); + // Check if the two dates fall within the same week range + if ((date1.isAfter(startOfWeek2) && date1.isBefore(endOfWeek2)) || + (date2.isAfter(startOfWeek1) && date2.isBefore(endOfWeek1))) { + return true; + } + return false; +} + // Create link default names: // Same day: "Dec 19, 2022" // Same month: "Dec 19 - 22, 2022"