feat[map_gallery]: implement map gallery

This commit is contained in:
Muhesh7 2023-06-04 23:59:03 +05:30
parent 3d07ca4e05
commit 6e2ab6d6ba
16 changed files with 754 additions and 13 deletions

1
.gitignore vendored
View file

@ -42,3 +42,4 @@ android/app/.settings/*
fastlane/report.xml
TensorFlowLiteC.framework

View file

@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -31,6 +31,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View file

@ -747,4 +747,4 @@
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import 'package:photos/ui/map/map_screen.dart';
import 'package:photos/ui/viewer/search/search_widget.dart';
class HomeHeaderWidget extends StatefulWidget {
@ -17,18 +18,65 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButtonWidget(
iconButtonType: IconButtonType.primary,
icon: Icons.menu_outlined,
onTap: () {
Scaffold.of(context).openDrawer();
},
Flexible(
flex: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButtonWidget(
iconButtonType: IconButtonType.primary,
icon: Icons.menu_outlined,
onTap: () {
Scaffold.of(context).openDrawer();
},
),
const SizedBox(width: 24),
],
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: widget.centerWidget,
Flexible(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: widget.centerWidget,
),
),
const SearchIconWidget(),
Flexible(
flex: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SearchIconWidget(),
PopupMenuButton(
padding: const EdgeInsets.symmetric(horizontal: 0),
offset: const Offset(0, 40),
itemBuilder: (context) => [
PopupMenuItem<int>(
value: 0,
child: Row(
children: const [
Icon(Icons.map_outlined),
SizedBox(width: 16),
Text(
"Map",
),
],
),
),
],
onSelected: (item) => {
if (item == 0)
{
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MapScreen(),
),
)
}
},
)
],
),
)
],
);
}

View file

@ -0,0 +1,13 @@
import "package:photos/models/file.dart";
class ImageMarker {
final File imageFile;
final double latitude;
final double longitude;
ImageMarker({
required this.imageFile,
required this.latitude,
required this.longitude,
});
}

View file

@ -0,0 +1,25 @@
import "package:flutter/material.dart";
class MapButton extends StatelessWidget {
final String heroTag;
final IconData icon;
final VoidCallback onPressed;
const MapButton({
super.key,
required this.icon,
required this.onPressed,
required this.heroTag,
});
@override
Widget build(BuildContext context) {
return FloatingActionButton(
heroTag: heroTag,
backgroundColor: Colors.white,
mini: true,
onPressed: onPressed,
child: Icon(icon),
);
}
}

View file

@ -0,0 +1,62 @@
import "package:flutter/gestures.dart";
import "package:flutter/material.dart";
import "package:url_launcher/url_launcher.dart";
class MapCredits extends StatelessWidget {
const MapCredits({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: GestureDetector(
child: Text.rich(
style: const TextStyle(
fontSize: 11,
),
TextSpan(
text: 'Map © ',
children: [
TextSpan(
text: 'OpenStreetMap',
style: const TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(Uri.parse('https://www.openstreetmap.org/'));
},
),
const TextSpan(text: ' contributors'),
const TextSpan(text: ' | Tiles © '),
TextSpan(
text: 'HOT',
style: const TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(Uri.parse('https://www.hotosm.org/'));
},
),
const TextSpan(text: ' | Hosted @ '),
TextSpan(
text: 'OSM France',
style: const TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(Uri.parse('https://www.openstreetmap.fr/'));
},
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,25 @@
import "package:flutter/material.dart";
import "package:photos/ui/map/image_marker.dart";
import "package:photos/ui/map/marker_image.dart";
class MapGalleryTile extends StatelessWidget {
final ImageMarker imageMarker;
const MapGalleryTile({super.key, required this.imageMarker});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.black,
),
child: MarkerImage(
key: key,
file: imageMarker.imageFile,
seperator: 65,
),
);
}
}

View file

@ -0,0 +1,45 @@
import "package:flutter/material.dart";
class MapGalleryTileBadge extends StatelessWidget {
final int size;
const MapGalleryTileBadge({super.key, required this.size});
String formatNumber(int number) {
if (number <= 99) {
return number.toString();
} else if (number <= 999) {
return '${(number / 100).toStringAsFixed(0)}00+';
} else if (number >= 1000 && number < 2000) {
return '1K+';
} else {
final int thousands = ((number - 1) ~/ 1000);
return '${thousands}K+';
}
}
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(5),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
shape: BoxShape.rectangle,
color: Colors.green,
),
child: Text(
formatNumber(size),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
),
);
}
}

View file

@ -0,0 +1,21 @@
import "package:flutter/material.dart";
import "package:flutter_map/flutter_map.dart";
import "package:latlong2/latlong.dart";
import "package:photos/ui/map/image_marker.dart";
import "package:photos/ui/map/marker_image.dart";
Marker mapMarker(ImageMarker imageMarker, String key) {
return Marker(
key: Key(key),
width: 75,
height: 75,
point: LatLng(
imageMarker.latitude,
imageMarker.longitude,
),
builder: (context) => MarkerImage(
file: imageMarker.imageFile,
seperator: 85,
),
);
}

187
lib/ui/map/map_screen.dart Normal file
View file

@ -0,0 +1,187 @@
import "dart:async";
import "dart:math";
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import "package:latlong2/latlong.dart";
import "package:photos/db/files_db.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_load_result.dart";
import "package:photos/services/ignored_files_service.dart";
import "package:photos/ui/map/image_marker.dart";
import "package:photos/ui/map/map_credits.dart";
import "package:photos/ui/map/map_view.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 MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<StatefulWidget> createState() {
return _MapScreenState();
}
}
class _MapScreenState extends State<MapScreen> {
List<ImageMarker> imageMarkers = [];
List<File> allImages = [];
List<File> visibleImages = [];
MapController mapController = MapController();
bool isLoading = true;
@override
void initState() {
super.initState();
initialize();
}
void initialize() async {
await getFiles();
processFiles(allImages);
}
Future<void> getFiles() async {
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
final ignoredIntIDs = <int>{};
for (var element in ignoredIDs) {
ignoredIntIDs.add(int.parse(element));
}
allImages = await FilesDB.instance.getAllFilesFromDB(ignoredIntIDs);
}
void processFiles(List<File> files) {
final List<ImageMarker> tempMarkers = [];
for (var file in files) {
// if (file.hasLocation && location != null) {
final rand = Random();
tempMarkers.add(
ImageMarker(
latitude: 10.786985 + rand.nextDouble() / 10,
longitude: 78.6882166 + rand.nextDouble() / 10,
imageFile: file,
),
);
// }
}
setState(() {
imageMarkers = tempMarkers;
isLoading = false;
});
updateVisibleImages(mapController.bounds!);
}
void updateVisibleImages(LatLngBounds bounds) async {
final images = imageMarkers
.where((imageMarker) {
final point = LatLng(imageMarker.latitude, imageMarker.longitude);
return bounds.contains(point);
})
.map((imageMarker) => imageMarker.imageFile)
.toList();
setState(() {
visibleImages = images;
});
}
String formatNumber(int number) {
if (number <= 99) {
return number.toString();
} else if (number <= 999) {
return '${(number / 100).toStringAsFixed(0)}00+';
} else if (number >= 1000 && number < 2000) {
return '1K+';
} else {
final int thousands = ((number - 1) ~/ 1000);
return '${thousands}K+';
}
}
void onTap(File image, int index) {
final page = DetailPage(
DetailPageConfiguration(
List.unmodifiable(visibleImages),
(
creationStartTime,
creationEndTime, {
limit,
asc,
}) async {
final result = FileLoadResult(allImages, false);
return result;
},
index,
'Map',
),
);
routeToPage(
context,
page,
forceCustomPageRoute: true,
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Stack(
children: [
Column(
children: [
Expanded(
child: MapView(
updateVisibleImages: updateVisibleImages,
controller: mapController,
imageMarkers: imageMarkers,
),
),
const SizedBox(
child: MapCredits(),
),
SizedBox(
height: 120,
child: Center(
child: ListView.builder(
itemCount: visibleImages.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final image = visibleImages[index];
return InkWell(
onTap: () => onTap(image, index),
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 10,
),
width: 100,
height: 100,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: ThumbnailWidget(image),
),
),
);
},
),
),
)
],
),
isLoading
? Container(
color: Colors.black87,
child: const Center(
child: CircularProgressIndicator(color: Colors.green),
),
)
: const SizedBox.shrink(),
],
),
),
);
}
}

160
lib/ui/map/map_view.dart Normal file
View file

@ -0,0 +1,160 @@
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";
import "package:latlong2/latlong.dart";
import "package:photos/ui/map/image_marker.dart";
import "package:photos/ui/map/map_button.dart";
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";
class MapView extends StatefulWidget {
final List<ImageMarker> imageMarkers;
final Function updateVisibleImages;
final MapController controller;
const MapView({
Key? key,
required this.updateVisibleImages,
required this.imageMarkers,
required this.controller,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _MapViewState();
}
class _MapViewState extends State<MapView> {
Timer? _debounceTimer;
LatLng center = LatLng(10.732951, 78.405635);
bool _isDebouncing = false;
void _onPositionChanged(position, hasGesture) {
if (position.bounds != null) {
if (!_isDebouncing) {
_isDebouncing = true;
_debounceTimer?.cancel(); // Cancel previous debounce timer
_debounceTimer = Timer(const Duration(milliseconds: 200), () {
widget.updateVisibleImages(position.bounds!);
_isDebouncing = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
FlutterMap(
mapController: widget.controller,
options: MapOptions(
center: center,
zoom: 5,
minZoom: 5,
maxZoom: 16.5,
onPositionChanged: _onPositionChanged,
plugins: [
MarkerClusterPlugin(),
],
),
layers: [
TileLayerOptions(
urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: ['a', 'b', 'c'],
),
MarkerClusterLayerOptions(
maxClusterRadius: 100,
showPolygon: true,
size: const Size(75, 75),
fitBoundsOptions: const FitBoundsOptions(
padding: EdgeInsets.all(50),
),
markers: widget.imageMarkers.asMap().entries.map((marker) {
final imageMarker = marker.value;
return mapMarker(imageMarker, marker.key.toString());
}).toList(),
polygonOptions: const PolygonOptions(
borderColor: Colors.redAccent,
color: Colors.black12,
borderStrokeWidth: 3,
),
builder: (context, markers) {
final index = int.parse(
markers.first.key
.toString()
.replaceAll(RegExp(r'[^0-9]'), ''),
);
return Stack(
children: [
MapGalleryTile(
key: Key(markers.first.key.toString()),
imageMarker: widget.imageMarkers[index],
),
MapGalleryTileBadge(size: markers.length)
],
);
},
),
],
),
Positioned(
bottom: 10,
left: 10,
child: MapButton(
icon: Icons.my_location,
onPressed: () {
widget.controller.move(
center,
widget.controller.zoom,
);
},
heroTag: 'location',
),
),
Positioned(
top: 10,
left: 10,
child: MapButton(
icon: Icons.arrow_back,
onPressed: () {
Navigator.pop(context);
},
heroTag: 'back',
),
),
Positioned(
bottom: 10,
right: 10,
child: Column(
children: [
MapButton(
icon: Icons.add,
onPressed: () {
widget.controller.move(
widget.controller.center,
widget.controller.zoom + 1,
);
},
heroTag: 'zoom-in',
),
MapButton(
icon: Icons.remove,
onPressed: () {
widget.controller.move(
widget.controller.center,
widget.controller.zoom - 1,
);
},
heroTag: 'zoom-out',
),
],
),
),
],
);
}
}

View file

@ -0,0 +1,55 @@
import "package:flutter/material.dart";
import "package:photos/models/file.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
class MarkerImage extends StatelessWidget {
final File file;
final double seperator;
const MarkerImage({super.key, required this.file, required this.seperator});
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.green,
width: 1.75,
),
),
child: ThumbnailWidget(file),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.only(top: seperator),
child: CustomPaint(
painter: MarkerPointer(),
),
),
)
],
);
}
}
class MarkerPointer extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.green;
final path = Path();
path.moveTo(5, -12);
path.lineTo(0, 0);
path.lineTo(-5, -12);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

View file

@ -49,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.5"
animated_stack_widget:
dependency: transitive
description:
name: animated_stack_widget
sha256: ce4788dd158768c9d4388354b6fb72600b78e041a37afc4c279c63ecafcb9408
url: "https://pub.dev"
source: hosted
version: "0.0.4"
archive:
dependency: "direct main"
description:
@ -696,6 +704,30 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_map:
dependency: "direct main"
description:
name: flutter_map
sha256: "9401bcc83b1118ddd35c0b25efaa5af182572707f1887bbb7817c2337fcd8c97"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_map_marker_cluster:
dependency: "direct main"
description:
name: flutter_map_marker_cluster
sha256: "3eefbe1ed8ef16be52f9992363875992c688f4246e2d99be488b25f238ebfa7b"
url: "https://pub.dev"
source: hosted
version: "0.4.4"
flutter_map_marker_popup:
dependency: transitive
description:
name: flutter_map_marker_popup
sha256: "4ce4eaef4efb1ca38fc0620beb26eb65f4535ba686ae9988d7f7c4ec05fe1e3a"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
flutter_native_splash:
dependency: "direct main"
description:
@ -1020,6 +1052,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.6.1"
latlong2:
dependency: "direct main"
description:
name: latlong2
sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0"
url: "https://pub.dev"
source: hosted
version: "0.8.2"
like_button:
dependency: "direct main"
description:
@ -1036,6 +1076,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
lists:
dependency: transitive
description:
name: lists
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
loading_animations:
dependency: "direct main"
description:
@ -1132,6 +1180,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.0"
mgrs_dart:
dependency: transitive
description:
name: mgrs_dart
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mime:
dependency: transitive
description:
@ -1430,6 +1486,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
positioned_tap_detector_2:
dependency: transitive
description:
name: positioned_tap_detector_2
sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
process:
dependency: transitive
description:
@ -1438,6 +1502,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.4"
proj4dart:
dependency: transitive
description:
name: proj4dart
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
url: "https://pub.dev"
source: hosted
version: "2.1.0"
provider:
dependency: "direct main"
description:
@ -1868,6 +1940,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
transparent_image:
dependency: transitive
description:
name: transparent_image
sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f
url: "https://pub.dev"
source: hosted
version: "2.0.1"
tuple:
dependency: "direct main"
description:
@ -1908,6 +1988,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0"
unicode:
dependency: transitive
description:
name: unicode
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
universal_io:
dependency: transitive
description:
@ -2137,6 +2225,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
wkt_parser:
dependency: transitive
description:
name: wkt_parser
sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
xdg_directories:
dependency: transitive
description:

View file

@ -63,6 +63,8 @@ dependencies:
flutter_local_notifications: ^9.5.3+1
flutter_localizations:
sdk: flutter
flutter_map: ^0.13.1
flutter_map_marker_cluster: ^0.4.0
flutter_native_splash: ^2.2.0+1
flutter_password_strength: ^0.1.6
flutter_secure_storage: ^8.0.0
@ -77,6 +79,7 @@ dependencies:
in_app_purchase: ^3.0.7
intl: ^0.17.0
json_annotation: ^4.8.0
latlong2: ^0.8.1
like_button: ^2.0.2
loading_animations: ^2.1.0
local_auth: ^2.1.5
@ -132,6 +135,7 @@ dependencies:
wallpaper_manager_flutter: ^0.0.2
widgets_to_image: ^0.0.2
flutter_intl:
enabled: true