Map improvements (#1245)

This commit is contained in:
Neeraj Gupta 2023-06-29 09:12:35 +05:30 committed by GitHub
commit 3ffb94022b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 430 additions and 273 deletions

View file

@ -71,4 +71,11 @@ class SelectedFiles extends ChangeNotifier {
files.clear();
notifyListeners();
}
/// Retains only the files that are present in the [images] set. Takes the
/// intersection of the two sets.
void filesToRetain(Set<File> images) {
files.retainAll(images);
notifyListeners();
}
}

View file

@ -17,6 +17,7 @@ class BottomActionBarWidget extends StatelessWidget {
final VoidCallback? onCancel;
final bool hasSmallerBottomPadding;
final GalleryType type;
final Color? backgroundColor;
BottomActionBarWidget({
required this.expandedMenu,
@ -26,6 +27,7 @@ class BottomActionBarWidget extends StatelessWidget {
this.text,
this.iconButtons,
this.onCancel,
this.backgroundColor,
super.key,
});
@ -42,7 +44,7 @@ class BottomActionBarWidget extends StatelessWidget {
: 0;
return Container(
decoration: BoxDecoration(
color: colorScheme.backgroundElevated,
color: backgroundColor ?? colorScheme.backgroundElevated2,
boxShadow: shadowFloatFaintLight,
),
padding: EdgeInsets.only(

View file

@ -83,6 +83,7 @@ class HomeGalleryWidget extends StatelessWidget {
scrollBottomSafeArea: bottomSafeArea + 180,
);
return Stack(
alignment: Alignment.bottomCenter,
children: [
gallery,
FileSelectionOverlayBar(GalleryType.homepage, selectedFiles)

View file

@ -62,6 +62,8 @@ class HugeListView<T> extends StatefulWidget {
final bool disableScroll;
final bool isScrollablePositionedList;
const HugeListView({
Key? key,
this.controller,
@ -80,6 +82,7 @@ class HugeListView<T> extends StatefulWidget {
this.isDraggableScrollbarEnabled = true,
this.thumbPadding,
this.disableScroll = false,
this.isScrollablePositionedList = true,
}) : super(key: key);
@override
@ -96,7 +99,9 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
void initState() {
super.initState();
listener.itemPositions.addListener(_sendScroll);
widget.isScrollablePositionedList
? listener.itemPositions.addListener(_sendScroll)
: null;
}
@override
@ -131,52 +136,56 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
return widget.emptyResultBuilder!(context);
}
return LayoutBuilder(
builder: (context, constraints) {
return DraggableScrollbar(
key: scrollKey,
totalCount: widget.totalCount,
initialScrollIndex: widget.startIndex,
onChange: (position) {
final int currentIndex = _currentFirst();
final int floorIndex = (position * widget.totalCount).floor();
final int cielIndex = (position * widget.totalCount).ceil();
int nextIndexToJump;
if (floorIndex != currentIndex && floorIndex > currentIndex) {
nextIndexToJump = floorIndex;
} else if (cielIndex != currentIndex && cielIndex < currentIndex) {
nextIndexToJump = floorIndex;
} else {
return;
}
if (lastIndexJump != nextIndexToJump) {
lastIndexJump = nextIndexToJump;
widget.controller?.jumpTo(index: nextIndexToJump);
}
},
labelTextBuilder: widget.labelTextBuilder,
backgroundColor: widget.thumbBackgroundColor,
drawColor: widget.thumbDrawColor,
heightScrollThumb: widget.thumbHeight,
bottomSafeArea: widget.bottomSafeArea,
currentFirstIndex: _currentFirst(),
isEnabled: widget.isDraggableScrollbarEnabled,
padding: widget.thumbPadding,
child: ScrollablePositionedList.builder(
physics: widget.disableScroll
? const NeverScrollableScrollPhysics()
: null,
itemScrollController: widget.controller,
itemPositionsListener: listener,
return widget.isScrollablePositionedList
? DraggableScrollbar(
key: scrollKey,
totalCount: widget.totalCount,
initialScrollIndex: widget.startIndex,
onChange: (position) {
final int currentIndex = _currentFirst();
final int floorIndex = (position * widget.totalCount).floor();
final int cielIndex = (position * widget.totalCount).ceil();
int nextIndexToJump;
if (floorIndex != currentIndex && floorIndex > currentIndex) {
nextIndexToJump = floorIndex;
} else if (cielIndex != currentIndex &&
cielIndex < currentIndex) {
nextIndexToJump = floorIndex;
} else {
return;
}
if (lastIndexJump != nextIndexToJump) {
lastIndexJump = nextIndexToJump;
widget.controller?.jumpTo(index: nextIndexToJump);
}
},
labelTextBuilder: widget.labelTextBuilder,
backgroundColor: widget.thumbBackgroundColor,
drawColor: widget.thumbDrawColor,
heightScrollThumb: widget.thumbHeight,
bottomSafeArea: widget.bottomSafeArea,
currentFirstIndex: _currentFirst(),
isEnabled: widget.isDraggableScrollbarEnabled,
padding: widget.thumbPadding,
child: ScrollablePositionedList.builder(
physics: widget.disableScroll
? const NeverScrollableScrollPhysics()
: null,
itemScrollController: widget.controller,
itemPositionsListener: listener,
initialScrollIndex: widget.startIndex,
itemCount: max(widget.totalCount, 0),
itemBuilder: (context, index) {
return widget.itemBuilder(context, index);
},
),
)
: ListView.builder(
itemCount: max(widget.totalCount, 0),
itemBuilder: (context, index) {
return widget.itemBuilder(context, index);
},
),
);
},
);
);
}
/// Jump to the [position] in the list. [position] is between 0.0 (first item) and 1.0 (last item), practically currentIndex / totalCount.

View file

@ -1,65 +0,0 @@
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_load_result.dart";
import "package:photos/ui/viewer/file/detail_page.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/utils/navigation_util.dart";
class ImageTile extends StatelessWidget {
final File image;
final int index;
final List<File> visibleImages;
const ImageTile({
super.key,
required this.image,
required this.index,
required this.visibleImages,
});
void onTap(BuildContext context, File image, int index) {
if (kDebugMode) {
debugPrint('size of visibleImages: ${visibleImages.length}');
}
final page = DetailPage(
DetailPageConfiguration(
List.unmodifiable(visibleImages),
(
creationStartTime,
creationEndTime, {
limit,
asc,
}) async {
final result = FileLoadResult(visibleImages, false);
return result;
},
index,
'Map',
),
);
routeToPage(
context,
page,
forceCustomPageRoute: true,
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onTap(context, image, index),
child: Padding(
padding: const EdgeInsets.fromLTRB(2, 0, 2, 4),
child: SizedBox(
width: 112,
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: ThumbnailWidget(image),
),
),
),
);
}
}

View file

@ -0,0 +1,223 @@
import "dart:async";
import "package:defer_pointer/defer_pointer.dart";
import "package:flutter/material.dart";
import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/files_updated_event.dart";
import "package:photos/events/local_photos_updated_event.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_load_result.dart";
import "package:photos/models/gallery_type.dart";
import "package:photos/models/selected_files.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/viewer/actions/file_selection_overlay_bar.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
class MapPullUpGallery extends StatefulWidget {
final StreamController<List<File>> visibleImages;
final double bottomUnsafeArea;
final double bottomSheetDraggableAreaHeight;
static const gridCrossAxisSpacing = 4.0;
static const gridMainAxisSpacing = 4.0;
static const gridPadding = 2.0;
static const gridCrossAxisCount = 4;
const MapPullUpGallery(
this.visibleImages,
this.bottomSheetDraggableAreaHeight,
this.bottomUnsafeArea, {
Key? key,
}) : super(key: key);
@override
State<MapPullUpGallery> createState() => _MapPullUpGalleryState();
}
class _MapPullUpGalleryState extends State<MapPullUpGallery> {
final _selectedFiles = SelectedFiles();
@override
Widget build(BuildContext context) {
final Logger logger = Logger("_MapPullUpGalleryState");
final screenHeight = MediaQuery.of(context).size.height;
final unsafeAreaProportion = widget.bottomUnsafeArea / screenHeight;
final double initialChildSize = 0.25 + unsafeAreaProportion;
Widget? cachedScrollableContent;
return DeferredPointerHandler(
child: Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
DraggableScrollableSheet(
expand: false,
initialChildSize: initialChildSize,
minChildSize: initialChildSize,
maxChildSize: 0.8,
snap: true,
snapSizes: const [0.5],
builder: (context, scrollController) {
//Must use cached widget here to avoid rebuilds when DraggableScrollableSheet
//is snapped to it's initialChildSize
cachedScrollableContent ??=
cacheScrollableContent(scrollController, context, logger);
return cachedScrollableContent!;
},
),
DeferPointer(
child: FileSelectionOverlayBar(
GalleryType.searchResults,
_selectedFiles,
backgroundColor: getEnteColorScheme(context).backgroundElevated2,
),
),
],
),
);
}
Widget cacheScrollableContent(
ScrollController scrollController,
BuildContext context,
logger,
) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
color: colorScheme.backgroundElevated,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
DraggableHeader(
scrollController: scrollController,
bottomSheetDraggableAreaHeight:
widget.bottomSheetDraggableAreaHeight,
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: StreamBuilder<List<File>>(
stream: widget.visibleImages.stream,
builder: (
BuildContext context,
AsyncSnapshot<List<File>> snapshot,
) {
if (!snapshot.hasData) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
child: const EnteLoadingWidget(),
);
}
final images = snapshot.data!;
logger.info("Visible images: ${images.length}");
//To retain only selected files that are in view (visible)
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_selectedFiles.filesToRetain(images.toSet());
});
if (images.isEmpty) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"No photos found here",
style: textTheme.large,
),
const SizedBox(height: 4),
Text(
"Zoom out to see photos",
style: textTheme.smallFaint,
)
],
),
),
),
);
}
return Gallery(
key: ValueKey(images),
asyncLoader: (
creationStartTime,
creationEndTime, {
limit,
asc,
}) async {
FileLoadResult result;
result = FileLoadResult(images, false);
return result;
},
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
},
tagPrefix: "map_gallery",
showSelectAllByDefault: true,
selectedFiles: _selectedFiles,
isScrollablePositionedList: false,
);
},
),
),
)
],
),
);
}
}
class DraggableHeader extends StatelessWidget {
const DraggableHeader({
Key? key,
required this.scrollController,
required this.bottomSheetDraggableAreaHeight,
}) : super(key: key);
static const indicatorHeight = 4.0;
final ScrollController scrollController;
final double bottomSheetDraggableAreaHeight;
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
controller: scrollController,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
color: colorScheme.backgroundElevated2,
),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(
vertical:
bottomSheetDraggableAreaHeight / 2 - indicatorHeight / 2,
),
child: Container(
height: indicatorHeight,
width: 72,
decoration: BoxDecoration(
color: colorScheme.fillBase,
borderRadius: const BorderRadius.all(Radius.circular(2)),
),
),
),
),
),
);
}
}

View file

@ -1,19 +1,18 @@
import "dart:async";
import "dart:isolate";
import "dart:math";
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import "package:latlong2/latlong.dart";
import "package:logging/logging.dart";
import "package:photos/models/file.dart";
import "package:photos/models/location/location.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/map/image_marker.dart";
import 'package:photos/ui/map/image_tile.dart';
import "package:photos/ui/map/map_isolate.dart";
import "package:photos/ui/map/map_pull_up_gallery.dart";
import "package:photos/ui/map/map_view.dart";
import "package:photos/utils/toast_util.dart";
@ -40,14 +39,16 @@ class _MapScreenState extends State<MapScreen> {
StreamController<List<File>>.broadcast();
MapController mapController = MapController();
bool isLoading = true;
double initialZoom = 4.0;
double initialZoom = 4.5;
double maxZoom = 18.0;
double minZoom = 0.0;
double minZoom = 2.8;
int debounceDuration = 500;
LatLng center = LatLng(46.7286, 4.8614);
final Logger _logger = Logger("_MapScreenState");
StreamSubscription? _mapMoveSubscription;
Isolate? isolate;
static const bottomSheetDraggableAreaHeight = 32.0;
List<File>? prevMessage;
@override
void initState() {
@ -72,30 +73,21 @@ class _MapScreenState extends State<MapScreen> {
}
Future<void> processFiles(List<File> files) async {
late double minLat, maxLat, minLon, maxLon;
final List<ImageMarker> tempMarkers = [];
bool hasAnyLocation = false;
File? mostRecentFile;
for (var file in files) {
if (kDebugMode && !file.hasLocation) {
final rand = Random();
file.location = Location(
latitude: 46.7286 + rand.nextDouble() * 0.1,
longitude: 4.8614 + rand.nextDouble() * 0.1,
);
}
if (file.hasLocation && file.location != null) {
if (!hasAnyLocation) {
minLat = file.location!.latitude!;
minLon = file.location!.longitude!;
maxLat = file.location!.latitude!;
maxLon = file.location!.longitude!;
hasAnyLocation = true;
hasAnyLocation = true;
if (mostRecentFile == null) {
mostRecentFile = file;
} else {
minLat = min(minLat, file.location!.latitude!);
minLon = min(minLon, file.location!.longitude!);
maxLat = max(maxLat, file.location!.latitude!);
maxLon = max(maxLon, file.location!.longitude!);
if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
mostRecentFile = file;
}
}
tempMarkers.add(
ImageMarker(
latitude: file.location!.latitude!,
@ -108,22 +100,12 @@ class _MapScreenState extends State<MapScreen> {
if (hasAnyLocation) {
center = LatLng(
minLat + (maxLat - minLat) / 2,
minLon + (maxLon - minLon) / 2,
mostRecentFile!.location!.latitude!,
mostRecentFile.location!.longitude!,
);
final latRange = maxLat - minLat;
final lonRange = maxLon - minLon;
final latZoom = log(360.0 / latRange) / log(2);
final lonZoom = log(180.0 / lonRange) / log(2);
initialZoom = min(latZoom, lonZoom);
if (initialZoom <= minZoom) initialZoom = minZoom + 1;
if (initialZoom >= (maxZoom - 1)) initialZoom = maxZoom - 1;
if (kDebugMode) {
debugPrint("Info for map: center $center, initialZoom $initialZoom");
debugPrint("Info for map: minLat $minLat, maxLat $maxLat");
debugPrint("Info for map: minLon $minLon, maxLon $maxLon");
}
} else {
showShortToast(context, "No images with location");
@ -159,7 +141,11 @@ class _MapScreenState extends State<MapScreen> {
_mapMoveSubscription = receivePort.listen((dynamic message) async {
if (message is List<File>) {
visibleImages.sink.add(message);
if (!message.equals(prevMessage ?? [])) {
visibleImages.sink.add(message);
}
prevMessage = message;
} else {
_mapMoveSubscription?.cancel();
isolate?.kill();
@ -188,98 +174,42 @@ class _MapScreenState extends State<MapScreen> {
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
final bottomUnsafeArea = MediaQuery.of(context).padding.bottom;
return Container(
color: colorScheme.backgroundBase,
child: SafeArea(
top: false,
child: Theme(
data: Theme.of(context).copyWith(
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.transparent,
),
),
child: Scaffold(
body: Stack(
children: [
Column(
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(6),
bottomRight: Radius.circular(6),
),
child: MapView(
key: ValueKey(
'image-marker-count-${imageMarkers.length}',
),
controller: mapController,
imageMarkers: imageMarkers,
updateVisibleImages: calculateVisibleMarkers,
center: center,
initialZoom: initialZoom,
minZoom: minZoom,
maxZoom: maxZoom,
debounceDuration: debounceDuration,
LayoutBuilder(
builder: (context, constrains) {
return SizedBox(
height: constrains.maxHeight * 0.75 +
bottomSheetDraggableAreaHeight -
bottomUnsafeArea,
child: MapView(
key: ValueKey(
'image-marker-count-${imageMarkers.length}',
),
controller: mapController,
imageMarkers: imageMarkers,
updateVisibleImages: calculateVisibleMarkers,
center: center,
initialZoom: initialZoom,
minZoom: minZoom,
maxZoom: maxZoom,
debounceDuration: debounceDuration,
bottomSheetDraggableAreaHeight:
bottomSheetDraggableAreaHeight,
),
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(2),
topRight: Radius.circular(2),
),
child: SizedBox(
height: 116,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: StreamBuilder<List<File>>(
stream: visibleImages.stream,
builder: (
BuildContext context,
AsyncSnapshot<List<File>> snapshot,
) {
if (!snapshot.hasData) {
return const Text("Loading...");
}
final images = snapshot.data!;
_logger.info("Visible images: ${images.length}");
if (images.isEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"No photos found here",
style: textTheme.large,
),
const SizedBox(height: 4),
Text(
"Zoom out to see photos",
style: textTheme.smallFaint,
)
],
);
}
return ListView.builder(
itemCount: images.length,
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 2),
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
final image = images[index];
return ImageTile(
image: image,
visibleImages: images,
index: index,
);
},
);
},
),
),
),
),
],
);
},
),
isLoading
? EnteLoadingWidget(
@ -289,6 +219,11 @@ class _MapScreenState extends State<MapScreen> {
: const SizedBox.shrink(),
],
),
bottomSheet: MapPullUpGallery(
visibleImages,
bottomSheetDraggableAreaHeight,
bottomUnsafeArea,
),
),
),
);

View file

@ -1,5 +1,3 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_map/flutter_map.dart";
import "package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart";
@ -10,6 +8,7 @@ import 'package:photos/ui/map/map_gallery_tile.dart';
import 'package:photos/ui/map/map_gallery_tile_badge.dart';
import "package:photos/ui/map/map_marker.dart";
import "package:photos/ui/map/tile/layers.dart";
import "package:photos/utils/debouncer.dart";
class MapView extends StatefulWidget {
final List<ImageMarker> imageMarkers;
@ -20,6 +19,7 @@ class MapView extends StatefulWidget {
final double maxZoom;
final double initialZoom;
final int debounceDuration;
final double bottomSheetDraggableAreaHeight;
const MapView({
Key? key,
@ -31,6 +31,7 @@ class MapView extends StatefulWidget {
required this.maxZoom,
required this.initialZoom,
required this.debounceDuration,
required this.bottomSheetDraggableAreaHeight,
}) : super(key: key);
@override
@ -38,9 +39,9 @@ class MapView extends StatefulWidget {
}
class _MapViewState extends State<MapView> {
Timer? _debounceTimer;
bool _isDebouncing = false;
late List<Marker> _markers;
final _debouncer =
Debouncer(const Duration(milliseconds: 300), executionInterval: 750);
@override
void initState() {
@ -50,23 +51,15 @@ class _MapViewState extends State<MapView> {
@override
void dispose() {
_debounceTimer?.cancel();
_debounceTimer = null;
super.dispose();
}
void onChange(LatLngBounds bounds) {
if (!_isDebouncing) {
_isDebouncing = true;
_debounceTimer?.cancel();
_debounceTimer = Timer(
Duration(milliseconds: widget.debounceDuration),
() {
widget.updateVisibleImages(bounds);
_isDebouncing = false;
},
);
}
_debouncer.run(
() async {
widget.updateVisibleImages(bounds);
},
);
}
@override
@ -81,13 +74,24 @@ class _MapViewState extends State<MapView> {
maxZoom: widget.maxZoom,
enableMultiFingerGestureRace: true,
zoom: widget.initialZoom,
maxBounds: LatLngBounds(
LatLng(-90, -180),
LatLng(90, 180),
),
onPositionChanged: (position, hasGesture) {
if (position.bounds != null) {
onChange(position.bounds!);
}
},
),
nonRotatedChildren: const [OSMFranceTileAttributes()],
nonRotatedChildren: [
Padding(
padding: EdgeInsets.only(
bottom: widget.bottomSheetDraggableAreaHeight,
),
child: const OSMFranceTileAttributes(),
)
],
children: [
const OSMFranceTileLayer(),
MarkerClusterLayerWidget(
@ -101,9 +105,7 @@ class _MapViewState extends State<MapView> {
),
markers: _markers,
onClusterTap: (_) {
if (!_isDebouncing) {
onChange(widget.controller.bounds!);
}
onChange(widget.controller.bounds!);
},
builder: (context, List<Marker> markers) {
final index = int.parse(
@ -143,7 +145,7 @@ class _MapViewState extends State<MapView> {
),
),
Positioned(
bottom: 10,
bottom: widget.bottomSheetDraggableAreaHeight + 10,
right: 10,
child: Column(
children: [

View file

@ -3,6 +3,7 @@ import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_map/plugin_api.dart";
import "package:photos/extensions/list.dart";
import "package:photos/theme/ente_theme.dart";
// Credit: This code is based on the Rich Attribution widget from the flutter_map
class MapAttributionWidget extends StatefulWidget {
@ -123,14 +124,16 @@ class MapAttributionWidgetState extends State<MapAttributionWidget> {
}
WidgetsBinding.instance.addPostFrameCallback(
(_) => WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(
() => persistentAttributionSize =
(persistentAttributionKey.currentContext!.findRenderObject()
as RenderBox)
.size,
),
),
(_) => WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(
() => persistentAttributionSize =
(persistentAttributionKey.currentContext!.findRenderObject()
as RenderBox)
.size,
);
}
}),
);
}
@ -182,6 +185,7 @@ class MapAttributionWidgetState extends State<MapAttributionWidget> {
icon: Icon(
Icons.info_outlined,
size: widget.permanentHeight,
color: getEnteColorScheme(context).backgroundElevated,
),
))(
context,

View file

@ -34,7 +34,7 @@ class OSMFranceTileLayer extends StatelessWidget {
fallbackUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: const ['a', 'b', 'c'],
tileProvider: CachedNetworkTileProvider(),
backgroundColor: Colors.transparent,
backgroundColor: const Color.fromARGB(255, 246, 246, 246),
userAgentPackageName: _userAgent,
panBuffer: 1,
);

View file

@ -19,12 +19,14 @@ class FileSelectionOverlayBar extends StatefulWidget {
final SelectedFiles selectedFiles;
final Collection? collection;
final DeviceCollection? deviceCollection;
final Color? backgroundColor;
const FileSelectionOverlayBar(
this.galleryType,
this.selectedFiles, {
this.collection,
this.deviceCollection,
this.backgroundColor,
Key? key,
}) : super(key: key);
@ -35,14 +37,14 @@ class FileSelectionOverlayBar extends StatefulWidget {
class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
final GlobalKey shareButtonKey = GlobalKey();
final ValueNotifier<double> _bottomPosition = ValueNotifier(-150.0);
final ValueNotifier<bool> _hasSelectedFilesNotifier = ValueNotifier(false);
late bool showDeleteOption;
@override
void initState() {
super.initState();
showDeleteOption = widget.galleryType.showDeleteIconOption();
widget.selectedFiles.addListener(_selectedFilesListener);
super.initState();
}
@override
@ -125,15 +127,17 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
),
);
return ValueListenableBuilder(
valueListenable: _bottomPosition,
valueListenable: _hasSelectedFilesNotifier,
builder: (context, value, child) {
return AnimatedPositioned(
curve: Curves.easeInOutExpo,
bottom: _bottomPosition.value,
right: 0,
left: 0,
return AnimatedCrossFade(
firstCurve: Curves.easeInOutExpo,
secondCurve: Curves.easeInOutExpo,
sizeCurve: Curves.easeInOutExpo,
crossFadeState: _hasSelectedFilesNotifier.value
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 400),
child: BottomActionBarWidget(
firstChild: BottomActionBarWidget(
selectedFiles: widget.selectedFiles,
hasSmallerBottomPadding: true,
type: widget.galleryType,
@ -151,7 +155,9 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
}
},
iconButtons: iconsButton,
backgroundColor: widget.backgroundColor,
),
secondChild: const SizedBox(width: double.infinity),
);
},
);
@ -167,8 +173,6 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
}
_selectedFilesListener() {
widget.selectedFiles.files.isNotEmpty
? _bottomPosition.value = 0.0
: _bottomPosition.value = -150.0;
_hasSelectedFilesNotifier.value = widget.selectedFiles.files.isNotEmpty;
}
}

View file

@ -40,6 +40,7 @@ class MultipleGroupsGalleryView extends StatelessWidget {
final String logTag;
final Logger logger;
final bool showSelectAllByDefault;
final bool isScrollablePositionedList;
const MultipleGroupsGalleryView({
required this.hugeListViewKey,
@ -60,6 +61,7 @@ class MultipleGroupsGalleryView extends StatelessWidget {
required this.logTag,
required this.logger,
required this.showSelectAllByDefault,
required this.isScrollablePositionedList,
super.key,
});
@ -72,6 +74,7 @@ class MultipleGroupsGalleryView extends StatelessWidget {
totalCount: groupedFiles.length,
isDraggableScrollbarEnabled: groupedFiles.length > 10,
disableScroll: disableScroll,
isScrollablePositionedList: isScrollablePositionedList,
waitBuilder: (_) {
return const EnteLoadingWidget();
},

View file

@ -46,6 +46,7 @@ class Gallery extends StatefulWidget {
final bool disableScroll;
final bool limitSelectionToOne;
final bool showSelectAllByDefault;
final bool isScrollablePositionedList;
// add a Function variable to get sort value in bool
final SortAscFn? sortAsyncFn;
@ -69,6 +70,7 @@ class Gallery extends StatefulWidget {
this.limitSelectionToOne = false,
this.sortAsyncFn,
this.showSelectAllByDefault = true,
this.isScrollablePositionedList = true,
Key? key,
}) : super(key: key);
@ -242,6 +244,7 @@ class _GalleryState extends State<Gallery> {
footer: widget.footer,
selectedFiles: widget.selectedFiles,
showSelectAllByDefault: widget.showSelectAllByDefault,
isScrollablePositionedList: widget.isScrollablePositionedList,
),
);
}

View file

@ -203,6 +203,7 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
builder: (context, snapshot) {
if (snapshot.hasData) {
return Stack(
alignment: Alignment.bottomCenter,
children: [
Gallery(
loadingWidget: Column(

View file

@ -1,15 +1,26 @@
import 'dart:async';
import 'package:flutter/material.dart';
import "package:photos/models/typedefs.dart";
class Debouncer {
final Duration _duration;
final ValueNotifier<bool> _debounceActiveNotifier = ValueNotifier(false);
/// If executionInterval is not null, then the debouncer will execute the
/// current callback it has in run() method repeatedly in the given interval.
final int? executionInterval;
Timer? _debounceTimer;
Debouncer(this._duration);
Debouncer(this._duration, {this.executionInterval});
final Stopwatch _stopwatch = Stopwatch();
void run(FutureVoidCallback fn) {
if (executionInterval != null) {
runCallbackIfIntervalTimeElapses(fn);
}
void run(Future<void> Function() fn) {
if (isActive()) {
_debounceTimer!.cancel();
}
@ -26,6 +37,14 @@ class Debouncer {
}
}
runCallbackIfIntervalTimeElapses(FutureVoidCallback fn) {
_stopwatch.isRunning ? null : _stopwatch.start();
if (_stopwatch.elapsedMilliseconds > executionInterval!) {
_stopwatch.reset();
fn();
}
}
bool isActive() => _debounceTimer != null && _debounceTimer!.isActive;
ValueNotifier<bool> get debounceActiveNotifier {

View file

@ -379,6 +379,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.4"
defer_pointer:
dependency: "direct main"
description:
name: defer_pointer
sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02
url: "https://pub.dev"
source: hosted
version: "0.0.2"
device_info:
dependency: "direct main"
description:

View file

@ -36,6 +36,7 @@ dependencies:
connectivity_plus: ^3.0.3
crypto: ^3.0.2
cupertino_icons: ^1.0.0
defer_pointer: ^0.0.2
device_info: ^2.0.2
dio: ^4.0.6
dots_indicator: ^2.0.0