diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 91b64bc..d60d089 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -51,4 +51,4 @@ jobs: api-level: ${{ matrix.api-level }} arch: x86_64 profile: Nexus 6 - script: flutter test integration_test + script: flutter drive --driver=integration_test/driver.dart --target=integration_test/panoramax_test.dart diff --git a/README.md b/README.md index df5abe0..de6625c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ docker-compose up -d ### Redirect WSL port (Only for docker WSL) ```shell +netsh interface portproxy delete v4tov4 listenport=5000 listenaddress=0.0.0.0 netsh interface portproxy add v4tov4 listenport=5000 listenaddress=0.0.0.0 connectport=5000 connectaddress= ``` To retrieve execute the following command from your wsl machine : diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 84af27a..edd56a3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - - - - - + main() async { + integrationDriver(); + await addPermission('android.permission.ACCESS_FINE_LOCATION'); + await addPermission('android.permission.CAMERA'); +} + +Future addPermission(String permission) async { + await Process.run( + 'adb', + [ + 'shell', + 'pm', + 'grant', + 'com.example.panoramax_mobile', + permission + ], + ); +} \ No newline at end of file diff --git a/lib/component/loader.dart b/lib/component/loader.dart new file mode 100644 index 0000000..79339ae --- /dev/null +++ b/lib/component/loader.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../constant.dart'; + +class Loader extends StatelessWidget { + final bool shadowBackground; + final Widget message; + + const Loader({ + super.key, + this.shadowBackground = false, + required this.message + }); + + @override + Widget build(BuildContext context) { + return Container( + color: this.shadowBackground ? + Color.fromRGBO(0, 0, 0, 50) : + Colors.transparent + , + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LoadingAnimationWidget.staggeredDotsWave( + color: DEFAULT_COLOR, + size: 50, + ), + message + ], + ) + ); + } +} \ No newline at end of file diff --git a/lib/constant.dart b/lib/constant.dart new file mode 100644 index 0000000..67e4371 --- /dev/null +++ b/lib/constant.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +const MaterialColor DEFAULT_COLOR = Colors.indigo; +const String API_HOSTNAME = '10.0.2.2:5000'; +const bool API_IS_HTTPS = false; \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5661c7a..a8210fb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -9,6 +9,7 @@ "capture": "Take a picture", "switchCamera": "Switch camera", "createSequenceWithPicture_tooltip": "Create a new sequence with captured pictures", + "waitDuringProcessing": "Processing, please wait...", "newSequenceNameField": "Name", "newSequenceNameField_placeholder": "Enter the new sequence name", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b37baf6..4525d4c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -9,6 +9,7 @@ "capture": "Prendre la une photo", "switchCamera": "Changer de camera", "createSequenceWithPicture_tooltip": "Créer une nouvelle séquence avec les photos prises", + "waitDuringProcessing": "Traitement en cours, veuillez patienter...", "newSequenceNameField": "Nom", "newSequenceNameField_placeholder": "Saisissez le nom de la nouvelle séquence", diff --git a/lib/main.dart b/lib/main.dart index 67f6455..0e93e96 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,8 +17,9 @@ import 'package:go_router/go_router.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:loading_btn/loading_btn.dart'; - +import 'component/loader.dart'; import 'service/api/api.dart'; +import 'constant.dart'; part 'component/app_bar.dart'; part 'component/collection_preview.dart'; @@ -26,6 +27,7 @@ part 'page/homepage.dart'; part 'page/capture_page.dart'; part 'page/collection_creation_page.dart'; part 'service/routing.dart'; +part 'service/permission_helper.dart'; final String DATE_FORMATTER = 'dd/MM/y HH:mm:ss'; @@ -42,7 +44,7 @@ class PanoramaxApp extends StatelessWidget { return MaterialApp.router( title: 'Panoramax', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + colorScheme: ColorScheme.fromSeed(seedColor: DEFAULT_COLOR), useMaterial3: true, ), localizationsDelegates: const [ diff --git a/lib/page/capture_page.dart b/lib/page/capture_page.dart index e1d6468..2396294 100644 --- a/lib/page/capture_page.dart +++ b/lib/page/capture_page.dart @@ -11,8 +11,9 @@ class CapturePage extends StatefulWidget { class _CapturePageState extends State { late CameraController _cameraController; + bool _isProcessing = false; bool _isRearCameraSelected = true; - List _imgListCaptured = []; + final List _imgListCaptured = []; @override void dispose() { @@ -31,6 +32,9 @@ class _CapturePageState extends State { } Future takePicture() async { + setState(() { + _isProcessing = true; + }); if (!_cameraController.value.isInitialized) { return null; } @@ -38,56 +42,50 @@ class _CapturePageState extends State { return null; } try { - await _cameraController.setFlashMode(FlashMode.off); - final XFile rawImage = await _cameraController.takePicture(); - debugPrint(rawImage.path); - - bool storagePermission = await Permission.storage.isGranted; - bool mediaPermission = await Permission.accessMediaLocation.isGranted; - bool manageExternalStoragePermission = await Permission.manageExternalStorage.isGranted; - bool locationPermission = await Permission.location.isGranted; - - if (!storagePermission) { - storagePermission = await Permission.storage.request().isGranted; - } - - if (!mediaPermission) { - mediaPermission = - await Permission.accessMediaLocation.request().isGranted; - } - - if (!manageExternalStoragePermission) { - manageExternalStoragePermission = (await Permission.manageExternalStorage.request()).isGranted; - } - - if (!locationPermission) { - locationPermission = (await Permission.location.request()).isGranted; - } - - bool isPermissionGranted = locationPermission && mediaPermission && manageExternalStoragePermission; - - if (isPermissionGranted) { - final exif = FlutterExif.fromPath(rawImage.path); - final currentLocation = await Geolocator.getCurrentPosition(); - await exif.setLatLong(currentLocation.latitude, currentLocation.longitude); - await exif.setAltitude(currentLocation.altitude); - await exif.saveAttributes(); - setState(() { - var capturedPicture = new File(rawImage.path); - _imgListCaptured.add(capturedPicture); + if (await PermissionHelper.isPermissionGranted()) { + await Future.wait([ + getPictureFromCamera(), + Geolocator.getCurrentPosition() + ]).then((value) async { + final XFile rawImage = value[0] as XFile; + final Position currentLocation = value[1] as Position; + await addExifTags(rawImage, currentLocation); + addImageToList(rawImage); }); } else { - throw Exception("No permission to move file"); + await PermissionHelper.askMissingPermission(); + takePicture(); } } on CameraException catch (e) { - debugPrint('Error occured while taking picture: $e'); + debugPrint('Error occurred while taking picture: $e'); return null; } } + void addImageToList(XFile rawImage) { + setState(() { + _imgListCaptured.add(File(rawImage.path)); + _isProcessing = false; + }); + } + + Future getPictureFromCamera() async { + await _cameraController.setFlashMode(FlashMode.off); + final XFile rawImage = await _cameraController.takePicture(); + debugPrint(rawImage.path); + return rawImage; + } + + Future addExifTags(XFile rawImage, Position currentLocation) async { + final exif = FlutterExif.fromPath(rawImage.path); + await exif.setLatLong(currentLocation.latitude, currentLocation.longitude); + await exif.setAltitude(currentLocation.altitude); + await exif.saveAttributes(); + } + Future initCamera(CameraDescription cameraDescription) async { _cameraController = - CameraController(cameraDescription, ResolutionPreset.high); + CameraController(cameraDescription, ResolutionPreset.high, enableAudio: false); try { await _cameraController.initialize().then((_) { if (!mounted) return; @@ -112,72 +110,122 @@ class _CapturePageState extends State { ); return Stack( children: [ - (_cameraController.value.isInitialized) - ? CameraPreview(_cameraController) - : Container( - color: Colors.transparent, - child: const Center(child: CircularProgressIndicator())), - new Positioned( - bottom: height, - left: 0, - child: new Container( - width: MediaQuery.of(context).size.width, - height: height, - decoration: new BoxDecoration(color: Colors.transparent), - child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: IconButton( - onPressed: takePicture, - iconSize: 100, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: const Icon(Icons.circle_outlined, color: Colors.white), - tooltip: AppLocalizations.of(context)!.capture - )), - ]), - ) - ), - new Positioned( + cameraPreview(), + captureButton(height, context), + Positioned( bottom: 0, left: 0, - child: new Container( + child: Container( width: MediaQuery.of(context).size.width, height: height, - decoration: new BoxDecoration(color: Colors.black), + decoration: const BoxDecoration(color: Colors.black), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 30, - icon: Icon( - _isRearCameraSelected - ? CupertinoIcons.switch_camera - : CupertinoIcons.switch_camera_solid, - color: Colors.white), - onPressed: () { - setState( - () => _isRearCameraSelected = !_isRearCameraSelected); - initCamera(widget.cameras![_isRearCameraSelected ? 0 : 1]); - }, - tooltip: AppLocalizations.of(context)!.switchCamera - )), - _imgListCaptured.length > 0 ? badges.Badge( - badgeContent: Text('${_imgListCaptured.length}'), - child: cartIcon, - ): cartIcon, - Expanded( - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 30, - icon: Icon(Icons.send_outlined, - color: Colors.white), - onPressed: goToCollectionCreationPage, - tooltip: AppLocalizations.of(context)!.createSequenceWithPicture_tooltip - )), + switchCameraButton(context), + imageCart(cartIcon), + createSequenceButton(context), ]), ) - ) + ), + if(_isProcessing) processingLoader(context) ] ); } + + Expanded switchCameraButton(BuildContext context) { + return Expanded( + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 30, + icon: Icon( + _isRearCameraSelected + ? CupertinoIcons.switch_camera + : CupertinoIcons.switch_camera_solid, + color: Colors.white), + onPressed: () { + setState( + () => _isRearCameraSelected = !_isRearCameraSelected); + initCamera(widget.cameras![_isRearCameraSelected ? 0 : 1]); + }, + tooltip: AppLocalizations.of(context)!.switchCamera + ) + ); + } + + Expanded createSequenceButton(BuildContext context) { + return Expanded( + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 30, + icon: const Icon( + Icons.send_outlined, + color: Colors.white + ), + onPressed: goToCollectionCreationPage, + tooltip: AppLocalizations.of(context)!.createSequenceWithPicture_tooltip + ) + ); + } + + Widget imageCart(IconButton cartIcon) { + return _imgListCaptured.isNotEmpty ? + badges.Badge( + badgeContent: Text('${_imgListCaptured.length}'), + child: cartIcon, + ): + cartIcon; + } + + Positioned captureButton(double height, BuildContext context) { + return Positioned( + bottom: height, + left: 0, + child: Container( + width: MediaQuery.of(context).size.width, + height: height, + decoration: const BoxDecoration(color: Colors.transparent), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Expanded( + child: IconButton( + onPressed: takePicture, + iconSize: 100, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.circle_outlined, color: Colors.white), + tooltip: AppLocalizations.of(context)!.capture + )), + ]), + ) + ); + } + + StatelessWidget cameraPreview() { + return _cameraController.value.isInitialized + ? CameraPreview(_cameraController) + : Container( + color: Colors.transparent, + child: const Center(child: CircularProgressIndicator() + ) + ); + } + + Positioned processingLoader(BuildContext context) { + return Positioned( + top: 0, + bottom: 0, + left: 0, + right: 0, + child: Loader( + message: DefaultTextStyle( + style: Theme.of(context).textTheme.bodyLarge!, + child: Text( + AppLocalizations.of(context)!.waitDuringProcessing, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + shadowBackground: true + ) + ); + } } \ No newline at end of file diff --git a/lib/page/collection_creation_page.dart b/lib/page/collection_creation_page.dart index b62d585..7e33a82 100644 --- a/lib/page/collection_creation_page.dart +++ b/lib/page/collection_creation_page.dart @@ -14,7 +14,7 @@ class CollectionCreationPage extends StatefulWidget { class _CarouselWithIndicatorState extends State { int _current = 0; final CarouselController _carouselController = CarouselController(); - final collectionNameTextController = TextEditingController(); + final collectionNameTextController = TextEditingController(text: 'My collection ${DateFormat(DATE_FORMATTER).format(new DateTime.now())}'); final _formKey = GlobalKey(); @override diff --git a/lib/page/homepage.dart b/lib/page/homepage.dart index 169a3cb..e88b3d8 100644 --- a/lib/page/homepage.dart +++ b/lib/page/homepage.dart @@ -36,8 +36,11 @@ class _HomePageState extends State { } Future _createCollection() async { + if (!await PermissionHelper.isPermissionGranted()) { + await PermissionHelper.askMissingPermission(); + } await availableCameras().then((availableCameras) => - context.push(Routes.newSequenceCapture, extra: availableCameras) + context.push(Routes.newSequenceCapture, extra: availableCameras) ); } @@ -66,27 +69,26 @@ class _HomePageState extends State { child: Scaffold( appBar: PanoramaxAppBar(context: context), body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: Semantics( - header: true, - child: Text( - AppLocalizations.of(context)!.yourSequence, - style: GoogleFonts.nunito( - fontSize: 25, - fontWeight: FontWeight.w400 - ) - ), - ) - ), - SizedBox( - height: 690, - child: displayBody(isLoading) - ) - ] + child: Stack( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Semantics( + header: true, + child: Text( + AppLocalizations.of(context)!.yourSequence, + style: GoogleFonts.nunito( + fontSize: 25, + fontWeight: FontWeight.w400 + ) + ), + ) + ), + SizedBox( + height: 720, + child: displayBody(isLoading) + ) + ] ), ), floatingActionButton: FloatingActionButton( @@ -133,7 +135,9 @@ class LoaderIndicatorView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Center( - child: Text(AppLocalizations.of(context)!.loading) + child: Loader( + message: Text(AppLocalizations.of(context)!.loading), + ), ) ] ); diff --git a/lib/service/api/api.dart b/lib/service/api/api.dart index 73ddf64..3badffd 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -5,8 +5,6 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import 'model/geo_visio.dart'; import 'dart:io'; +import '../../constant.dart'; part 'endpoint/collections_api.dart'; - -const String API_HOSTNAME = '10.0.2.2:5000'; -const bool API_IS_HTTPS = false; diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index 94a5989..3916598 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -34,7 +34,9 @@ class CollectionsApi { var response = await http.get(url); if(response.statusCode >= 200) { - return GeoVisioCollections.fromJson(json.decode(response.body)); + var geovisioCollections = GeoVisioCollections.fromJson(json.decode(response.body)); + geovisioCollections.collections.sort((a, b) => b.updated!.compareTo(a.updated!)); + return geovisioCollections; } else { throw new Exception('${response.statusCode} - ${response.body}'); } diff --git a/lib/service/permission_helper.dart b/lib/service/permission_helper.dart new file mode 100644 index 0000000..199459b --- /dev/null +++ b/lib/service/permission_helper.dart @@ -0,0 +1,23 @@ +part of panoramax; + +class PermissionHelper { + + static Future isPermissionGranted() async { + bool cameraPermission = await Permission.camera.isGranted; + bool locationPermission = await Permission.location.isGranted; + return locationPermission && cameraPermission; + } + + static Future askMissingPermission() async { + bool locationPermission = await Permission.location.isGranted; + bool cameraPermission = await Permission.camera.isGranted; + + if (!locationPermission) { + locationPermission = (await Permission.location.request()).isGranted; + } + + if (!cameraPermission) { + cameraPermission = (await Permission.camera.request()).isGranted; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 4837852..c1c26dd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -540,6 +540,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + loading_animation_widget: + dependency: "direct main" + description: + name: loading_animation_widget + sha256: "1901682600273a966c34cf44a85fc5355da92a8d08a8a43c11adc4e471993e3a" + url: "https://pub.dev" + source: hosted + version: "1.2.0+4" loading_btn: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d3b21dc..974846c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,9 +39,6 @@ dependencies: geolocator: ^10.1.0 carousel_slider: ^4.2.1 loading_btn: ^1.0.3 - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 google_fonts: ^6.1.0 flutter_localization: ^0.2.0 @@ -49,6 +46,7 @@ dependencies: camera: ^0.10.5+9 go_router: ^13.2.0 equatable: ^2.0.5 + loading_animation_widget: 1.2.0+4 dev_dependencies: flutter_test: