From bf93e28a3dfedaa6e2bd5aa54b2a3e68ba68b076 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 25 Jun 2025 09:19:56 +0530 Subject: [PATCH 1/5] Improve UX of FullScreenMemory widget --- .../reset_zoom_of_photo_view_event.dart | 18 +++ .../lib/ui/home/memories/custom_listener.dart | 150 ++++++++++++++++++ .../ui/home/memories/full_screen_memory.dart | 63 ++++++-- mobile/lib/ui/viewer/file/zoomable_image.dart | 13 ++ 4 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 mobile/lib/events/reset_zoom_of_photo_view_event.dart create mode 100644 mobile/lib/ui/home/memories/custom_listener.dart diff --git a/mobile/lib/events/reset_zoom_of_photo_view_event.dart b/mobile/lib/events/reset_zoom_of_photo_view_event.dart new file mode 100644 index 0000000000..fd25ad6917 --- /dev/null +++ b/mobile/lib/events/reset_zoom_of_photo_view_event.dart @@ -0,0 +1,18 @@ +import "package:photos/events/event.dart"; + +class ResetZoomOfPhotoView extends Event { + final int? uploadedFileID; + final String? localID; + + ResetZoomOfPhotoView({ + required this.localID, + required this.uploadedFileID, + }); + + bool isSamePhoto({required int? uploadedFileID, required String? localID}) { + if (this.uploadedFileID == uploadedFileID && this.localID == localID) { + return true; + } + return false; + } +} diff --git a/mobile/lib/ui/home/memories/custom_listener.dart b/mobile/lib/ui/home/memories/custom_listener.dart new file mode 100644 index 0000000000..981f9919cc --- /dev/null +++ b/mobile/lib/ui/home/memories/custom_listener.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; + +class ActivePointers with ChangeNotifier { + final Set _activePointers = {}; + bool get hasActivePointers => _activePointers.isNotEmpty; + bool activePointerWasPartOfMultitouch = false; + + void add(int pointer) { + if (_activePointers.isNotEmpty && !_activePointers.contains(pointer)) { + activePointerWasPartOfMultitouch = true; + } + _activePointers.add(pointer); + notifyListeners(); + } + + void remove(int pointer) { + _activePointers.remove(pointer); + if (_activePointers.isEmpty) { + activePointerWasPartOfMultitouch = false; + } + notifyListeners(); + } +} + +/// `onLongPress` and `onLongPressUp` have not been tested enough to make sure +/// it works as expected, so they are commented out for now. +class MemoriesPointerGestureListener extends StatefulWidget { + final Widget child; + final Function(PointerEvent)? onTap; + // final VoidCallback? onLongPress; + // final VoidCallback? onLongPressUp; + + /// How long the pointer must stay down before a long‐press fires. + final Duration longPressDuration; + + /// Maximum movement (in logical pixels) before we consider it a drag. + final double touchSlop; + + /// Notifier that indicates whether there are active pointers. + final ValueNotifier? hasPointerNotifier; + static const double kTouchSlop = 18.0; // Default touch slop value + + const MemoriesPointerGestureListener({ + super.key, + required this.child, + this.onTap, + // this.onLongPress, + // this.onLongPressUp, + this.hasPointerNotifier, + this.longPressDuration = const Duration(milliseconds: 500), + this.touchSlop = kTouchSlop, // from flutter/gestures/constants.dart + }); + + @override + MemoriesPointerGestureListenerState createState() => + MemoriesPointerGestureListenerState(); +} + +class MemoriesPointerGestureListenerState + extends State { + Timer? _longPressTimer; + bool _longPressFired = false; + Offset? _downPosition; + bool hasPointerMoved = false; + final _activePointers = ActivePointers(); + + @override + void initState() { + super.initState(); + _activePointers.addListener(_activatePointerListener); + } + + void _activatePointerListener() { + if (widget.hasPointerNotifier != null) { + widget.hasPointerNotifier!.value = _activePointers.hasActivePointers; + } + } + + void _handlePointerDown(PointerDownEvent event) { + _addPointer(event.pointer); + _downPosition = event.localPosition; + _longPressFired = false; + _longPressTimer?.cancel(); + _longPressTimer = Timer(widget.longPressDuration, () { + _longPressFired = true; + // widget.onLongPress?.call(); + }); + } + + void _handlePointerMove(PointerMoveEvent event) { + if (_longPressTimer != null && _downPosition != null) { + final distance = (event.localPosition - _downPosition!).distance; + if (distance > widget.touchSlop) { + // user started dragging – cancel long‐press + hasPointerMoved = true; + _longPressTimer!.cancel(); + _longPressTimer = null; + } + } + } + + void _handlePointerUp(PointerUpEvent event) { + _longPressTimer?.cancel(); + _longPressTimer = null; + + if (_longPressFired) { + // widget.onLongPressUp?.call(); + } else { + if (!_activePointers.activePointerWasPartOfMultitouch && + !hasPointerMoved) { + widget.onTap?.call(event); + } + } + _removePointer(event.pointer); + _reset(); + } + + void _handlePointerCancel(PointerCancelEvent event) { + _longPressTimer?.cancel(); + _longPressTimer = null; + _longPressFired = false; + _removePointer(event.pointer); + _reset(); + } + + void _removePointer(int pointer) { + _activePointers.remove(pointer); + } + + void _addPointer(int pointer) { + _activePointers.add(pointer); + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: _handlePointerDown, + onPointerMove: _handlePointerMove, + onPointerUp: _handlePointerUp, + onPointerCancel: _handlePointerCancel, + behavior: HitTestBehavior.opaque, + child: widget.child, + ); + } + + void _reset() { + hasPointerMoved = false; + } +} diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index f9e54eb519..0f598a5545 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -6,6 +6,8 @@ import "dart:ui"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:photos/core/configuration.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/reset_zoom_of_photo_view_event.dart"; import "package:photos/models/file/file_type.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/service_locator.dart"; @@ -13,6 +15,7 @@ import "package:photos/services/smart_memories_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/actions/file/file_actions.dart"; +import "package:photos/ui/home/memories/custom_listener.dart"; import "package:photos/ui/home/memories/memory_progress_indicator.dart"; import "package:photos/ui/viewer/file/file_widget.dart"; @@ -146,22 +149,52 @@ class _FullScreenMemoryState extends State { final ValueNotifier durationNotifier = ValueNotifier(const Duration(seconds: 5)); + /// Used to check if any pointer is on the screen. + final hasPointerOnScreenNotifier = ValueNotifier(false); + @override void initState() { super.initState(); Future.delayed(const Duration(seconds: 3), () { if (mounted) _showTitle.value = false; }); + hasPointerOnScreenNotifier.addListener( + _hasPointerListener, + ); } @override void dispose() { _showTitle.dispose(); durationNotifier.dispose(); + hasPointerOnScreenNotifier.removeListener(_hasPointerListener); + super.dispose(); } - void _toggleAnimation(bool pause) { + /// Used to check if user has touched the screen and then to pause animation + /// and once the pointer is removed from the screen, it resumes the animation + /// It also resets the zoom of the photo view to default for better user + /// experience after finger(s) is removed from the screen after zooming in by + /// pinching. + void _hasPointerListener() { + if (hasPointerOnScreenNotifier.value) { + _toggleAnimation(pause: true); + } else { + _toggleAnimation(pause: false); + final inheritedData = FullScreenMemoryData.of(context)!; + final currentFile = + inheritedData.memories[inheritedData.indexNotifier.value].file; + Bus.instance.fire( + ResetZoomOfPhotoView( + localID: currentFile.localID, + uploadedFileID: currentFile.uploadedFileID, + ), + ); + } + } + + void _toggleAnimation({required bool pause}) { if (pause) { _progressAnimationController?.stop(); _zoomAnimationController?.stop(); @@ -325,19 +358,17 @@ class _FullScreenMemoryState extends State { final isVideo = currentMemory.file.fileType == FileType.video; final currentFile = currentMemory.file; - return GestureDetector( - onTapUp: (TapUpDetails details) { + return MemoriesPointerGestureListener( + onTap: (PointerEvent event) { final screenWidth = MediaQuery.sizeOf(context).width; - final edgeWidth = screenWidth * 0.20; - if (details.localPosition.dx < edgeWidth) { + final goToPreviousTapAreaWidth = screenWidth * 0.20; + if (event.localPosition.dx < goToPreviousTapAreaWidth) { _goToPrevious(inheritedData); - } else if (details.localPosition.dx > - screenWidth - edgeWidth) { + } else { _goToNext(inheritedData); } }, - onLongPress: () => isVideo ? null : _toggleAnimation(true), - onLongPressUp: () => isVideo ? null : _toggleAnimation(false), + hasPointerNotifier: hasPointerOnScreenNotifier, child: MemoriesZoomWidget( scaleController: (controller) { _zoomAnimationController = controller; @@ -352,7 +383,7 @@ class _FullScreenMemoryState extends State { const BoxDecoration(color: Colors.transparent), isFromMemories: true, playbackCallback: (isPlaying) { - _toggleAnimation(!isPlaying); + _toggleAnimation(pause: !isPlaying); }, onFinalFileLoad: ({required int memoryDuration}) { onFinalFileLoad(memoryDuration); @@ -424,9 +455,9 @@ class BottomIcons extends StatelessWidget { color: Colors.white, //same for both themes ), onPressed: () async { - fullScreenState?._toggleAnimation(true); + fullScreenState?._toggleAnimation(pause: true); await showDetailsSheet(context, currentFile); - fullScreenState?._toggleAnimation(false); + fullScreenState?._toggleAnimation(pause: false); }, ), ]; @@ -442,7 +473,7 @@ class BottomIcons extends StatelessWidget { color: Colors.white, //same for both themes ), onPressed: () async { - fullScreenState?._toggleAnimation(true); + fullScreenState?._toggleAnimation(pause: true); await showSingleFileDeleteSheet( context, inheritedData @@ -455,7 +486,7 @@ class BottomIcons extends StatelessWidget { }, }, ); - fullScreenState?._toggleAnimation(false); + fullScreenState?._toggleAnimation(pause: false); }, ), SizedBox( @@ -471,9 +502,9 @@ class BottomIcons extends StatelessWidget { color: Colors.white, //same for both themes ), onPressed: () async { - fullScreenState?._toggleAnimation(true); + fullScreenState?._toggleAnimation(pause: true); await share(context, [currentFile]); - fullScreenState?._toggleAnimation(false); + fullScreenState?._toggleAnimation(pause: false); }, ), ); diff --git a/mobile/lib/ui/viewer/file/zoomable_image.dart b/mobile/lib/ui/viewer/file/zoomable_image.dart index aa69d1cd1d..45f4f2b016 100644 --- a/mobile/lib/ui/viewer/file/zoomable_image.dart +++ b/mobile/lib/ui/viewer/file/zoomable_image.dart @@ -13,6 +13,7 @@ import 'package:photos/db/files_db.dart'; import "package:photos/events/file_caption_updated_event.dart"; import "package:photos/events/files_updated_event.dart"; import 'package:photos/events/local_photos_updated_event.dart'; +import "package:photos/events/reset_zoom_of_photo_view_event.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/states/detail_page_state.dart"; @@ -64,6 +65,7 @@ class _ZoomableImageState extends State { final _scaleStateController = PhotoViewScaleStateController(); late final StreamSubscription _captionUpdatedSubscription; + late final StreamSubscription _resetZoomSubscription; // This is to prevent the app from crashing when loading 200MP images // https://github.com/flutter/flutter/issues/110331 @@ -92,6 +94,16 @@ class _ZoomableImageState extends State { } } }); + + _resetZoomSubscription = + Bus.instance.on().listen((event) { + if (event.isSamePhoto( + uploadedFileID: widget.photo.uploadedFileID, + localID: widget.photo.localID, + )) { + _scaleStateController.scaleState = PhotoViewScaleState.initial; + } + }); } @override @@ -99,6 +111,7 @@ class _ZoomableImageState extends State { _photoViewController.dispose(); _scaleStateController.dispose(); _captionUpdatedSubscription.cancel(); + _resetZoomSubscription.cancel(); super.dispose(); } From 88498f11fca21436ba25332e8ef63ffef3dc8191 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 25 Jun 2025 10:45:19 +0530 Subject: [PATCH 2/5] Start scale animation in FullScreenMemory widget only if final image is loaded --- mobile/lib/ui/home/memories/full_screen_memory.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index 0f598a5545..75a5264eef 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -41,6 +41,8 @@ import "package:photos/utils/share_util.dart"; //ValueNotifier inside the InheritedWidget and the widgets that need to change //are wrapped in a ValueListenableBuilder. +//TODO: Use better naming convention. "Memory" should be a whole memory and +//parts of the memory should be called "items". class FullScreenMemoryDataUpdater extends StatefulWidget { final List memories; final int initialIndex; @@ -151,6 +153,7 @@ class _FullScreenMemoryState extends State { /// Used to check if any pointer is on the screen. final hasPointerOnScreenNotifier = ValueNotifier(false); + bool hasFinalFileLoaded = false; @override void initState() { @@ -199,8 +202,10 @@ class _FullScreenMemoryState extends State { _progressAnimationController?.stop(); _zoomAnimationController?.stop(); } else { - _progressAnimationController?.forward(); - _zoomAnimationController?.forward(); + if (hasFinalFileLoaded) { + _progressAnimationController?.forward(); + _zoomAnimationController?.forward(); + } } } @@ -214,6 +219,7 @@ class _FullScreenMemoryState extends State { } void onFinalFileLoad(int duration) { + hasFinalFileLoaded = true; if (_progressAnimationController?.isAnimating == true) { _progressAnimationController!.stop(); } @@ -230,6 +236,7 @@ class _FullScreenMemoryState extends State { } void _goToNext(FullScreenMemoryData inheritedData) { + hasFinalFileLoaded = false; final currentIndex = inheritedData.indexNotifier.value; if (currentIndex < inheritedData.memories.length - 1) { inheritedData.indexNotifier.value += 1; @@ -240,6 +247,7 @@ class _FullScreenMemoryState extends State { } void _goToPrevious(FullScreenMemoryData inheritedData) { + hasFinalFileLoaded = false; final currentIndex = inheritedData.indexNotifier.value; if (currentIndex > 0) { inheritedData.indexNotifier.value -= 1; From 24507f5f23d8c35e832a89246739abb53da0676b Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 25 Jun 2025 11:11:43 +0530 Subject: [PATCH 3/5] Use better name --- mobile/lib/ui/home/memories/full_screen_memory.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index 75a5264eef..934dd1c8ac 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -353,7 +353,7 @@ class _FullScreenMemoryState extends State { body: Stack( alignment: Alignment.bottomCenter, children: [ - const MemoryBackDrop(), + const _MemoryBlur(), ValueListenableBuilder( valueListenable: inheritedData.indexNotifier, builder: (context, index, _) { @@ -576,8 +576,8 @@ class BottomGradient extends StatelessWidget { } } -class MemoryBackDrop extends StatelessWidget { - const MemoryBackDrop({super.key}); +class _MemoryBlur extends StatelessWidget { + const _MemoryBlur(); @override Widget build(BuildContext context) { From 86446ab8bf1c8a5a6249595d60ab946754c04085 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 25 Jun 2025 15:55:04 +0530 Subject: [PATCH 4/5] Improve FullScreenMemeory UI --- .../ui/home/memories/all_memories_page.dart | 34 +- .../ui/home/memories/full_screen_memory.dart | 358 ++++++++++-------- 2 files changed, 209 insertions(+), 183 deletions(-) diff --git a/mobile/lib/ui/home/memories/all_memories_page.dart b/mobile/lib/ui/home/memories/all_memories_page.dart index f01df12620..7180ab4203 100644 --- a/mobile/lib/ui/home/memories/all_memories_page.dart +++ b/mobile/lib/ui/home/memories/all_memories_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:photos/models/memories/memory.dart"; +import "package:photos/theme/colors.dart"; import "package:photos/ui/home/memories/full_screen_memory.dart"; class AllMemoriesPage extends StatefulWidget { @@ -47,27 +48,26 @@ class _AllMemoriesPageState extends State return FullScreenMemoryDataUpdater( initialIndex: initialMemoryIndex, memories: widget.allMemories[index], - child: ClipRRect( - child: FullScreenMemory( - widget.allTitles[index], - initialMemoryIndex, - onNextMemory: index < widget.allMemories.length - 1 - ? () => pageController.nextPage( - duration: const Duration(milliseconds: 250), - curve: Curves.ease, - ) - : null, - onPreviousMemory: index > 0 - ? () => pageController.previousPage( - duration: const Duration(milliseconds: 250), - curve: Curves.ease, - ) - : null, - ), + child: FullScreenMemory( + widget.allTitles[index], + initialMemoryIndex, + onNextMemory: index < widget.allMemories.length - 1 + ? () => pageController.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + ) + : null, + onPreviousMemory: index > 0 + ? () => pageController.previousPage( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + ) + : null, ), ); }, ), + backgroundColor: backgroundBaseDark, ); } diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index 934dd1c8ac..3b4a3ad67e 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -270,174 +270,203 @@ class _FullScreenMemoryState extends State { @override Widget build(BuildContext context) { + final screenPadding = MediaQuery.paddingOf(context); final inheritedData = FullScreenMemoryData.of(context)!; final showStepProgressIndicator = inheritedData.memories.length < 60; - return Scaffold( - backgroundColor: Colors.black, - extendBodyBehindAppBar: true, - appBar: AppBar( - toolbarHeight: 84, - automaticallyImplyLeading: false, - title: ValueListenableBuilder( - valueListenable: inheritedData.indexNotifier, - child: InkWell( - onTap: () => Navigator.pop(context), - child: const Padding( - padding: EdgeInsets.fromLTRB(4, 8, 8, 8), - child: Icon(Icons.close, color: Colors.white), - ), - ), - builder: (context, value, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - showStepProgressIndicator - ? ValueListenableBuilder( - valueListenable: durationNotifier, - builder: (context, duration, _) { - return MemoryProgressIndicator( - totalSteps: inheritedData.memories.length, - currentIndex: value, - selectedColor: Colors.white, - unselectedColor: Colors.white.withOpacity(0.4), - duration: duration, - animationController: (controller) { - _progressAnimationController = controller; - }, - onComplete: () { - _goToNext(inheritedData); - }, - ); - }, - ) - : const SizedBox.shrink(), - const SizedBox(height: 10), - Row( - children: [ - child!, - Text( - SmartMemoriesService.getDateFormatted( - creationTime: - inheritedData.memories[value].file.creationTime!, - context: context, - ), - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - color: Colors.white, - ), - ), - ], - ), - ], - ); - }, - ), - flexibleSpace: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black54, - Colors.black45, - Colors.transparent, - ], - stops: [0, 0.6, 1], - ), - ), - ), - backgroundColor: Colors.transparent, - elevation: 0, + return Padding( + padding: EdgeInsets.fromLTRB( + 8, + screenPadding.top + 8, + 8, + screenPadding.bottom + 8, ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - const _MemoryBlur(), - ValueListenableBuilder( - valueListenable: inheritedData.indexNotifier, - builder: (context, index, _) { - if (index < inheritedData.memories.length - 1) { - final nextFile = inheritedData.memories[index + 1].file; - preloadThumbnail(nextFile); - preloadFile(nextFile); - } - final currentMemory = inheritedData.memories[index]; - final isVideo = currentMemory.file.fileType == FileType.video; - final currentFile = currentMemory.file; - - return MemoriesPointerGestureListener( - onTap: (PointerEvent event) { - final screenWidth = MediaQuery.sizeOf(context).width; - final goToPreviousTapAreaWidth = screenWidth * 0.20; - if (event.localPosition.dx < goToPreviousTapAreaWidth) { - _goToPrevious(inheritedData); - } else { - _goToNext(inheritedData); - } - }, - hasPointerNotifier: hasPointerOnScreenNotifier, - child: MemoriesZoomWidget( - scaleController: (controller) { - _zoomAnimationController = controller; - }, - zoomIn: index % 2 == 0, - isVideo: isVideo, - child: FileWidget( - currentFile, - autoPlay: false, - tagPrefix: "memories", - backgroundDecoration: - const BoxDecoration(color: Colors.transparent), - isFromMemories: true, - playbackCallback: (isPlaying) { - _toggleAnimation(pause: !isPlaying); - }, - onFinalFileLoad: ({required int memoryDuration}) { - onFinalFileLoad(memoryDuration); - }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: getEnteColorScheme(context).strokeFainter, + width: 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Scaffold( + backgroundColor: Colors.black, + extendBodyBehindAppBar: true, + appBar: AppBar( + automaticallyImplyLeading: false, + title: ValueListenableBuilder( + valueListenable: inheritedData.indexNotifier, + child: InkWell( + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.fromLTRB(4, 8, 8, 8), + child: Icon(Icons.close, color: Colors.white), ), ), - ); - }, - ), - SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.only(bottom: 72), - child: ValueListenableBuilder( - valueListenable: _showTitle, - builder: (context, value, _) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeIn, - child: value - ? Padding( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), - child: Hero( - tag: widget.title, - child: Text( - widget.title, - style: getEnteTextTheme(context) - .largeBold - .copyWith( - color: Colors.white, - ), - ), + builder: (context, value, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + showStepProgressIndicator + ? ValueListenableBuilder( + valueListenable: durationNotifier, + builder: (context, duration, _) { + return MemoryProgressIndicator( + totalSteps: inheritedData.memories.length, + currentIndex: value, + selectedColor: Colors.white, + unselectedColor: + Colors.white.withOpacity(0.4), + duration: duration, + animationController: (controller) { + _progressAnimationController = controller; + }, + onComplete: () { + _goToNext(inheritedData); + }, + ); + }, + ) + : const SizedBox.shrink(), + const SizedBox(height: 10), + Row( + children: [ + child!, + Text( + SmartMemoriesService.getDateFormatted( + creationTime: inheritedData + .memories[value].file.creationTime!, + context: context, ), - ) - : showStepProgressIndicator - ? const SizedBox.shrink() - : const MemoryCounter(), + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + color: Colors.white, + ), + ), + ], + ), + ], ); }, ), + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black54, + Colors.black45, + Colors.transparent, + ], + stops: [0, 0.6, 1], + ), + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: Stack( + alignment: Alignment.bottomCenter, + children: [ + const _MemoryBlur(), + ValueListenableBuilder( + valueListenable: inheritedData.indexNotifier, + builder: (context, index, _) { + if (index < inheritedData.memories.length - 1) { + final nextFile = inheritedData.memories[index + 1].file; + preloadThumbnail(nextFile); + preloadFile(nextFile); + } + final currentMemory = inheritedData.memories[index]; + final isVideo = + currentMemory.file.fileType == FileType.video; + final currentFile = currentMemory.file; + + return MemoriesPointerGestureListener( + onTap: (PointerEvent event) { + final screenWidth = MediaQuery.sizeOf(context).width; + final goToPreviousTapAreaWidth = screenWidth * 0.20; + if (event.localPosition.dx < goToPreviousTapAreaWidth) { + _goToPrevious(inheritedData); + } else { + _goToNext(inheritedData); + } + }, + hasPointerNotifier: hasPointerOnScreenNotifier, + child: MemoriesZoomWidget( + key: ValueKey( + currentFile.uploadedFileID ?? currentFile.localID, + ), + scaleController: (controller) { + _zoomAnimationController = controller; + }, + zoomIn: index % 2 == 0, + isVideo: isVideo, + child: FileWidget( + currentFile, + autoPlay: false, + tagPrefix: "memories", + backgroundDecoration: + const BoxDecoration(color: Colors.transparent), + isFromMemories: true, + playbackCallback: (isPlaying) { + _toggleAnimation(pause: !isPlaying); + }, + onFinalFileLoad: ({required int memoryDuration}) { + onFinalFileLoad(memoryDuration); + }, + ), + ), + ); + }, + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + ValueListenableBuilder( + valueListenable: _showTitle, + builder: (context, value, _) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: value + ? Padding( + padding: + const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Hero( + tag: widget.title, + child: Text( + widget.title, + style: getEnteTextTheme(context) + .largeBold + .copyWith( + color: Colors.white, + ), + ), + ), + ) + : showStepProgressIndicator + ? const SizedBox.shrink() + : const MemoryCounter(), + ); + }, + ), + const BottomIcons(), + ], + ), + const BottomGradient(), + ], ), ), - const BottomGradient(), - const BottomIcons(), - ], + ), ), ); } @@ -517,15 +546,12 @@ class BottomIcons extends StatelessWidget { ), ); - return SafeArea( - top: false, - child: Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.fromLTRB(12, 0, 12, 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: rowChildren, - ), + return Container( + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.fromLTRB(12, 0, 12, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: rowChildren, ), ); }, @@ -558,7 +584,7 @@ class BottomGradient extends StatelessWidget { Widget build(BuildContext context) { return IgnorePointer( child: Container( - height: 124, + height: 96, width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( From 324d2fbe4f6f9fa962130556deb0229f431abbb2 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 25 Jun 2025 16:17:17 +0530 Subject: [PATCH 5/5] Minor UI tweaks --- mobile/lib/ui/home/memories/full_screen_memory.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index 3b4a3ad67e..4b38c336ce 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -12,6 +12,7 @@ import "package:photos/models/file/file_type.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/smart_memories_service.dart"; +import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/actions/file/file_actions.dart"; @@ -276,16 +277,16 @@ class _FullScreenMemoryState extends State { return Padding( padding: EdgeInsets.fromLTRB( - 8, + 4, screenPadding.top + 8, - 8, + 4, screenPadding.bottom + 8, ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all( - color: getEnteColorScheme(context).strokeFainter, + color: strokeFainterDark, width: 1, ), ),