[mob][photos] Memories improvement (#6152)
## Description 1. A subtle zoom-in/out effect for photos and replaced the black area around landscape photos with a blurred background. 2. Auto play each image for 5 second and video for its duration with a step progress animation. 3. Long-press to pause animation. Releasing will resume the playback.
This commit is contained in:
@@ -75,6 +75,8 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- GoogleDataTransport (10.1.0):
|
||||
@@ -260,6 +262,7 @@ DEPENDENCIES:
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_editor_common (from `.symlinks/plugins/image_editor_common/ios`)
|
||||
@@ -300,7 +303,7 @@ DEPENDENCIES:
|
||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
|
||||
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git:
|
||||
- ffmpeg_kit_custom
|
||||
trunk:
|
||||
- Firebase
|
||||
@@ -361,6 +364,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_sodium/ios"
|
||||
flutter_timezone:
|
||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||
flutter_timezone:
|
||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
home_widget:
|
||||
@@ -445,12 +450,23 @@ SPEC CHECKSUMS:
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
|
||||
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
|
||||
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
@@ -459,7 +475,7 @@ SPEC CHECKSUMS:
|
||||
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
|
||||
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
|
||||
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
|
||||
@@ -472,7 +488,13 @@ SPEC CHECKSUMS:
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
@@ -482,19 +504,35 @@ SPEC CHECKSUMS:
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
@@ -507,7 +545,6 @@ SPEC CHECKSUMS:
|
||||
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
|
||||
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
|
||||
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
|
||||
thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41
|
||||
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
|
||||
18
mobile/lib/events/reset_zoom_of_photo_view_event.dart
Normal file
18
mobile/lib/events/reset_zoom_of_photo_view_event.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import "dart:async";
|
||||
import "dart:io" show File;
|
||||
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/foundation.dart" show kDebugMode;
|
||||
import "package:flutter/material.dart" show BuildContext;
|
||||
import "package:logging/logging.dart";
|
||||
@@ -24,6 +25,8 @@ import "package:photos/services/language_service.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
|
||||
import "package:photos/services/notification_service.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/ui/home/memories/all_memories_page.dart";
|
||||
import "package:photos/ui/home/memories/full_screen_memory.dart";
|
||||
import "package:photos/ui/viewer/people/people_page.dart";
|
||||
import "package:photos/utils/cache_util.dart";
|
||||
@@ -485,10 +488,12 @@ class MemoriesCacheService {
|
||||
}
|
||||
await routeToPage(
|
||||
context,
|
||||
FullScreenMemoryDataUpdater(
|
||||
initialIndex: fileIdx,
|
||||
memories: allMemories[memoryIdx].memories,
|
||||
child: FullScreenMemory(allMemories[memoryIdx].title, fileIdx),
|
||||
AllMemoriesPage(
|
||||
allMemories: _cachedMemories!.map((e) => e.memories).toList(),
|
||||
allTitles: _cachedMemories!.map((e) => e.title).toList(),
|
||||
initialPageIndex: memoryIdx,
|
||||
inititalFileIndex: fileIdx,
|
||||
isFromWidgetOrNotifications: true,
|
||||
),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
@@ -515,10 +520,12 @@ class MemoriesCacheService {
|
||||
}
|
||||
await routeToPage(
|
||||
context,
|
||||
FullScreenMemoryDataUpdater(
|
||||
initialIndex: 0,
|
||||
memories: allMemories[memoryIdx].memories,
|
||||
child: FullScreenMemory(allMemories[memoryIdx].title, 0),
|
||||
AllMemoriesPage(
|
||||
allMemories: allMemories.map((e) => e.memories).toList(),
|
||||
allTitles: allMemories.map((e) => e.title).toList(),
|
||||
initialPageIndex: memoryIdx,
|
||||
inititalFileIndex: 0,
|
||||
isFromWidgetOrNotifications: true,
|
||||
),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
@@ -582,7 +589,12 @@ class MemoriesCacheService {
|
||||
FullScreenMemoryDataUpdater(
|
||||
initialIndex: 0,
|
||||
memories: personMemory!.memories,
|
||||
child: FullScreenMemory(personMemory.title, 0),
|
||||
child: Container(
|
||||
color: backgroundBaseDark,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: FullScreenMemory(personMemory.title, 0),
|
||||
),
|
||||
),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
|
||||
106
mobile/lib/ui/home/memories/all_memories_page.dart
Normal file
106
mobile/lib/ui/home/memories/all_memories_page.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
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";
|
||||
|
||||
// TODO: Use a single instance variable for `allMemories` and `allTitles`
|
||||
class AllMemoriesPage extends StatefulWidget {
|
||||
final int initialPageIndex;
|
||||
final int inititalFileIndex;
|
||||
final List<List<Memory>> allMemories;
|
||||
final List<String> allTitles;
|
||||
final bool isFromWidgetOrNotifications;
|
||||
|
||||
const AllMemoriesPage({
|
||||
super.key,
|
||||
required this.allMemories,
|
||||
required this.allTitles,
|
||||
required this.initialPageIndex,
|
||||
this.inititalFileIndex = 0,
|
||||
this.isFromWidgetOrNotifications = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AllMemoriesPage> createState() => _AllMemoriesPageState();
|
||||
}
|
||||
|
||||
class _AllMemoriesPageState extends State<AllMemoriesPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late PageController pageController;
|
||||
bool isFirstLoad = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
pageController = PageController(initialPage: widget.initialPageIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: backgroundBaseDark,
|
||||
child: PageView.builder(
|
||||
controller: pageController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
itemCount: widget.allMemories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final initialMemoryIndex =
|
||||
widget.isFromWidgetOrNotifications && isFirstLoad
|
||||
? widget.inititalFileIndex
|
||||
: _getNextMemoryIndex(index);
|
||||
isFirstLoad = false;
|
||||
return FullScreenMemoryDataUpdater(
|
||||
initialIndex: initialMemoryIndex,
|
||||
memories: widget.allMemories[index],
|
||||
child: FullScreenMemory(
|
||||
widget.allTitles[index],
|
||||
initialMemoryIndex,
|
||||
onNextMemory: index < widget.allMemories.length - 1
|
||||
? () => pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 675),
|
||||
curve: Curves.easeOutQuart,
|
||||
)
|
||||
: null,
|
||||
onPreviousMemory: index > 0
|
||||
? () => pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 675),
|
||||
curve: Curves.easeOutQuart,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _getNextMemoryIndex(int currentIndex) {
|
||||
int lastSeenIndex = 0;
|
||||
int lastSeenTimestamp = 0;
|
||||
final allMemoriesLength = widget.allMemories[currentIndex].length;
|
||||
for (var index = 0; index < allMemoriesLength; index++) {
|
||||
final memory = widget.allMemories[currentIndex][index];
|
||||
if (!memory.isSeen()) {
|
||||
return index;
|
||||
} else {
|
||||
if (memory.seenTime() > lastSeenTimestamp) {
|
||||
lastSeenIndex = index;
|
||||
lastSeenTimestamp = memory.seenTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastSeenIndex == widget.allMemories[currentIndex].length - 1) {
|
||||
return 0;
|
||||
}
|
||||
return lastSeenIndex + 1;
|
||||
}
|
||||
}
|
||||
150
mobile/lib/ui/home/memories/custom_listener.dart
Normal file
150
mobile/lib/ui/home/memories/custom_listener.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ActivePointers with ChangeNotifier {
|
||||
final Set<int> _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<bool>? 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<MemoriesPointerGestureListener> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
import "dart:math";
|
||||
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";
|
||||
import "package:photos/services/smart_memories_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/theme/colors.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";
|
||||
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/favorite_widget.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/share_util.dart";
|
||||
import "package:step_progress_indicator/step_progress_indicator.dart";
|
||||
|
||||
//There are two states of variables that FullScreenMemory depends on:
|
||||
//1. The list of memories
|
||||
@@ -32,6 +39,8 @@ import "package:step_progress_indicator/step_progress_indicator.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<Memory> memories;
|
||||
final int initialIndex;
|
||||
@@ -118,9 +127,14 @@ class FullScreenMemoryData extends InheritedWidget {
|
||||
class FullScreenMemory extends StatefulWidget {
|
||||
final String title;
|
||||
final int initialIndex;
|
||||
final VoidCallback? onNextMemory;
|
||||
final VoidCallback? onPreviousMemory;
|
||||
|
||||
const FullScreenMemory(
|
||||
this.title,
|
||||
this.initialIndex, {
|
||||
this.onNextMemory,
|
||||
this.onPreviousMemory,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -129,198 +143,336 @@ class FullScreenMemory extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FullScreenMemoryState extends State<FullScreenMemory> {
|
||||
PageController? _pageController;
|
||||
final _showTitle = ValueNotifier<bool>(true);
|
||||
AnimationController? _progressAnimationController;
|
||||
AnimationController? _zoomAnimationController;
|
||||
final ValueNotifier<Duration> durationNotifier =
|
||||
ValueNotifier(const Duration(seconds: 5));
|
||||
|
||||
/// Used to check if any pointer is on the screen.
|
||||
final hasPointerOnScreenNotifier = ValueNotifier<bool>(false);
|
||||
bool hasFinalFileLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showTitle.value = false;
|
||||
});
|
||||
}
|
||||
if (mounted) _showTitle.value = false;
|
||||
});
|
||||
hasPointerOnScreenNotifier.addListener(
|
||||
_hasPointerListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController?.dispose();
|
||||
_showTitle.dispose();
|
||||
durationNotifier.dispose();
|
||||
hasPointerOnScreenNotifier.removeListener(_hasPointerListener);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 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();
|
||||
} else {
|
||||
if (hasFinalFileLoaded) {
|
||||
_progressAnimationController?.forward();
|
||||
_zoomAnimationController?.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _resetAnimation() {
|
||||
_progressAnimationController
|
||||
?..stop()
|
||||
..reset();
|
||||
_zoomAnimationController
|
||||
?..stop()
|
||||
..reset();
|
||||
}
|
||||
|
||||
void onFinalFileLoad(int duration) {
|
||||
hasFinalFileLoaded = true;
|
||||
if (_progressAnimationController?.isAnimating == true) {
|
||||
_progressAnimationController!.stop();
|
||||
}
|
||||
durationNotifier.value = Duration(seconds: duration);
|
||||
_progressAnimationController
|
||||
?..stop()
|
||||
..reset()
|
||||
..duration = durationNotifier.value
|
||||
..forward();
|
||||
_zoomAnimationController
|
||||
?..stop()
|
||||
..reset()
|
||||
..forward();
|
||||
}
|
||||
|
||||
void _goToNext(FullScreenMemoryData inheritedData) {
|
||||
hasFinalFileLoaded = false;
|
||||
final currentIndex = inheritedData.indexNotifier.value;
|
||||
if (currentIndex < inheritedData.memories.length - 1) {
|
||||
inheritedData.indexNotifier.value += 1;
|
||||
_onPageChange(inheritedData, currentIndex + 1);
|
||||
} else if (widget.onNextMemory != null) {
|
||||
widget.onNextMemory!();
|
||||
}
|
||||
}
|
||||
|
||||
void _goToPrevious(FullScreenMemoryData inheritedData) {
|
||||
hasFinalFileLoaded = false;
|
||||
final currentIndex = inheritedData.indexNotifier.value;
|
||||
if (currentIndex > 0) {
|
||||
inheritedData.indexNotifier.value -= 1;
|
||||
_onPageChange(inheritedData, currentIndex - 1);
|
||||
} else if (widget.onPreviousMemory != null) {
|
||||
widget.onPreviousMemory!();
|
||||
}
|
||||
}
|
||||
|
||||
void _onPageChange(FullScreenMemoryData inheritedData, int index) {
|
||||
unawaited(
|
||||
memoriesCacheService.markMemoryAsSeen(
|
||||
inheritedData.memories[index],
|
||||
false,
|
||||
),
|
||||
);
|
||||
inheritedData.indexNotifier.value = index;
|
||||
_resetAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext 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, //same for both themes
|
||||
),
|
||||
),
|
||||
),
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
showStepProgressIndicator
|
||||
? StepProgressIndicator(
|
||||
totalSteps: inheritedData.memories.length,
|
||||
currentStep: value + 1,
|
||||
size: 2,
|
||||
selectedColor: Colors.white, //same for both themes
|
||||
unselectedColor: Colors.white.withOpacity(0.4),
|
||||
)
|
||||
: 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,
|
||||
), //same for both themes
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
flexibleSpace: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.6),
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.6, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: const Color(0x00000000),
|
||||
elevation: 0,
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
),
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _pageController ??= PageController(
|
||||
initialPage: widget.initialIndex,
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: strokeFainterDark,
|
||||
width: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (index < inheritedData.memories.length - 1) {
|
||||
final nextFile = inheritedData.memories[index + 1].file;
|
||||
preloadThumbnail(nextFile);
|
||||
preloadFile(nextFile);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final edgeWidth = screenWidth * 0.20;
|
||||
if (details.localPosition.dx < edgeWidth) {
|
||||
if (index > 0) {
|
||||
_pageController!.previousPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
} else if (details.localPosition.dx >
|
||||
screenWidth - edgeWidth) {
|
||||
if (index < (inheritedData.memories.length - 1)) {
|
||||
_pageController!.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: FileWidget(
|
||||
inheritedData.memories[index].file,
|
||||
autoPlay: false,
|
||||
tagPrefix: "memories",
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 64,
|
||||
primary: false,
|
||||
automaticallyImplyLeading: false,
|
||||
title: ValueListenableBuilder(
|
||||
valueListenable: inheritedData.indexNotifier,
|
||||
child: GestureDetector(
|
||||
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: [
|
||||
const SizedBox(height: 32),
|
||||
showStepProgressIndicator
|
||||
? ValueListenableBuilder<Duration>(
|
||||
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: 6),
|
||||
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: [
|
||||
Color.fromARGB(75, 0, 0, 0),
|
||||
Color.fromARGB(37, 0, 0, 0),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: [0, 0.45, 0.8, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onPageChanged: (index) {
|
||||
unawaited(
|
||||
memoriesCacheService.markMemoryAsSeen(
|
||||
inheritedData.memories[index],
|
||||
inheritedData.memories.length == index + 1,
|
||||
),
|
||||
);
|
||||
inheritedData.indexNotifier.value = index;
|
||||
},
|
||||
itemCount: inheritedData.memories.length,
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 72),
|
||||
child: ValueListenableBuilder(
|
||||
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, //same for both themes
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
const _MemoryBlur(),
|
||||
ValueListenableBuilder<int>(
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BottomGradient(showTitle: _showTitle),
|
||||
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(
|
||||
32,
|
||||
4,
|
||||
32,
|
||||
12,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: showStepProgressIndicator
|
||||
? const SizedBox.shrink()
|
||||
: const MemoryCounter(),
|
||||
);
|
||||
},
|
||||
valueListenable: _showTitle,
|
||||
child: Hero(
|
||||
tag: widget.title,
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 30,
|
||||
fontFamily: "Montserrat",
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
)
|
||||
: showStepProgressIndicator
|
||||
? const SizedBox.shrink()
|
||||
: const MemoryCounter(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const BottomIcons(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const BottomGradient(),
|
||||
const BottomIcons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -332,6 +484,8 @@ class BottomIcons extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final inheritedData = FullScreenMemoryData.of(context)!;
|
||||
final fullScreenState =
|
||||
context.findAncestorStateOfType<_FullScreenMemoryState>();
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: inheritedData.indexNotifier,
|
||||
@@ -343,8 +497,10 @@ class BottomIcons extends StatelessWidget {
|
||||
Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
|
||||
color: Colors.white, //same for both themes
|
||||
),
|
||||
onPressed: () {
|
||||
showDetailsSheet(context, currentFile);
|
||||
onPressed: () async {
|
||||
fullScreenState?._toggleAnimation(pause: true);
|
||||
await showDetailsSheet(context, currentFile);
|
||||
fullScreenState?._toggleAnimation(pause: false);
|
||||
},
|
||||
),
|
||||
];
|
||||
@@ -360,6 +516,7 @@ class BottomIcons extends StatelessWidget {
|
||||
color: Colors.white, //same for both themes
|
||||
),
|
||||
onPressed: () async {
|
||||
fullScreenState?._toggleAnimation(pause: true);
|
||||
await showSingleFileDeleteSheet(
|
||||
context,
|
||||
inheritedData
|
||||
@@ -372,6 +529,7 @@ class BottomIcons extends StatelessWidget {
|
||||
},
|
||||
},
|
||||
);
|
||||
fullScreenState?._toggleAnimation(pause: false);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
@@ -386,21 +544,20 @@ class BottomIcons extends StatelessWidget {
|
||||
Icons.adaptive.share,
|
||||
color: Colors.white, //same for both themes
|
||||
),
|
||||
onPressed: () {
|
||||
share(context, [currentFile]);
|
||||
onPressed: () async {
|
||||
fullScreenState?._toggleAnimation(pause: true);
|
||||
await share(context, [currentFile]);
|
||||
fullScreenState?._toggleAnimation(pause: false);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -427,26 +584,174 @@ class MemoryCounter extends StatelessWidget {
|
||||
}
|
||||
|
||||
class BottomGradient extends StatelessWidget {
|
||||
const BottomGradient({super.key});
|
||||
final ValueNotifier<bool> showTitle;
|
||||
const BottomGradient({super.key, required this.showTitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
height: 124,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5), //same for both themes
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.8],
|
||||
),
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: showTitle,
|
||||
builder: (context, value, _) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 875),
|
||||
curve: Curves.easeOutQuart,
|
||||
height: value ? 240 : 120,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Color.fromARGB(97, 0, 0, 0),
|
||||
Color.fromARGB(42, 0, 0, 0),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: [0, 0.5, 1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MemoryBlur extends StatelessWidget {
|
||||
const _MemoryBlur();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final inheritedData = FullScreenMemoryData.of(context)!;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: inheritedData.indexNotifier,
|
||||
builder: (context, value, _) {
|
||||
final currentFile = inheritedData.memories[value].file;
|
||||
if (currentFile.fileType == FileType.video) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 750),
|
||||
switchInCurve: Curves.easeOutExpo,
|
||||
switchOutCurve: Curves.easeInExpo,
|
||||
child: ImageFiltered(
|
||||
key: ValueKey(inheritedData.indexNotifier.value),
|
||||
imageFilter: ImageFilter.blur(
|
||||
sigmaX: 100,
|
||||
sigmaY: 100,
|
||||
),
|
||||
child: ThumbnailWidget(
|
||||
currentFile,
|
||||
shouldShowSyncStatus: false,
|
||||
shouldShowFavoriteIcon: false,
|
||||
shouldShowVideoOverlayIcon: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MemoriesZoomWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool isVideo;
|
||||
final void Function(AnimationController)? scaleController;
|
||||
final bool zoomIn;
|
||||
|
||||
const MemoriesZoomWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.isVideo,
|
||||
required this.zoomIn,
|
||||
this.scaleController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MemoriesZoomWidget> createState() => _MemoriesZoomWidgetState();
|
||||
}
|
||||
|
||||
class _MemoriesZoomWidgetState extends State<MemoriesZoomWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<Offset> _panAnimation;
|
||||
Random random = Random();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initAnimation();
|
||||
}
|
||||
|
||||
void _initAnimation() {
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
);
|
||||
|
||||
final startScale = widget.zoomIn ? 1.05 : 1.15;
|
||||
final endScale = widget.zoomIn ? 1.15 : 1.05;
|
||||
|
||||
final startX = (random.nextDouble() - 0.5) * 0.1;
|
||||
final startY = (random.nextDouble() - 0.5) * 0.1;
|
||||
final endX = (random.nextDouble() - 0.5) * 0.1;
|
||||
final endY = (random.nextDouble() - 0.5) * 0.1;
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: startScale,
|
||||
end: endScale,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
_panAnimation = Tween<Offset>(
|
||||
begin: Offset(startX, startY),
|
||||
end: Offset(endX, endY),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.scaleController != null) {
|
||||
widget.scaleController!(_controller);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.isVideo
|
||||
? widget.child
|
||||
: ClipRect(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
child: widget.child,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
_panAnimation.value.dx * 100,
|
||||
_panAnimation.value.dy * 100,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,11 @@ class MemoriesWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MemoriesWidgetState extends State<MemoriesWidget> {
|
||||
late ScrollController _controller;
|
||||
late StreamSubscription<MemoriesSettingChanged> _memoriesSettingSubscription;
|
||||
late StreamSubscription<MemoriesChangedEvent> _memoriesChangedSubscription;
|
||||
late StreamSubscription<MemorySeenEvent> _memorySeenSubscription;
|
||||
late double _maxHeight;
|
||||
late double _maxWidth;
|
||||
late double _memoryheight;
|
||||
late double _memoryWidth;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -48,7 +47,6 @@ class _MemoriesWidgetState extends State<MemoriesWidget> {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_controller = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -57,8 +55,8 @@ class _MemoriesWidgetState extends State<MemoriesWidget> {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
//factor will be 2 for most phones in portrait mode
|
||||
final factor = (screenWidth / 220).ceil();
|
||||
_maxWidth = screenWidth / (factor * 2);
|
||||
_maxHeight = _maxWidth / MemoryCoverWidget.aspectRatio;
|
||||
_memoryWidth = screenWidth / (factor * 2);
|
||||
_memoryheight = _memoryWidth / MemoryCoverWidget.aspectRatio;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -66,7 +64,6 @@ class _MemoriesWidgetState extends State<MemoriesWidget> {
|
||||
_memoriesSettingSubscription.cancel();
|
||||
_memoriesChangedSubscription.cancel();
|
||||
_memorySeenSubscription.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -84,7 +81,7 @@ class _MemoriesWidgetState extends State<MemoriesWidget> {
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError || !snapshot.hasData) {
|
||||
return SizedBox(
|
||||
height: _maxHeight + 12 + 10,
|
||||
height: _memoryheight + 12 + 10,
|
||||
child: const EnteLoadingWidget(),
|
||||
);
|
||||
} else {
|
||||
@@ -121,27 +118,22 @@ class _MemoriesWidgetState extends State<MemoriesWidget> {
|
||||
collatedMemories.addAll(seenMemories.map((e) => (e.memories, e.title)));
|
||||
|
||||
return SizedBox(
|
||||
height: _maxHeight + MemoryCoverWidget.outerStrokeWidth * 2,
|
||||
height: _memoryheight + MemoryCoverWidget.outerStrokeWidth * 2,
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _controller,
|
||||
itemCount: collatedMemories.length,
|
||||
itemBuilder: (context, itemIndex) {
|
||||
final maxScaleOffsetX =
|
||||
_maxWidth + MemoryCoverWidget.horizontalPadding * 2;
|
||||
final offsetOfItem =
|
||||
(_maxWidth + MemoryCoverWidget.horizontalPadding * 2) * itemIndex;
|
||||
return MemoryCoverWidget(
|
||||
memories: collatedMemories[itemIndex].$1,
|
||||
controller: _controller,
|
||||
offsetOfItem: offsetOfItem,
|
||||
maxHeight: _maxHeight,
|
||||
maxWidth: _maxWidth,
|
||||
maxScaleOffsetX: maxScaleOffsetX,
|
||||
allMemories: collatedMemories.map((e) => e.$1).toList(),
|
||||
height: _memoryheight,
|
||||
width: _memoryWidth,
|
||||
title: collatedMemories[itemIndex].$2,
|
||||
allTitle: collatedMemories.map((e) => e.$2).toList(),
|
||||
currentMemoryIndex: itemIndex,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/scheduler.dart";
|
||||
import "package:photos/models/memories/memory.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/effects.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/home/memories/full_screen_memory.dart";
|
||||
import "package:photos/ui/home/memories/all_memories_page.dart";
|
||||
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
// TODO: Use a single instance variable for `allMemories` and `allTitles`
|
||||
class MemoryCoverWidget extends StatefulWidget {
|
||||
final List<Memory> memories;
|
||||
final ScrollController controller;
|
||||
final double offsetOfItem;
|
||||
final double maxHeight;
|
||||
final double maxWidth;
|
||||
final List<List<Memory>> allMemories;
|
||||
final double height;
|
||||
final double width;
|
||||
static const outerStrokeWidth = 1.0;
|
||||
static const aspectRatio = 0.68;
|
||||
static const horizontalPadding = 2.5;
|
||||
final double maxScaleOffsetX;
|
||||
final String title;
|
||||
final List<String> allTitle;
|
||||
final int currentMemoryIndex;
|
||||
|
||||
const MemoryCoverWidget({
|
||||
required this.memories,
|
||||
required this.controller,
|
||||
required this.offsetOfItem,
|
||||
required this.maxHeight,
|
||||
required this.maxWidth,
|
||||
required this.maxScaleOffsetX,
|
||||
required this.allMemories,
|
||||
required this.height,
|
||||
required this.width,
|
||||
required this.title,
|
||||
required this.allTitle,
|
||||
required this.currentMemoryIndex,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -36,6 +37,12 @@ class MemoryCoverWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_preloadFirstUnseenMemory();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//memories will be empty if all memories are deleted and setState is called
|
||||
@@ -44,183 +51,180 @@ class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final widthOfScreen = MediaQuery.sizeOf(context).width;
|
||||
final index = _getNextMemoryIndex();
|
||||
final title = widget.title;
|
||||
|
||||
final memory = widget.memories[index];
|
||||
final isSeen = memory.isSeen();
|
||||
final brightness =
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
final brightness = Theme.of(context).brightness;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.controller,
|
||||
builder: (context, child) {
|
||||
final diff = (widget.controller.offset - widget.offsetOfItem) +
|
||||
widget.maxScaleOffsetX;
|
||||
final scale = 1 - (diff / widthOfScreen).abs() / 3.7;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: MemoryCoverWidget.horizontalPadding,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await routeToPage(
|
||||
context,
|
||||
FullScreenMemoryDataUpdater(
|
||||
initialIndex: index,
|
||||
memories: widget.memories,
|
||||
child: FullScreenMemory(title, index),
|
||||
),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
setState(() {});
|
||||
}, //Adding this row is a workaround for making height of memory cover
|
||||
//render as [MemoryCoverWidgetNew.height] * scale. Without this, height of rendered memory
|
||||
//cover will be [MemoryCoverWidgetNew.height].
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: widget.maxHeight * scale,
|
||||
width: widget.maxWidth * scale,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: brightness == Brightness.dark
|
||||
? [
|
||||
const BoxShadow(
|
||||
color: strokeFainterDark,
|
||||
spreadRadius: MemoryCoverWidget.outerStrokeWidth,
|
||||
blurRadius: 0,
|
||||
),
|
||||
]
|
||||
: [...shadowFloatFaintestLight],
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: isSeen
|
||||
? ColorFiltered(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
Color(0xFFBFBFBF),
|
||||
BlendMode.hue,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
child!,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 1],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8 * scale,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: SizedBox(
|
||||
width: widget.maxWidth,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Hero(
|
||||
tag: title,
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: getEnteTextTheme(context)
|
||||
.miniBold
|
||||
.copyWith(
|
||||
color: isSeen
|
||||
? textFaintDark
|
||||
: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
child!,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 1],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8 * scale,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: SizedBox(
|
||||
width: widget.maxWidth,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Hero(
|
||||
tag: title,
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: getEnteTextTheme(context)
|
||||
.miniBold
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: MemoryCoverWidget.horizontalPadding,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await routeToPage(
|
||||
context,
|
||||
forceCustomPageRoute: true,
|
||||
AllMemoriesPage(
|
||||
initialPageIndex: widget.currentMemoryIndex,
|
||||
allMemories: widget.allMemories,
|
||||
allTitles: widget.allTitle,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: brightness == Brightness.dark
|
||||
? [
|
||||
const BoxShadow(
|
||||
color: strokeFainterDark,
|
||||
spreadRadius: MemoryCoverWidget.outerStrokeWidth,
|
||||
blurRadius: 0,
|
||||
),
|
||||
]
|
||||
: [...shadowFloatFaintestLight],
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: isSeen
|
||||
? ColorFiltered(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
Color(0xFFBFBFBF),
|
||||
BlendMode.hue,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Hero(
|
||||
tag: "memories" + memory.file.tag,
|
||||
child: ThumbnailWidget(
|
||||
memory.file,
|
||||
shouldShowArchiveStatus: false,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("memories" + memory.file.tag),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 1],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Hero(
|
||||
tag: title,
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: getEnteTextTheme(context)
|
||||
.miniBold
|
||||
.copyWith(
|
||||
color: isSeen
|
||||
? textFaintDark
|
||||
: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Hero(
|
||||
tag: "memories" + memory.file.tag,
|
||||
child: ThumbnailWidget(
|
||||
memory.file,
|
||||
shouldShowArchiveStatus: false,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("memories" + memory.file.tag),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 1],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Hero(
|
||||
tag: title,
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: getEnteTextTheme(context)
|
||||
.miniBold
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: "memories" + memory.file.tag,
|
||||
child: ThumbnailWidget(
|
||||
memory.file,
|
||||
shouldShowArchiveStatus: false,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("memories" + memory.file.tag),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _preloadFirstUnseenMemory() {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
if (widget.memories.isEmpty) return;
|
||||
|
||||
final index = _getNextMemoryIndex();
|
||||
preloadThumbnail(widget.memories[index].file);
|
||||
preloadFile(widget.memories[index].file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Returns either the first unseen memory or the memory that succeeds the
|
||||
// last seen memory
|
||||
int _getNextMemoryIndex() {
|
||||
|
||||
107
mobile/lib/ui/home/memories/memory_progress_indicator.dart
Normal file
107
mobile/lib/ui/home/memories/memory_progress_indicator.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class MemoryProgressIndicator extends StatefulWidget {
|
||||
final int totalSteps;
|
||||
final int currentIndex;
|
||||
final Duration duration;
|
||||
final Color selectedColor;
|
||||
final Color unselectedColor;
|
||||
final double height;
|
||||
final double gap;
|
||||
final void Function(AnimationController)? animationController;
|
||||
final VoidCallback? onComplete;
|
||||
|
||||
const MemoryProgressIndicator({
|
||||
super.key,
|
||||
required this.totalSteps,
|
||||
required this.currentIndex,
|
||||
this.duration = const Duration(seconds: 5),
|
||||
this.selectedColor = Colors.white,
|
||||
this.unselectedColor = Colors.white54,
|
||||
this.height = 2.0,
|
||||
this.gap = 4.0,
|
||||
this.animationController,
|
||||
this.onComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MemoryProgressIndicator> createState() =>
|
||||
_MemoryProgressIndicatorState();
|
||||
}
|
||||
|
||||
class _MemoryProgressIndicatorState extends State<MemoryProgressIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
);
|
||||
|
||||
_animation =
|
||||
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
|
||||
|
||||
if (widget.animationController != null) {
|
||||
widget.animationController!(_animationController);
|
||||
}
|
||||
|
||||
_animationController.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed && widget.onComplete != null) {
|
||||
widget.onComplete!();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: List.generate(widget.totalSteps, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: widget.gap),
|
||||
child: index < widget.currentIndex
|
||||
? Container(
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.selectedColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
)
|
||||
: index == widget.currentIndex
|
||||
? AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return LinearProgressIndicator(
|
||||
value: _animation.value,
|
||||
backgroundColor: widget.unselectedColor,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
widget.selectedColor,
|
||||
),
|
||||
minHeight: widget.height,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.unselectedColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ class FileWidget extends StatelessWidget {
|
||||
final Function(bool)? playbackCallback;
|
||||
final BoxDecoration? backgroundDecoration;
|
||||
final bool? autoPlay;
|
||||
final bool? isFromMemories;
|
||||
final Function({required int memoryDuration})? onFinalFileLoad;
|
||||
|
||||
const FileWidget(
|
||||
this.file, {
|
||||
@@ -20,6 +22,8 @@ class FileWidget extends StatelessWidget {
|
||||
this.playbackCallback,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
this.isFromMemories = false,
|
||||
this.onFinalFileLoad,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -37,7 +41,9 @@ class FileWidget extends StatelessWidget {
|
||||
shouldDisableScroll: shouldDisableScroll,
|
||||
tagPrefix: tagPrefix,
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
isFromMemories: isFromMemories ?? false,
|
||||
key: key ?? ValueKey(fileKey),
|
||||
onFinalFileLoad: onFinalFileLoad,
|
||||
);
|
||||
} else if (file.fileType == FileType.video) {
|
||||
// use old video widget on iOS simulator as the new one crashes while
|
||||
@@ -54,6 +60,8 @@ class FileWidget extends StatelessWidget {
|
||||
file,
|
||||
tagPrefix: tagPrefix,
|
||||
playbackCallback: playbackCallback,
|
||||
onFinalFileLoad: onFinalFileLoad,
|
||||
isFromMemories: isFromMemories ?? false,
|
||||
key: key ?? ValueKey(fileKey),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -23,10 +23,15 @@ class VideoWidget extends StatefulWidget {
|
||||
final EnteFile file;
|
||||
final String? tagPrefix;
|
||||
final Function(bool)? playbackCallback;
|
||||
final Function({required int memoryDuration})? onFinalFileLoad;
|
||||
final bool isFromMemories;
|
||||
|
||||
const VideoWidget(
|
||||
this.file, {
|
||||
this.tagPrefix,
|
||||
this.playbackCallback,
|
||||
this.onFinalFileLoad,
|
||||
this.isFromMemories = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -149,6 +154,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
playbackCallback: widget.playbackCallback,
|
||||
playlistData: playlistData,
|
||||
selectedPreview: playPreview,
|
||||
isFromMemories: widget.isFromMemories,
|
||||
onStreamChange: () {
|
||||
setState(() {
|
||||
selectPreviewForPlay = !selectPreviewForPlay;
|
||||
@@ -162,6 +168,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
);
|
||||
});
|
||||
},
|
||||
onFinalFileLoad: widget.onFinalFileLoad,
|
||||
);
|
||||
}
|
||||
return VideoWidgetMediaKit(
|
||||
@@ -171,6 +178,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
playbackCallback: widget.playbackCallback,
|
||||
preview: playlistData?.preview,
|
||||
selectedPreview: playPreview,
|
||||
isFromMemories: widget.isFromMemories,
|
||||
onStreamChange: () {
|
||||
setState(() {
|
||||
selectPreviewForPlay = !selectPreviewForPlay;
|
||||
@@ -184,6 +192,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
);
|
||||
});
|
||||
},
|
||||
onFinalFileLoad: widget.onFinalFileLoad,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class VideoWidgetMediaKit extends StatefulWidget {
|
||||
final void Function() onStreamChange;
|
||||
final File? preview;
|
||||
final bool selectedPreview;
|
||||
final Function({required int memoryDuration})? onFinalFileLoad;
|
||||
|
||||
const VideoWidgetMediaKit(
|
||||
this.file, {
|
||||
@@ -45,6 +46,7 @@ class VideoWidgetMediaKit extends StatefulWidget {
|
||||
required this.onStreamChange,
|
||||
this.preview,
|
||||
required this.selectedPreview,
|
||||
this.onFinalFileLoad,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -313,6 +315,13 @@ class _VideoWidgetMediaKitState extends State<VideoWidgetMediaKit>
|
||||
}
|
||||
player.open(Media(url), play: _isAppInFG);
|
||||
});
|
||||
int duration = controller!.player.state.duration.inSeconds;
|
||||
if (duration == 0) {
|
||||
duration = 10;
|
||||
}
|
||||
widget.onFinalFileLoad?.call(
|
||||
memoryDuration: duration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,56 +99,79 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
showControlsNotifier.value = !showControlsNotifier.value;
|
||||
if (widget.playbackCallback != null) {
|
||||
widget.playbackCallback!(
|
||||
!showControlsNotifier.value,
|
||||
);
|
||||
onTap: widget.isFromMemories
|
||||
? null
|
||||
: () {
|
||||
showControlsNotifier.value =
|
||||
!showControlsNotifier.value;
|
||||
if (widget.playbackCallback != null) {
|
||||
widget.playbackCallback!(
|
||||
!showControlsNotifier.value,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.isFromMemories) {
|
||||
widget.playbackCallback?.call(false);
|
||||
if (widget.controller.player.state.playing) {
|
||||
widget.controller.player.pause();
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPressUp: () {
|
||||
if (widget.isFromMemories) {
|
||||
widget.playbackCallback?.call(true);
|
||||
if (!widget.controller.player.state.playing) {
|
||||
widget.controller.player.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: PlayPauseButtonMediaKit(widget.controller),
|
||||
),
|
||||
Positioned(
|
||||
bottom: verticalMargin,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: widget.isFromMemories ? 32 : 0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
VideoStreamChangeWidget(
|
||||
showControls: value,
|
||||
file: widget.file,
|
||||
isPreviewPlayer: widget.isPreviewPlayer,
|
||||
onStreamChange: widget.onStreamChange,
|
||||
widget.isFromMemories
|
||||
? const SizedBox.shrink()
|
||||
: IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: PlayPauseButtonMediaKit(widget.controller),
|
||||
),
|
||||
widget.isFromMemories
|
||||
? const SizedBox.shrink()
|
||||
: Positioned(
|
||||
bottom: verticalMargin,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: widget.isFromMemories ? 32 : 0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
VideoStreamChangeWidget(
|
||||
showControls: value,
|
||||
file: widget.file,
|
||||
isPreviewPlayer: widget.isPreviewPlayer,
|
||||
onStreamChange: widget.onStreamChange,
|
||||
),
|
||||
SeekBarAndDuration(
|
||||
controller: widget.controller,
|
||||
isSeekingNotifier: _isSeekingNotifier,
|
||||
file: widget.file,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SeekBarAndDuration(
|
||||
controller: widget.controller,
|
||||
isSeekingNotifier: _isSeekingNotifier,
|
||||
file: widget.file,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ class VideoWidgetNative extends StatefulWidget {
|
||||
final void Function()? onStreamChange;
|
||||
final PlaylistData? playlistData;
|
||||
final bool selectedPreview;
|
||||
final Function({required int memoryDuration})? onFinalFileLoad;
|
||||
|
||||
const VideoWidgetNative(
|
||||
this.file, {
|
||||
@@ -55,6 +56,7 @@ class VideoWidgetNative extends StatefulWidget {
|
||||
required this.onStreamChange,
|
||||
super.key,
|
||||
this.playlistData,
|
||||
this.onFinalFileLoad,
|
||||
required this.selectedPreview,
|
||||
});
|
||||
|
||||
@@ -138,11 +140,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
widget.file.uploadedFileID!,
|
||||
)
|
||||
.listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_progressNotifier.value = event.progress;
|
||||
});
|
||||
}
|
||||
_progressNotifier.value = event.progress;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -303,12 +301,27 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
_showControls.value = !_showControls.value;
|
||||
if (widget.playbackCallback != null) {
|
||||
widget.playbackCallback!(!_showControls.value);
|
||||
onTap: widget.isFromMemories
|
||||
? null
|
||||
: () {
|
||||
_showControls.value = !_showControls.value;
|
||||
if (widget.playbackCallback != null) {
|
||||
widget
|
||||
.playbackCallback!(!_showControls.value);
|
||||
}
|
||||
_elTooltipController.hide();
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.isFromMemories) {
|
||||
widget.playbackCallback?.call(false);
|
||||
_controller?.pause();
|
||||
}
|
||||
},
|
||||
onLongPressUp: () {
|
||||
if (widget.isFromMemories) {
|
||||
widget.playbackCallback?.call(true);
|
||||
_controller?.play();
|
||||
}
|
||||
_elTooltipController.hide();
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
@@ -333,86 +346,101 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, bool value, _) {
|
||||
return value
|
||||
? ValueListenableBuilder(
|
||||
builder: (context, bool value, _) {
|
||||
return AnimatedOpacity(
|
||||
duration:
|
||||
const Duration(milliseconds: 200),
|
||||
opacity: value ? 1 : 0,
|
||||
curve: Curves.easeInOutQuad,
|
||||
child: IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: PlayPauseButton(_controller),
|
||||
),
|
||||
);
|
||||
},
|
||||
valueListenable: _showControls,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
valueListenable: _isPlaybackReady,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: verticalMargin,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: widget.isFromMemories ? 32 : 0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_VideoDescriptionAndSwitchToMediaKitButton(
|
||||
file: widget.file,
|
||||
showControls: _showControls,
|
||||
elTooltipController: _elTooltipController,
|
||||
controller: _controller,
|
||||
selectedPreview: widget.selectedPreview,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showControls,
|
||||
builder: (context, value, _) {
|
||||
return VideoStreamChangeWidget(
|
||||
showControls: value,
|
||||
file: widget.file,
|
||||
isPreviewPlayer: widget.selectedPreview,
|
||||
onStreamChange: widget.onStreamChange,
|
||||
);
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _isPlaybackReady,
|
||||
widget.isFromMemories
|
||||
? const SizedBox.shrink()
|
||||
: Positioned.fill(
|
||||
child: Center(
|
||||
child: ValueListenableBuilder(
|
||||
builder:
|
||||
(BuildContext context, bool value, _) {
|
||||
return value
|
||||
? _SeekBarAndDuration(
|
||||
controller: _controller,
|
||||
duration: duration,
|
||||
showControls: _showControls,
|
||||
isSeeking: _isSeeking,
|
||||
position: position,
|
||||
file: widget.file,
|
||||
? ValueListenableBuilder(
|
||||
builder: (context, bool value, _) {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
opacity: value ? 1 : 0,
|
||||
curve: Curves.easeInOutQuad,
|
||||
child: IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: PlayPauseButton(
|
||||
_controller,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
valueListenable: _showControls,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
valueListenable: _isPlaybackReady,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
widget.isFromMemories
|
||||
? const SizedBox.shrink()
|
||||
: Positioned(
|
||||
bottom: verticalMargin,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: widget.isFromMemories ? 32 : 0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
_VideoDescriptionAndSwitchToMediaKitButton(
|
||||
file: widget.file,
|
||||
showControls: _showControls,
|
||||
elTooltipController:
|
||||
_elTooltipController,
|
||||
controller: _controller,
|
||||
selectedPreview: widget.selectedPreview,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showControls,
|
||||
builder: (context, value, _) {
|
||||
return VideoStreamChangeWidget(
|
||||
showControls: value,
|
||||
file: widget.file,
|
||||
isPreviewPlayer:
|
||||
widget.selectedPreview,
|
||||
onStreamChange:
|
||||
widget.onStreamChange,
|
||||
);
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _isPlaybackReady,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
bool value,
|
||||
_,
|
||||
) {
|
||||
return value
|
||||
? _SeekBarAndDuration(
|
||||
controller: _controller,
|
||||
duration: duration,
|
||||
showControls: _showControls,
|
||||
isSeeking: _isSeeking,
|
||||
position: position,
|
||||
file: widget.file,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -465,6 +493,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
}
|
||||
|
||||
void _seekListener() {
|
||||
if (widget.isFromMemories) return;
|
||||
if (!_isSeeking.value &&
|
||||
_controller?.playbackStatus == PlaybackStatus.playing) {
|
||||
_debouncer.run(() async {
|
||||
@@ -483,6 +512,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
}
|
||||
|
||||
void _onPlaybackStatusChanged() {
|
||||
if (widget.isFromMemories) return;
|
||||
final duration = widget.file.duration != null
|
||||
? widget.file.duration! * 1000
|
||||
: _controller?.videoInfo?.durationInMilliseconds;
|
||||
@@ -527,6 +557,8 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
Future<void> _onPlaybackReady() async {
|
||||
if (_isPlaybackReady.value) return;
|
||||
await _controller!.play();
|
||||
final durationInSeconds = durationToSeconds(duration) ?? 10;
|
||||
widget.onFinalFileLoad?.call(memoryDuration: durationInSeconds);
|
||||
unawaited(_controller!.setVolume(1));
|
||||
_isPlaybackReady.value = true;
|
||||
}
|
||||
@@ -740,11 +772,7 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
bool value,
|
||||
_,
|
||||
) {
|
||||
builder: (BuildContext context, bool value, _) {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(
|
||||
milliseconds: 200,
|
||||
|
||||
@@ -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";
|
||||
@@ -31,6 +32,7 @@ class ZoomableImage extends StatefulWidget {
|
||||
final Decoration? backgroundDecoration;
|
||||
final bool shouldCover;
|
||||
final bool isGuestView;
|
||||
final Function({required int memoryDuration})? onFinalFileLoad;
|
||||
|
||||
const ZoomableImage(
|
||||
this.photo, {
|
||||
@@ -40,6 +42,7 @@ class ZoomableImage extends StatefulWidget {
|
||||
this.backgroundDecoration,
|
||||
this.shouldCover = false,
|
||||
this.isGuestView = false,
|
||||
this.onFinalFileLoad,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -62,6 +65,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
final _scaleStateController = PhotoViewScaleStateController();
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
late final StreamSubscription<ResetZoomOfPhotoView> _resetZoomSubscription;
|
||||
|
||||
// This is to prevent the app from crashing when loading 200MP images
|
||||
// https://github.com/flutter/flutter/issues/110331
|
||||
@@ -90,6 +94,16 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_resetZoomSubscription =
|
||||
Bus.instance.on<ResetZoomOfPhotoView>().listen((event) {
|
||||
if (event.isSamePhoto(
|
||||
uploadedFileID: widget.photo.uploadedFileID,
|
||||
localID: widget.photo.localID,
|
||||
)) {
|
||||
_scaleStateController.scaleState = PhotoViewScaleState.initial;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -97,6 +111,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
_photoViewController.dispose();
|
||||
_scaleStateController.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
_resetZoomSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -426,6 +441,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
_loadedFinalImage = true;
|
||||
_logger.info("Final image loaded");
|
||||
});
|
||||
widget.onFinalFileLoad?.call(memoryDuration: 5);
|
||||
}
|
||||
|
||||
Future<void> _updatePhotoViewController({
|
||||
|
||||
@@ -23,13 +23,17 @@ class ZoomableLiveImageNew extends StatefulWidget {
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
|
||||
final bool isFromMemories;
|
||||
final Function({required int memoryDuration})? onFinalFileLoad;
|
||||
|
||||
const ZoomableLiveImageNew(
|
||||
this.enteFile, {
|
||||
super.key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
this.isFromMemories = false,
|
||||
this.onFinalFileLoad,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -94,13 +98,17 @@ class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
|
||||
shouldDisableScroll: widget.shouldDisableScroll,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
isGuestView: isGuestView,
|
||||
onFinalFileLoad: widget.onFinalFileLoad,
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onLongPressStart: (_) => {_onLongPressEvent(true)},
|
||||
onLongPressEnd: (_) => {_onLongPressEvent(false)},
|
||||
child: content,
|
||||
);
|
||||
if (!widget.isFromMemories) {
|
||||
return GestureDetector(
|
||||
onLongPressStart: (_) => _onLongPressEvent(true),
|
||||
onLongPressEnd: (_) => _onLongPressEvent(false),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user