diff --git a/.gitignore b/.gitignore index 5d4c02131..3a331414f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ android/app/.settings/* fastlane/report.xml +TensorFlowLiteC.framework \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml index b26e945b8..5f349f7f4 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml @@ -2,5 +2,4 @@ - diff --git a/android/build.gradle b/android/build.gradle index 04b667263..be653d816 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,6 +31,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index bcef537c9..c047d22df 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -747,4 +747,4 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; -} +} \ No newline at end of file diff --git a/lib/ui/components/home_header_widget.dart b/lib/ui/components/home_header_widget.dart index 62e747bc3..58c35229f 100644 --- a/lib/ui/components/home_header_widget.dart +++ b/lib/ui/components/home_header_widget.dart @@ -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 { 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( + 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(), + ), + ) + } + }, + ) + ], + ), + ) ], ); } diff --git a/lib/ui/map/image_marker.dart b/lib/ui/map/image_marker.dart new file mode 100644 index 000000000..53a56e933 --- /dev/null +++ b/lib/ui/map/image_marker.dart @@ -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, + }); +} diff --git a/lib/ui/map/map_button.dart b/lib/ui/map/map_button.dart new file mode 100644 index 000000000..c1c656a12 --- /dev/null +++ b/lib/ui/map/map_button.dart @@ -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), + ); + } +} diff --git a/lib/ui/map/map_credits.dart b/lib/ui/map/map_credits.dart new file mode 100644 index 000000000..d250af925 --- /dev/null +++ b/lib/ui/map/map_credits.dart @@ -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/')); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/map/map_gallery_tile.dart b/lib/ui/map/map_gallery_tile.dart new file mode 100644 index 000000000..36b15fa44 --- /dev/null +++ b/lib/ui/map/map_gallery_tile.dart @@ -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, + ), + ); + } +} diff --git a/lib/ui/map/map_gallery_tile_badge.dart b/lib/ui/map/map_gallery_tile_badge.dart new file mode 100644 index 000000000..b51afc9cc --- /dev/null +++ b/lib/ui/map/map_gallery_tile_badge.dart @@ -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, + ), + ), + ), + ); + } +} diff --git a/lib/ui/map/map_marker.dart b/lib/ui/map/map_marker.dart new file mode 100644 index 000000000..076c168d8 --- /dev/null +++ b/lib/ui/map/map_marker.dart @@ -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, + ), + ); +} diff --git a/lib/ui/map/map_screen.dart b/lib/ui/map/map_screen.dart new file mode 100644 index 000000000..2baff4210 --- /dev/null +++ b/lib/ui/map/map_screen.dart @@ -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 createState() { + return _MapScreenState(); + } +} + +class _MapScreenState extends State { + List imageMarkers = []; + List allImages = []; + List visibleImages = []; + MapController mapController = MapController(); + bool isLoading = true; + + @override + void initState() { + super.initState(); + initialize(); + } + + void initialize() async { + await getFiles(); + processFiles(allImages); + } + + Future getFiles() async { + final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs; + final ignoredIntIDs = {}; + for (var element in ignoredIDs) { + ignoredIntIDs.add(int.parse(element)); + } + allImages = await FilesDB.instance.getAllFilesFromDB(ignoredIntIDs); + } + + void processFiles(List files) { + final List 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(), + ], + ), + ), + ); + } +} diff --git a/lib/ui/map/map_view.dart b/lib/ui/map/map_view.dart new file mode 100644 index 000000000..b02dd1889 --- /dev/null +++ b/lib/ui/map/map_view.dart @@ -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 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 createState() => _MapViewState(); +} + +class _MapViewState extends State { + 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', + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/map/marker_image.dart b/lib/ui/map/marker_image.dart new file mode 100644 index 000000000..32e61af9e --- /dev/null +++ b/lib/ui/map/marker_image.dart @@ -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: [ + 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; + } +} diff --git a/pubspec.lock b/pubspec.lock index 71ee3c7a1..1e4f69d18 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 819f5ab67..9e535955c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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