Map improvements (#1245)
This commit is contained in:
commit
3ffb94022b
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -83,6 +83,7 @@ class HomeGalleryWidget extends StatelessWidget {
|
|||
scrollBottomSafeArea: bottomSafeArea + 180,
|
||||
);
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
gallery,
|
||||
FileSelectionOverlayBar(GalleryType.homepage, selectedFiles)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
223
lib/ui/map/map_pull_up_gallery.dart
Normal file
223
lib/ui/map/map_pull_up_gallery.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -203,6 +203,7 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Gallery(
|
||||
loadingWidget: Column(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue