From bd4a9e1141620f2c57d307e9d22c3ac1f6411173 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Tue, 25 Jun 2024 14:42:03 +0200 Subject: [PATCH 01/22] feat: remember selected instance --- lib/constant.dart | 1 - lib/main.dart | 1 + lib/page/instance_page.dart | 22 ++++++++++------- lib/service/api/api.dart | 3 +-- .../api/endpoint/authentication_api.dart | 14 ++++++----- lib/service/api/endpoint/collections_api.dart | 17 ++++++------- lib/user.dart | 24 +++++++++++++++++++ 7 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 lib/user.dart diff --git a/lib/constant.dart b/lib/constant.dart index 2cdee5f..8c72c02 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -5,4 +5,3 @@ import 'package:flutter/material.dart'; const MaterialColor DEFAULT_COLOR = Colors.indigo; const bool API_IS_HTTPS = false; const Color BLUE = Color(0xFF010D37); -String API_HOSTNAME = 'openstreetmap'; diff --git a/lib/main.dart b/lib/main.dart index bd3bab8..53974ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,6 +43,7 @@ part 'page/upload_pictures_page.dart'; part 'service/routing.dart'; part 'service/permission_helper.dart'; part 'utils/gravity_orientation_detector.dart'; +part 'user.dart'; const String DATE_FORMATTER = 'dd/MM - HH:mm'; diff --git a/lib/page/instance_page.dart b/lib/page/instance_page.dart index a4bb10f..c8f6275 100644 --- a/lib/page/instance_page.dart +++ b/lib/page/instance_page.dart @@ -17,11 +17,9 @@ class _InstanceState extends State { bool isInstanceChosen = false; final cookieManager = WebviewCookieManager(); - int _selectedIndex = -1; - void authentication(String instance) { setState(() { - API_HOSTNAME = instance; + setInstance(instance); url = "https://panoramax.${instance}.fr/api/auth/login"; isInstanceChosen = true; }); @@ -29,20 +27,28 @@ class _InstanceState extends State { void getToken() async { final cookies = - await cookieManager.getCookies('https://panoramax.$API_HOSTNAME.fr'); + await cookieManager.getCookies('https://panoramax.${getInstance()}.fr'); var tokens = await AuthenticationApi.INSTANCE.apiTokensGet(cookies); var token = await AuthenticationApi.INSTANCE.apiTokenGet(tokens.id, cookies); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - print(token.jwt_token); - prefs.setString('token', token.jwt_token); + setToken(token.jwt_token); GetIt.instance() .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); } + void initState() { + super.initState(); + getInstance().then((instance) { + if (instance != null) { + GetIt.instance() + .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -56,7 +62,7 @@ class _InstanceState extends State { ..setNavigationDelegate(NavigationDelegate( onNavigationRequest: (request) { if (request.url == - "https://panoramax.$API_HOSTNAME.fr/") { + "https://panoramax.${getInstance()}.fr/") { getToken(); } return NavigationDecision.navigate; diff --git a/lib/service/api/api.dart b/lib/service/api/api.dart index 441c79c..a161b58 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -2,13 +2,12 @@ library panoramax.api; import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:panoramax_mobile/main.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'model/geo_visio.dart'; import 'model/geo_visio_auth.dart'; import 'dart:io'; -import '../../constant.dart'; - part 'endpoint/collections_api.dart'; part 'endpoint/authentication_api.dart'; diff --git a/lib/service/api/endpoint/authentication_api.dart b/lib/service/api/endpoint/authentication_api.dart index 89faf49..a49548e 100644 --- a/lib/service/api/endpoint/authentication_api.dart +++ b/lib/service/api/endpoint/authentication_api.dart @@ -4,7 +4,8 @@ class AuthenticationApi { static final AuthenticationApi INSTANCE = new AuthenticationApi(); Future apiTokensGet(List cookies) async { - final url = Uri.https("panoramax.$API_HOSTNAME.fr", '/api/users/me/tokens'); + final instance = await getInstance(); + final url = Uri.https("panoramax.$instance.fr", '/api/users/me/tokens'); var session = null; for (var cookie in cookies) { @@ -24,10 +25,12 @@ class AuthenticationApi { } } - Future apiTokenGet(String tokenId, List cookies) async { + Future apiTokenGet( + String tokenId, List cookies) async { // create path and map variables - var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/users/me/tokens/${tokenId}'); + final instance = await getInstance(); + var url = + Uri.https("panoramax.$instance.fr", '/api/users/me/tokens/${tokenId}'); var session = null; for (var cookie in cookies) { @@ -38,7 +41,7 @@ class AuthenticationApi { final response = await http.get(url, headers: {'cookie': session}); - if (response.statusCode >= 200) { + if (response.statusCode >= 200 && response.statusCode < 400) { var geoVisioJWTToken = GeoVisioJWTToken.fromJson(json.decode(response.body)); return geoVisioJWTToken; @@ -47,4 +50,3 @@ class AuthenticationApi { } } } - diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index 752f776..10f29e0 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -32,8 +32,9 @@ class CollectionsApi { } // create path and map variables - var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/collections', queryParams); + final instance = getInstance(); + var url = + Uri.https("panoramax.$instance.fr", '/api/collections', queryParams); var response = await http.get(url); if (response.statusCode >= 200) { @@ -49,10 +50,10 @@ class CollectionsApi { Future apiCollectionsCreate( {required String newCollectionName}) async { - var url = Uri.https("panoramax.$API_HOSTNAME.fr", '/api/collections'); + final instance = await getInstance(); + var url = Uri.https("panoramax.$instance.fr", '/api/collections'); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final token = prefs.getString('token'); + final token = await getToken(); var response = await http.post( url, @@ -75,11 +76,11 @@ class CollectionsApi { {required String collectionId, required int position, required File pictureToUpload}) async { + final instance = await getInstance(); var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/collections/${collectionId}/items'); + "panoramax.$instance.fr", '/api/collections/${collectionId}/items'); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final token = prefs.getString('token'); + final token = await getToken(); var request = http.MultipartRequest('POST', url) ..headers['Content-Type'] = 'application/json; charset=UTF-8' diff --git a/lib/user.dart b/lib/user.dart new file mode 100644 index 0000000..04d4c2c --- /dev/null +++ b/lib/user.dart @@ -0,0 +1,24 @@ +part of panoramax; + +const TAG_INSTANCE = "instance"; +const TAG_TOKEN = "token"; + +Future getInstance() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(TAG_INSTANCE); +} + +void setInstance(String instance) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString(TAG_INSTANCE, instance); +} + +Future getToken() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(TAG_TOKEN); +} + +void setToken(String token) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString(TAG_TOKEN, token); +} From 93b1d9f41234c5a423ae4783db31204b42f85713 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Mon, 8 Jul 2024 09:38:52 +0200 Subject: [PATCH 02/22] feat: display only own sequence --- lib/component/sequence_card.dart | 122 ++++++++++++++ lib/l10n/app_en.arb | 8 + lib/l10n/app_fr.arb | 8 + lib/main.dart | 2 + lib/page/homepage.dart | 21 ++- lib/page/instance_page.dart | 27 +-- lib/page/upload_pictures_page.dart | 158 ++++++++++++------ lib/service/api/api.dart | 1 - lib/service/api/endpoint/collections_api.dart | 43 ++++- lib/service/api/model/geo_visio.dart | 62 ++++++- lib/service/api/model/geo_visio.g.dart | 74 +++++++- 11 files changed, 449 insertions(+), 77 deletions(-) create mode 100644 lib/component/sequence_card.dart diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart new file mode 100644 index 0000000..75867cd --- /dev/null +++ b/lib/component/sequence_card.dart @@ -0,0 +1,122 @@ +part of panoramax; + +class SequenceCard extends StatelessWidget { + final GeoVisioLink sequence; + final GeoVisioCollectionImportStatus geovisioStatus; + const SequenceCard(this.sequence, this.geovisioStatus, {super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + height: 230, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(18), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + spreadRadius: 4, + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + geovisioStatus.status == "ready" + ? Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + image: DecorationImage( + image: Image.network(sequence.getThumbUrl()!).image, + fit: BoxFit.cover, + ))) + : Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + ), + child: Container( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + geovisioStatus.status == "preparing" + ? Icon( + Icons.check_circle_outline, + color: Colors.blue, + size: 60, + ) + : CircularProgressIndicator( + strokeWidth: 4, // thickness of the circle + color: Colors.blue, // color of the progress bar + ), + Text(geovisioStatus.status == "preparing" + ? AppLocalizations.of(context)!.sendingCompleted + : AppLocalizations.of(context)!.sendingInProgress) + ])))), + Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${geovisioStatus.items.length} ${AppLocalizations.of(context)!.element}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ), + /*Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${sequence.stats_items?.count} ${AppLocalizations.of(context)!.photo}', + style: GoogleFonts.nunito( + fontSize: 14, + color: Colors.grey[500], + fontWeight: FontWeight.w400, + ), + ) + ], + ), + ),*/ + Container( + margin: const EdgeInsets.fromLTRB(10, 3, 10, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + sequence.created != null + ? DateFormat(DATE_FORMATTER) + .format(DateTime.parse(sequence.created!)) + : "", + style: GoogleFonts.nunito( + fontSize: 14, + color: Colors.grey[500], + fontWeight: FontWeight.w400, + )) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index df51d4d..c657b65 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8,6 +8,14 @@ "emptyError": "No element found", "loading": "Loading...", + "mySequences": "My sequence", + "pictures": "pictures", + "shooting": "Shooting", + "sendingInProgress": "Sending in progress", + "sendingCompleted": "Sending completed", + "publishing": "Publishing", + "blurringInProgress": "Blurring in progress", + "capture": "Take a picture", "createSequenceWithPicture_tooltip": "Create a new sequence with captured pictures", "noCameraFoundError": "No camera found for this device", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7011383..e13d202 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -8,6 +8,14 @@ "emptyError": "Aucun élément trouvé", "loading": "Chargement...", + "mySequences": "Mes séquences", + "pictures": "photos", + "shooting": "Prise de vue", + "sendingInProgress": "Envoi en cours", + "sendingCompleted": "Envoi terminé", + "publishing": "Publication", + "blurringInProgress": "Floutage en cours", + "capture": "Prendre une photo", "createSequenceWithPicture_tooltip": "Créer une nouvelle séquence avec les photos prises", "noCameraFoundError": "Pas de caméra trouvée pour cet appareil", diff --git a/lib/main.dart b/lib/main.dart index 53974ae..4c155b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'dart:math'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:camera/camera.dart'; @@ -35,6 +36,7 @@ import 'constant.dart'; part 'component/app_bar.dart'; part 'component/collection_preview.dart'; +part 'component/sequence_card.dart'; part 'page/homepage.dart'; part 'page/capture_page.dart'; part 'page/collection_creation_page.dart'; diff --git a/lib/page/homepage.dart b/lib/page/homepage.dart index 800bde5..56963ab 100644 --- a/lib/page/homepage.dart +++ b/lib/page/homepage.dart @@ -24,7 +24,8 @@ class _HomePageState extends State { isLoading = true; }); try { - refreshedCollections = await CollectionsApi.INSTANCE.apiCollectionsGet(); + refreshedCollections = + await CollectionsApi.INSTANCE.apiCollectionsGetAll(); } catch (e) { print(e); } finally { @@ -39,9 +40,9 @@ class _HomePageState extends State { if (!await PermissionHelper.isPermissionGranted()) { await PermissionHelper.askMissingPermission(); } - await availableCameras().then( - (availableCameras) => GetIt.instance().pushTo(Routes.newSequenceCapture, arguments: availableCameras) - ); + await availableCameras().then((availableCameras) => + GetIt.instance() + .pushTo(Routes.newSequenceCapture, arguments: availableCameras)); } Widget displayBody(isLoading) { @@ -76,7 +77,8 @@ class _HomePageState extends State { child: Semantics( header: true, child: Text(AppLocalizations.of(context)!.yourSequence, - style: GoogleFonts.nunito(fontSize: 25, fontWeight: FontWeight.w400)), + style: GoogleFonts.nunito( + fontSize: 25, fontWeight: FontWeight.w400)), ), ), Expanded( @@ -106,7 +108,8 @@ class CollectionListView extends StatelessWidget { Widget build(BuildContext context) { return ListView.builder( itemCount: collections.length, - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + physics: + const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemBuilder: (BuildContext context, int index) { return CollectionPreview(collections[index]); }, @@ -147,7 +150,8 @@ class NoElementView extends StatelessWidget { Center( child: Text( AppLocalizations.of(context)!.emptyError, - style: GoogleFonts.nunito(fontSize: 18, color: Colors.grey, fontWeight: FontWeight.w400), + style: GoogleFonts.nunito( + fontSize: 18, color: Colors.grey, fontWeight: FontWeight.w400), ), ) ], @@ -168,7 +172,8 @@ class UnknownErrorView extends StatelessWidget { Center( child: Text( AppLocalizations.of(context)!.unknownError, - style: GoogleFonts.nunito(fontSize: 20, color: Colors.red, fontWeight: FontWeight.w400), + style: GoogleFonts.nunito( + fontSize: 20, color: Colors.red, fontWeight: FontWeight.w400), ), ) ], diff --git a/lib/page/instance_page.dart b/lib/page/instance_page.dart index c8f6275..64df6a3 100644 --- a/lib/page/instance_page.dart +++ b/lib/page/instance_page.dart @@ -25,9 +25,10 @@ class _InstanceState extends State { }); } - void getToken() async { + void getJWTToken() async { + final instance = await getInstance(); final cookies = - await cookieManager.getCookies('https://panoramax.${getInstance()}.fr'); + await cookieManager.getCookies('https://panoramax.$instance.fr'); var tokens = await AuthenticationApi.INSTANCE.apiTokensGet(cookies); var token = @@ -41,8 +42,9 @@ class _InstanceState extends State { void initState() { super.initState(); - getInstance().then((instance) { - if (instance != null) { + getInstance().then((instance) async { + final token = await getToken(); + if (instance != null && token != null) { GetIt.instance() .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); } @@ -60,12 +62,17 @@ class _InstanceState extends State { controller: WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( - onNavigationRequest: (request) { - if (request.url == - "https://panoramax.${getInstance()}.fr/") { - getToken(); - } - return NavigationDecision.navigate; + onNavigationRequest: (request) async { + bool shouldNavigate = true; + await getInstance().then((instance) { + if (request.url == "https://panoramax.$instance.fr/") { + getJWTToken(); + shouldNavigate = false; + } + }); + return shouldNavigate + ? NavigationDecision.navigate + : NavigationDecision.prevent; }, )) ..loadRequest(Uri.parse(url!))) diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index 9130034..ccd6e23 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -10,49 +10,82 @@ class UploadPicturesPage extends StatefulWidget { } class _UploadPicturesState extends State { - int _uploadedImagesCount = 0; - bool _isUploading = false; - String? _errorMessage; + late bool isLoading; + GeoVisioCatalog? sequences; + Map mapStatus = Map(); @override void initState() { super.initState(); + isLoading = true; uploadImages(); + getMyCollections(); } - Future uploadImages() async { + Future getMyCollections() async { + GeoVisioCatalog? refreshedSequences; setState(() { - _isUploading = true; - _errorMessage = null; + isLoading = true; }); - try { - final collectionId = await createCollection(); - await sendPictures(collectionId); - setState(() { - _uploadedImagesCount = widget.imgList.length; - _isUploading = false; - }); + refreshedSequences = await CollectionsApi.INSTANCE.getMeCatalog(); } catch (e) { + print(e); + } finally { setState(() { - _errorMessage = e.toString(); - _isUploading = false; + sequences = refreshedSequences; + getStatusOfSequences(); }); + } + } + + Future getStatusOfSequences() async { + Map mapRefresh = Map(); + setState(() { + isLoading = true; + }); + try { + List>? futures = sequences?.links + .where((sequence) => sequence.rel == "child") + .map((sequence) async { + var geovisioStatus = await CollectionsApi.INSTANCE + .getGeovisioStatus(collectionId: sequence.id!); + + mapRefresh[sequence] = geovisioStatus; + }).toList(); + await Future.wait(futures!.cast>()); + } catch (e) { + print(e); } finally { setState(() { - _isUploading = false; + mapStatus = mapRefresh; + isLoading = false; }); } } + Widget displayBodySequences(isLoading) { + if (isLoading) { + return const LoaderIndicatorView(); + } else if (sequences == null || sequences?.links == null) { + return const UnknownErrorView(); + } else if (sequences!.links.isNotEmpty) { + return SequencesListView(mapStatus: mapStatus); + } else { + return const NoElementView(); + } + } + + Future uploadImages() async { + final collectionId = await createCollection(); + await sendPictures(collectionId); + } + Future createCollection() async { try { final collectionName = DateFormat('y_M_d_H_m_s').format(DateTime.now()); final collection = await CollectionsApi.INSTANCE .apiCollectionsCreate(newCollectionName: collectionName); - if (collection == null) { - throw Exception(AppLocalizations.of(context)!.newCollectionFail); - } return collection.id; } catch (e) { rethrow; @@ -77,39 +110,62 @@ class _UploadPicturesState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: BLUE, - body: Center( - child: _isUploading - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - Text( - style: TextStyle(color: Colors.white), - AppLocalizations.of(context)!.newCollectionLoading( - _uploadedImagesCount / widget.imgList.length)), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_errorMessage != null) - Text(AppLocalizations.of(context)! - .newCollectionError(_errorMessage.toString())), - Text( - style: TextStyle(color: Colors.white), - AppLocalizations.of(context)!.newCollectionUploadSuccess), - ElevatedButton( - onPressed: () { - goToCapture(); - }, - child: - Text(AppLocalizations.of(context)!.newCollectionBack), - ), - ], + return RefreshIndicator( + displacement: 250, + strokeWidth: 3, + triggerMode: RefreshIndicatorTriggerMode.onEdge, + onRefresh: () async { + setState(() { + isLoading = true; + getMyCollections(); + }); + }, + child: Scaffold( + appBar: PanoramaxAppBar(context: context), + body: Column( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Semantics( + header: true, + child: Text(AppLocalizations.of(context)!.mySequences, + style: GoogleFonts.nunito( + fontSize: 25, fontWeight: FontWeight.w400)), + ), + ), + Expanded( + child: displayBodySequences(isLoading), ), - ), + ], + ), + )); + } +} + +class SequencesListView extends StatelessWidget { + const SequencesListView({ + super.key, + required this.mapStatus, + }); + + final Map mapStatus; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: mapStatus.length, + physics: + const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + itemBuilder: (BuildContext context, int index) { + GeoVisioLink key = mapStatus.keys.elementAt(index); + GeoVisioCollectionImportStatus value = + mapStatus.values.elementAt(index); + if (key.geovisio_status != "hidden") { + return SequenceCard(key, value); + } else { + return Container(); + } + }, ); } } diff --git a/lib/service/api/api.dart b/lib/service/api/api.dart index a161b58..14f76d4 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -3,7 +3,6 @@ library panoramax.api; import 'dart:async'; import 'package:http/http.dart' as http; import 'package:panoramax_mobile/main.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'model/geo_visio.dart'; import 'model/geo_visio_auth.dart'; diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index 10f29e0..e5cd442 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -7,7 +7,7 @@ class CollectionsApi { /// /// List available collections /// - Future apiCollectionsGet( + Future apiCollectionsGetAll( {int? limit, String? format, List? bbox, @@ -32,7 +32,7 @@ class CollectionsApi { } // create path and map variables - final instance = getInstance(); + final instance = await getInstance(); var url = Uri.https("panoramax.$instance.fr", '/api/collections', queryParams); @@ -95,4 +95,43 @@ class CollectionsApi { throw new Exception('${response.statusCode} - ${response.reasonPhrase}'); } } + + Future getMeCatalog() async { + final instance = await getInstance(); + var url = Uri.https("panoramax.$instance.fr", '/api/users/me/catalog'); + + final token = await getToken(); + + var response = await http.get(url, headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token' + }); + if (response.statusCode >= 200 && response.statusCode < 400) { + var geovisioCatalog = + GeoVisioCatalog.fromJson(json.decode(response.body)); + return geovisioCatalog; + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } + + Future getGeovisioStatus( + {required String collectionId}) async { + final instance = await getInstance(); + var url = Uri.https("panoramax.$instance.fr", + '/api/collections/${collectionId}/geovisio_status'); + + final token = await getToken(); + var response = await http.get(url, headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token' + }); + if (response.statusCode >= 200 && response.statusCode < 400) { + var geovisioStatus = + GeoVisioCollectionImportStatus.fromJson(json.decode(response.body)); + return geovisioStatus; + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } } diff --git a/lib/service/api/model/geo_visio.dart b/lib/service/api/model/geo_visio.dart index 5637e31..92a1dd5 100644 --- a/lib/service/api/model/geo_visio.dart +++ b/lib/service/api/model/geo_visio.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:json_annotation/json_annotation.dart'; part 'geo_visio.g.dart'; @@ -5,18 +7,28 @@ part 'geo_visio.g.dart'; @JsonSerializable() class GeoVisioLink { late String href; - late String rel; + late String? rel; String? type; String? title; String? method; Object? headers; Object? body; bool? merge; + String? created; + @JsonKey(name: "geovisio:status") + String? geovisio_status; + String? id; + @JsonKey(name: "stats:items") + StatsItems? stats_items; factory GeoVisioLink.fromJson(Map json) => _$GeoVisioLinkFromJson(json); Map toJson() => _$GeoVisioLinkToJson(this); + String? getThumbUrl() { + return '$href/thumb.jpg'; + } + GeoVisioLink(); } @@ -86,4 +98,50 @@ class GeoVisioCollections { Map toJson() => _$GeoVisioCollectionsToJson(this); GeoVisioCollections(); -} \ No newline at end of file +} + +@JsonSerializable() +class GeoVisioCatalog { + late String stac_version; + List? stac_extension; + final String type = "Collection"; + late String id; + String? title; + late String description; + late List links; + + factory GeoVisioCatalog.fromJson(Map json) => + _$GeoVisioCatalogFromJson(json); + Map toJson() => _$GeoVisioCatalogToJson(this); + + GeoVisioCatalog(); +} + +@JsonSerializable() +class GeoVisioCollectionImportStatus { + late String status; + late List items; + + factory GeoVisioCollectionImportStatus.fromJson(Map json) => + _$GeoVisioCollectionImportStatusFromJson(json); + Map toJson() => _$GeoVisioCollectionImportStatusToJson(this); + + GeoVisioCollectionImportStatus(); +} + +@JsonSerializable() +class StatusItem { + String? id; + String? status; + bool? processing_in_progress; + int? rank; + int? nb_errors; + String? process_error; + String? processed_at; + + factory StatusItem.fromJson(Map json) => + _$StatusItemFromJson(json); + Map toJson() => _$StatusItemToJson(this); + + StatusItem(); +} diff --git a/lib/service/api/model/geo_visio.g.dart b/lib/service/api/model/geo_visio.g.dart index 4d0f344..0e507e3 100644 --- a/lib/service/api/model/geo_visio.g.dart +++ b/lib/service/api/model/geo_visio.g.dart @@ -8,13 +8,19 @@ part of 'geo_visio.dart'; GeoVisioLink _$GeoVisioLinkFromJson(Map json) => GeoVisioLink() ..href = json['href'] as String - ..rel = json['rel'] as String + ..rel = json['rel'] as String? ..type = json['type'] as String? ..title = json['title'] as String? ..method = json['method'] as String? ..headers = json['headers'] ..body = json['body'] - ..merge = json['merge'] as bool?; + ..merge = json['merge'] as bool? + ..created = json['created'] as String? + ..geovisio_status = json['geovisio:status'] as String? + ..id = json['id'] as String? + ..stats_items = json['stats:items'] == null + ? null + : StatsItems.fromJson(json['stats:items'] as Map); Map _$GeoVisioLinkToJson(GeoVisioLink instance) => { @@ -26,6 +32,10 @@ Map _$GeoVisioLinkToJson(GeoVisioLink instance) => 'headers': instance.headers, 'body': instance.body, 'merge': instance.merge, + 'created': instance.created, + 'geovisio:status': instance.geovisio_status, + 'id': instance.id, + 'stats:items': instance.stats_items, }; GeoVisioProvider _$GeoVisioProviderFromJson(Map json) => @@ -45,7 +55,7 @@ Map _$GeoVisioProviderToJson(GeoVisioProvider instance) => }; StatsItems _$StatsItemsFromJson(Map json) => - StatsItems()..count = json['count'] as int; + StatsItems()..count = (json['count'] as num).toInt(); Map _$StatsItemsToJson(StatsItems instance) => { @@ -112,3 +122,61 @@ Map _$GeoVisioCollectionsToJson( 'links': instance.links, 'collections': instance.collections, }; + +GeoVisioCatalog _$GeoVisioCatalogFromJson(Map json) => + GeoVisioCatalog() + ..stac_version = json['stac_version'] as String + ..stac_extension = (json['stac_extension'] as List?) + ?.map((e) => e as String) + .toList() + ..id = json['id'] as String + ..title = json['title'] as String? + ..description = json['description'] as String + ..links = (json['links'] as List) + .map((e) => GeoVisioLink.fromJson(e as Map)) + .toList(); + +Map _$GeoVisioCatalogToJson(GeoVisioCatalog instance) => + { + 'stac_version': instance.stac_version, + 'stac_extension': instance.stac_extension, + 'id': instance.id, + 'title': instance.title, + 'description': instance.description, + 'links': instance.links, + }; + +GeoVisioCollectionImportStatus _$GeoVisioCollectionImportStatusFromJson( + Map json) => + GeoVisioCollectionImportStatus() + ..status = json['status'] as String + ..items = (json['items'] as List) + .map((e) => StatusItem.fromJson(e as Map)) + .toList(); + +Map _$GeoVisioCollectionImportStatusToJson( + GeoVisioCollectionImportStatus instance) => + { + 'status': instance.status, + 'items': instance.items, + }; + +StatusItem _$StatusItemFromJson(Map json) => StatusItem() + ..id = json['id'] as String? + ..status = json['status'] as String? + ..processing_in_progress = json['processing_in_progress'] as bool? + ..rank = (json['rank'] as num?)?.toInt() + ..nb_errors = (json['nb_errors'] as num?)?.toInt() + ..process_error = json['process_error'] as String? + ..processed_at = json['processed_at'] as String?; + +Map _$StatusItemToJson(StatusItem instance) => + { + 'id': instance.id, + 'status': instance.status, + 'processing_in_progress': instance.processing_in_progress, + 'rank': instance.rank, + 'nb_errors': instance.nb_errors, + 'process_error': instance.process_error, + 'processed_at': instance.processed_at, + }; From eed584a1b7709f48b6988708e5338bd9cd340392 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 21 Jul 2024 07:17:22 +0200 Subject: [PATCH 03/22] display publishing and shooting date --- lib/component/sequence_card.dart | 132 ++++++++++-------- lib/l10n/app_en.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/page/upload_pictures_page.dart | 33 ++--- lib/service/api/endpoint/collections_api.dart | 37 +++++ lib/service/api/model/geo_visio.dart | 23 +++ lib/service/api/model/geo_visio.g.dart | 28 +++- 7 files changed, 181 insertions(+), 76 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index 75867cd..7c06661 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -1,15 +1,33 @@ part of panoramax; -class SequenceCard extends StatelessWidget { +class SequenceCard extends StatefulWidget { + const SequenceCard(this.sequence, {super.key}); + final GeoVisioLink sequence; - final GeoVisioCollectionImportStatus geovisioStatus; - const SequenceCard(this.sequence, this.geovisioStatus, {super.key}); + + @override + State createState() => _SequenceCardState(); +} + +class _SequenceCardState extends State { + late int itemCount; + GeoVisioCollectionImportStatus? geovisioStatus; + + @override + void initState() { + super.initState(); + itemCount = widget.sequence.stats_items!.count; + if (widget.sequence.geovisio_status != "preparing") { + getStatus(); + } + } + + Future getStatus() async {} @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.all(10), - height: 230, width: double.infinity, decoration: BoxDecoration( color: Colors.white, @@ -27,7 +45,7 @@ class SequenceCard extends StatelessWidget { ), child: Column( children: [ - geovisioStatus.status == "ready" + widget.sequence.geovisio_status == "ready" ? Container( height: 140, decoration: BoxDecoration( @@ -36,7 +54,8 @@ class SequenceCard extends StatelessWidget { topRight: Radius.circular(18), ), image: DecorationImage( - image: Image.network(sequence.getThumbUrl()!).image, + image: + Image.network(widget.sequence.getThumbUrl()!).image, fit: BoxFit.cover, ))) : Container( @@ -52,7 +71,7 @@ class SequenceCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - geovisioStatus.status == "preparing" + widget.sequence.geovisio_status == "preparing" ? Icon( Icons.check_circle_outline, color: Colors.blue, @@ -62,61 +81,64 @@ class SequenceCard extends StatelessWidget { strokeWidth: 4, // thickness of the circle color: Colors.blue, // color of the progress bar ), - Text(geovisioStatus.status == "preparing" + Text(widget.sequence.geovisio_status == "preparing" ? AppLocalizations.of(context)!.sendingCompleted : AppLocalizations.of(context)!.sendingInProgress) ])))), Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${geovisioStatus.items.length} ${AppLocalizations.of(context)!.element}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ], - ), - ), - /*Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${sequence.stats_items?.count} ${AppLocalizations.of(context)!.photo}', - style: GoogleFonts.nunito( - fontSize: 14, - color: Colors.grey[500], - fontWeight: FontWeight.w400, + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ], ), - ) - ], - ), - ),*/ - Container( - margin: const EdgeInsets.fromLTRB(10, 3, 10, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - sequence.created != null - ? DateFormat(DATE_FORMATTER) - .format(DateTime.parse(sequence.created!)) - : "", - style: GoogleFonts.nunito( - fontSize: 14, - color: Colors.grey[500], - fontWeight: FontWeight.w400, - )) - ], - ), - ), + Shooting(), + widget.sequence.geovisio_status == "ready" + ? Publishing() + : Container() + ], + )), ], ), ); } + + Widget Shooting() { + String? date = widget.sequence.extent?.temporal?.interval?[0]?[0]; + DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); + return Row( + children: [ + const Icon(Icons.photo_camera), + Padding( + padding: EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)!.shooting)), + Spacer(), + Text(date == null ? "" : dateFormat.format(DateTime.parse(date))) + ], + ); + } + + Widget Publishing() { + DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); + String? date = widget.sequence.created; + return Row( + children: [ + const Icon(Icons.cloud_upload), + Padding( + padding: EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)!.publishing)), + Spacer(), + Text(date == null ? "" : dateFormat.format(DateTime.parse(date))) + ], + ); + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c657b65..c4904a1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -9,7 +9,7 @@ "loading": "Loading...", "mySequences": "My sequence", - "pictures": "pictures", + "pictures": "picture(s)", "shooting": "Shooting", "sendingInProgress": "Sending in progress", "sendingCompleted": "Sending completed", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e13d202..8c0fe5a 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -9,7 +9,7 @@ "loading": "Chargement...", "mySequences": "Mes séquences", - "pictures": "photos", + "pictures": "photo(s)", "shooting": "Prise de vue", "sendingInProgress": "Envoi en cours", "sendingCompleted": "Envoi terminé", diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index ccd6e23..cec18c1 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -11,35 +11,35 @@ class UploadPicturesPage extends StatefulWidget { class _UploadPicturesState extends State { late bool isLoading; - GeoVisioCatalog? sequences; - Map mapStatus = Map(); + GeoVisioCollection? sequences; @override void initState() { super.initState(); isLoading = true; - uploadImages(); + //uploadImages(); getMyCollections(); } Future getMyCollections() async { - GeoVisioCatalog? refreshedSequences; + GeoVisioCollection? refreshedSequences; setState(() { isLoading = true; }); try { - refreshedSequences = await CollectionsApi.INSTANCE.getMeCatalog(); + refreshedSequences = await CollectionsApi.INSTANCE.getMeCollection(); } catch (e) { print(e); } finally { setState(() { sequences = refreshedSequences; - getStatusOfSequences(); + isLoading = false; + //getStatusOfSequences(); }); } } - Future getStatusOfSequences() async { + /*Future getStatusOfSequences() async { Map mapRefresh = Map(); setState(() { isLoading = true; @@ -62,7 +62,7 @@ class _UploadPicturesState extends State { isLoading = false; }); } - } + }*/ Widget displayBodySequences(isLoading) { if (isLoading) { @@ -70,7 +70,7 @@ class _UploadPicturesState extends State { } else if (sequences == null || sequences?.links == null) { return const UnknownErrorView(); } else if (sequences!.links.isNotEmpty) { - return SequencesListView(mapStatus: mapStatus); + return SequencesListView(links: sequences!.links); } else { return const NoElementView(); } @@ -116,7 +116,6 @@ class _UploadPicturesState extends State { triggerMode: RefreshIndicatorTriggerMode.onEdge, onRefresh: () async { setState(() { - isLoading = true; getMyCollections(); }); }, @@ -145,23 +144,21 @@ class _UploadPicturesState extends State { class SequencesListView extends StatelessWidget { const SequencesListView({ super.key, - required this.mapStatus, + required this.links, }); - final Map mapStatus; + final List links; @override Widget build(BuildContext context) { return ListView.builder( - itemCount: mapStatus.length, + itemCount: links.length, physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemBuilder: (BuildContext context, int index) { - GeoVisioLink key = mapStatus.keys.elementAt(index); - GeoVisioCollectionImportStatus value = - mapStatus.values.elementAt(index); - if (key.geovisio_status != "hidden") { - return SequenceCard(key, value); + if (links[index].rel == "child" && + links[index].geovisio_status != "hidden") { + return SequenceCard(links[index]); } else { return Container(); } diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index e5cd442..4228dfe 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -96,6 +96,43 @@ class CollectionsApi { } } + Future getMeCollection( + {int? limit, List? bbox, String? filter, String? sortby}) async { + // query params + Map queryParams = {}; + if (limit != null) { + queryParams.putIfAbsent("limit", limit as String Function()); + } + if (sortby != null) { + queryParams.putIfAbsent("sortby", sortby as String Function()); + } + if (bbox != null) { + queryParams.putIfAbsent("bbox", bbox as String Function()); + } + if (filter != null) { + queryParams.putIfAbsent("filter", filter as String Function()); + } + + final token = await getToken(); + + final instance = await getInstance(); + var url = Uri.https( + "panoramax.$instance.fr", '/api/users/me/collection', queryParams); + + var response = await http.get(url, headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token' + }); + + if (response.statusCode >= 200) { + var geovisioCollection = + GeoVisioCollection.fromJson(json.decode(response.body)); + return geovisioCollection; + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } + Future getMeCatalog() async { final instance = await getInstance(); var url = Uri.https("panoramax.$instance.fr", '/api/users/me/catalog'); diff --git a/lib/service/api/model/geo_visio.dart b/lib/service/api/model/geo_visio.dart index 92a1dd5..e6f26f6 100644 --- a/lib/service/api/model/geo_visio.dart +++ b/lib/service/api/model/geo_visio.dart @@ -20,6 +20,7 @@ class GeoVisioLink { String? id; @JsonKey(name: "stats:items") StatsItems? stats_items; + GeoVisioExtent? extent; factory GeoVisioLink.fromJson(Map json) => _$GeoVisioLinkFromJson(json); @@ -32,6 +33,28 @@ class GeoVisioLink { GeoVisioLink(); } +@JsonSerializable() +class GeoVisioExtent { + Object? spatial; + Temporal? temporal; + + factory GeoVisioExtent.fromJson(Map json) => + _$GeoVisioExtentFromJson(json); + Map toJson() => _$GeoVisioExtentToJson(this); + + GeoVisioExtent(); +} + +@JsonSerializable() +class Temporal { + List?>? interval; + factory Temporal.fromJson(Map json) => + _$TemporalFromJson(json); + Map toJson() => _$TemporalToJson(this); + + Temporal(); +} + @JsonSerializable() class GeoVisioProvider { late String name; diff --git a/lib/service/api/model/geo_visio.g.dart b/lib/service/api/model/geo_visio.g.dart index 0e507e3..10056bd 100644 --- a/lib/service/api/model/geo_visio.g.dart +++ b/lib/service/api/model/geo_visio.g.dart @@ -20,7 +20,10 @@ GeoVisioLink _$GeoVisioLinkFromJson(Map json) => GeoVisioLink() ..id = json['id'] as String? ..stats_items = json['stats:items'] == null ? null - : StatsItems.fromJson(json['stats:items'] as Map); + : StatsItems.fromJson(json['stats:items'] as Map) + ..extent = json['extent'] == null + ? null + : GeoVisioExtent.fromJson(json['extent'] as Map); Map _$GeoVisioLinkToJson(GeoVisioLink instance) => { @@ -36,6 +39,29 @@ Map _$GeoVisioLinkToJson(GeoVisioLink instance) => 'geovisio:status': instance.geovisio_status, 'id': instance.id, 'stats:items': instance.stats_items, + 'extent': instance.extent, + }; + +GeoVisioExtent _$GeoVisioExtentFromJson(Map json) => + GeoVisioExtent() + ..spatial = json['spatial'] + ..temporal = json['temporal'] == null + ? null + : Temporal.fromJson(json['temporal'] as Map); + +Map _$GeoVisioExtentToJson(GeoVisioExtent instance) => + { + 'spatial': instance.spatial, + 'temporal': instance.temporal, + }; + +Temporal _$TemporalFromJson(Map json) => Temporal() + ..interval = (json['interval'] as List?) + ?.map((e) => (e as List?)?.map((e) => e as String?).toList()) + .toList(); + +Map _$TemporalToJson(Temporal instance) => { + 'interval': instance.interval, }; GeoVisioProvider _$GeoVisioProviderFromJson(Map json) => From 4650c3e2ff0fccd7b7821da720a26648ec8cf02a Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 21 Jul 2024 07:31:36 +0200 Subject: [PATCH 04/22] refactor: split the code of sequence card into smaller components --- lib/component/sequence_card.dart | 133 +++++++++++++++++-------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index 7c06661..e02d8b8 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -45,73 +45,38 @@ class _SequenceCardState extends State { ), child: Column( children: [ - widget.sequence.geovisio_status == "ready" - ? Container( - height: 140, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), - ), - image: DecorationImage( - image: - Image.network(widget.sequence.getThumbUrl()!).image, - fit: BoxFit.cover, - ))) - : Container( - height: 140, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), - ), - ), - child: Container( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.sequence.geovisio_status == "preparing" - ? Icon( - Icons.check_circle_outline, - color: Colors.blue, - size: 60, - ) - : CircularProgressIndicator( - strokeWidth: 4, // thickness of the circle - color: Colors.blue, // color of the progress bar - ), - Text(widget.sequence.geovisio_status == "preparing" - ? AppLocalizations.of(context)!.sendingCompleted - : AppLocalizations.of(context)!.sendingInProgress) - ])))), - Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$itemCount ${AppLocalizations.of(context)!.pictures}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ], - ), - Shooting(), - widget.sequence.geovisio_status == "ready" - ? Publishing() - : Container() - ], - )), + widget.sequence.geovisio_status == "ready" ? Picture() : Loader(), + PictureDetail(), ], ), ); } + Widget PictureDetail() { + return Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + Shooting(), + widget.sequence.geovisio_status == "ready" + ? Publishing() + : Container() + ], + )); + } + Widget Shooting() { String? date = widget.sequence.extent?.temporal?.interval?[0]?[0]; DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); @@ -141,4 +106,48 @@ class _SequenceCardState extends State { ], ); } + + Widget Loader() { + return Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + ), + child: Container( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.sequence.geovisio_status == "preparing" + ? Icon( + Icons.check_circle_outline, + color: Colors.blue, + size: 60, + ) + : CircularProgressIndicator( + strokeWidth: 4, // thickness of the circle + color: Colors.blue, // color of the progress bar + ), + Text(widget.sequence.geovisio_status == "preparing" + ? AppLocalizations.of(context)!.sendingCompleted + : AppLocalizations.of(context)!.sendingInProgress) + ])))); + } + + Widget Picture() { + return Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + image: DecorationImage( + image: Image.network(widget.sequence.getThumbUrl()!).image, + fit: BoxFit.cover, + ))); + } } From c53c1626b8ef6e212700e9ff8d1783aef46961d2 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 21 Jul 2024 09:26:58 +0200 Subject: [PATCH 05/22] feat: add share button on sequence card --- lib/component/sequence_card.dart | 49 ++++++++++++++++++++++++-------- lib/l10n/app_en.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/main.dart | 1 + pubspec.yaml | 1 + 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index e02d8b8..e1f6550 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -24,6 +24,15 @@ class _SequenceCardState extends State { Future getStatus() async {} + Future openUrl() async { + final instance = await getInstance(); + final Uri url = + Uri.https("panoramax.$instance.fr", '/sequence/${widget.sequence.id}'); + if (!await launchUrl(url)) { + throw Exception("Could not launch $url"); + } + } + @override Widget build(BuildContext context) { return Container( @@ -57,18 +66,7 @@ class _SequenceCardState extends State { margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), child: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$itemCount ${AppLocalizations.of(context)!.pictures}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ], - ), + PictureCount(), Shooting(), widget.sequence.geovisio_status == "ready" ? Publishing() @@ -77,6 +75,33 @@ class _SequenceCardState extends State { )); } + Widget PictureCount() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + widget.sequence.geovisio_status == "ready" + ? FloatingActionButton( + onPressed: openUrl, + child: Icon( + Icons.share, + //size: 14, + ), + shape: CircleBorder(), + mini: true, + backgroundColor: Colors.grey, + tooltip: AppLocalizations.of(context)!.share) + : Container(), + ], + ); + } + Widget Shooting() { String? date = widget.sequence.extent?.temporal?.interval?[0]?[0]; DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c4904a1..87654cc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -15,6 +15,7 @@ "sendingCompleted": "Sending completed", "publishing": "Publishing", "blurringInProgress": "Blurring in progress", + "share": "Share", "capture": "Take a picture", "createSequenceWithPicture_tooltip": "Create a new sequence with captured pictures", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8c0fe5a..9236e88 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -15,6 +15,7 @@ "sendingCompleted": "Envoi terminé", "publishing": "Publication", "blurringInProgress": "Floutage en cours", + "share": "Partager", "capture": "Prendre une photo", "createSequenceWithPicture_tooltip": "Créer une nouvelle séquence avec les photos prises", diff --git a/lib/main.dart b/lib/main.dart index 4c155b8..c9e6588 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,6 +30,7 @@ import 'package:native_exif/native_exif.dart'; import 'package:app_settings/app_settings.dart'; import 'package:flutter_exif_plugin/flutter_exif_plugin.dart'; import 'package:sensors/sensors.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'component/loader.dart'; import 'service/api/api.dart'; import 'constant.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 79d44f8..7fe1fb5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: app_settings: ^5.1.1 flutter_exif_plugin: ^1.1.0 sensors: ^2.0.3 + url_launcher: ^6.3.0 dev_dependencies: flutter_test: From 1208af2d319d42f7df631f1184c35a44a27ef667 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 25 Jul 2024 11:42:34 +0200 Subject: [PATCH 06/22] feat: automatically updates the upload information of sequences --- lib/component/sequence_card.dart | 83 ++++++++++++++++++++++++++---- lib/page/upload_pictures_page.dart | 68 +++++++++++------------- 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index e1f6550..b4133a4 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -1,9 +1,11 @@ part of panoramax; class SequenceCard extends StatefulWidget { - const SequenceCard(this.sequence, {super.key}); + const SequenceCard(this.sequence, {this.sequenceCount, super.key}); final GeoVisioLink sequence; + final int? + sequenceCount; //if sequenceCount is not null, there are photos being uploaded @override State createState() => _SequenceCardState(); @@ -12,17 +14,39 @@ class SequenceCard extends StatefulWidget { class _SequenceCardState extends State { late int itemCount; GeoVisioCollectionImportStatus? geovisioStatus; + Timer? timer; @override void initState() { super.initState(); itemCount = widget.sequence.stats_items!.count; - if (widget.sequence.geovisio_status != "preparing") { - getStatus(); + if (widget.sequence.geovisio_status != "preparing" || + widget.sequenceCount != null) { + timer = Timer.periodic(Duration(seconds: 1), (timer) { + getStatus(); + }); } } - Future getStatus() async {} + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + Future getStatus() async { + GeoVisioCollectionImportStatus? geovisioStatusRefresh; + try { + geovisioStatusRefresh = await CollectionsApi.INSTANCE + .getGeovisioStatus(collectionId: widget.sequence.id!); + } catch (e) { + print(e); + } finally { + setState(() { + geovisioStatus = geovisioStatusRefresh; + }); + } + } Future openUrl() async { final instance = await getInstance(); @@ -54,7 +78,10 @@ class _SequenceCardState extends State { ), child: Column( children: [ - widget.sequence.geovisio_status == "ready" ? Picture() : Loader(), + widget.sequence.geovisio_status == "ready" && + widget.sequenceCount == null + ? Picture() + : Loader(), PictureDetail(), ], ), @@ -70,23 +97,30 @@ class _SequenceCardState extends State { Shooting(), widget.sequence.geovisio_status == "ready" ? Publishing() + : Container(), + widget.sequenceCount == null || + widget.sequence.stats_items?.count == widget.sequenceCount + ? Blurring() : Container() ], )); } Widget PictureCount() { + final count = + widget.sequenceCount == null ? itemCount : widget.sequenceCount; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '$itemCount ${AppLocalizations.of(context)!.pictures}', + '$count ${AppLocalizations.of(context)!.pictures}', style: GoogleFonts.nunito( fontSize: 18, fontWeight: FontWeight.w800, ), ), - widget.sequence.geovisio_status == "ready" + widget.sequence.geovisio_status == "ready" && + widget.sequenceCount == null ? FloatingActionButton( onPressed: openUrl, child: Icon( @@ -146,7 +180,8 @@ class _SequenceCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - widget.sequence.geovisio_status == "preparing" + widget.sequenceCount == null || + geovisioStatus?.items.length == widget.sequenceCount ? Icon( Icons.check_circle_outline, color: Colors.blue, @@ -156,7 +191,8 @@ class _SequenceCardState extends State { strokeWidth: 4, // thickness of the circle color: Colors.blue, // color of the progress bar ), - Text(widget.sequence.geovisio_status == "preparing" + Text(widget.sequenceCount == null || + geovisioStatus?.items.length == widget.sequenceCount ? AppLocalizations.of(context)!.sendingCompleted : AppLocalizations.of(context)!.sendingInProgress) ])))); @@ -175,4 +211,33 @@ class _SequenceCardState extends State { fit: BoxFit.cover, ))); } + + Widget Blurring() { + int count = geovisioStatus?.items + .where( + (element) => element.status == "ready", + ) + .length ?? + 0; + double total = geovisioStatus?.items.length.toDouble() ?? 0; + return Container( + child: Column( + children: [ + LinearProgressIndicator( + value: (total == 0) ? 0 : count / total, //don't divide by 0 ! + semanticsLabel: AppLocalizations.of(context)!.blurringInProgress, + minHeight: 16, + borderRadius: BorderRadius.circular(8), + ), + Row( + children: [ + const Icon(Icons.blur_on_outlined), + Padding( + padding: EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)!.blurringInProgress)) + ], + ) + ], + )); + } } diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index cec18c1..ce85b4e 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -12,12 +12,15 @@ class UploadPicturesPage extends StatefulWidget { class _UploadPicturesState extends State { late bool isLoading; GeoVisioCollection? sequences; + late int sequenceCount; + String? collectionId; @override void initState() { super.initState(); isLoading = true; - //uploadImages(); + sequenceCount = widget.imgList.length; + uploadImages(); getMyCollections(); } @@ -34,68 +37,50 @@ class _UploadPicturesState extends State { setState(() { sequences = refreshedSequences; isLoading = false; - //getStatusOfSequences(); }); } } - /*Future getStatusOfSequences() async { - Map mapRefresh = Map(); - setState(() { - isLoading = true; - }); - try { - List>? futures = sequences?.links - .where((sequence) => sequence.rel == "child") - .map((sequence) async { - var geovisioStatus = await CollectionsApi.INSTANCE - .getGeovisioStatus(collectionId: sequence.id!); - - mapRefresh[sequence] = geovisioStatus; - }).toList(); - await Future.wait(futures!.cast>()); - } catch (e) { - print(e); - } finally { - setState(() { - mapStatus = mapRefresh; - isLoading = false; - }); - } - }*/ - Widget displayBodySequences(isLoading) { if (isLoading) { return const LoaderIndicatorView(); } else if (sequences == null || sequences?.links == null) { return const UnknownErrorView(); } else if (sequences!.links.isNotEmpty) { - return SequencesListView(links: sequences!.links); + return SequencesListView( + links: sequences!.links, + collectionId: collectionId, + lastSequenceCount: sequenceCount); } else { return const NoElementView(); } } Future uploadImages() async { - final collectionId = await createCollection(); - await sendPictures(collectionId); + await createCollection(); + await sendPictures(); } - Future createCollection() async { + Future createCollection() async { try { final collectionName = DateFormat('y_M_d_H_m_s').format(DateTime.now()); final collection = await CollectionsApi.INSTANCE .apiCollectionsCreate(newCollectionName: collectionName); - return collection.id; + setState(() { + collectionId = collection.id; + }); } catch (e) { rethrow; } } - Future sendPictures(String collectionId) async { + Future sendPictures() async { + if (collectionId == null) { + return; + } for (var i = 0; i < widget.imgList.length; i++) { await CollectionsApi.INSTANCE.apiCollectionsUploadPicture( - collectionId: collectionId, + collectionId: collectionId!, position: i + 1, pictureToUpload: widget.imgList[i], ); @@ -142,12 +127,15 @@ class _UploadPicturesState extends State { } class SequencesListView extends StatelessWidget { - const SequencesListView({ - super.key, - required this.links, - }); + const SequencesListView( + {super.key, + required this.links, + required this.collectionId, + required this.lastSequenceCount}); final List links; + final String? collectionId; + final int lastSequenceCount; @override Widget build(BuildContext context) { @@ -158,7 +146,9 @@ class SequencesListView extends StatelessWidget { itemBuilder: (BuildContext context, int index) { if (links[index].rel == "child" && links[index].geovisio_status != "hidden") { - return SequenceCard(links[index]); + return SequenceCard(links[index], + sequenceCount: + links[index].id == collectionId ? lastSequenceCount : null); } else { return Container(); } From 95a659e0c1935eae59dffafd800c5991f43f9c47 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 28 Jul 2024 21:46:19 +0200 Subject: [PATCH 07/22] fix: fix the status check of the last sequence --- lib/component/sequence_card.dart | 110 +++++++++++++++++++------------ 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index b4133a4..807e8a8 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -15,19 +15,48 @@ class _SequenceCardState extends State { late int itemCount; GeoVisioCollectionImportStatus? geovisioStatus; Timer? timer; + SequenceState sequenceState = SequenceState.SENDING; @override void initState() { super.initState(); itemCount = widget.sequence.stats_items!.count; - if (widget.sequence.geovisio_status != "preparing" || - widget.sequenceCount != null) { - timer = Timer.periodic(Duration(seconds: 1), (timer) { + checkSequenceState(); + if (sequenceState != SequenceState.READY || widget.sequenceCount != null) { + timer = Timer.periodic(Duration(seconds: 5), (timer) { getStatus(); }); } } + void checkSequenceState() { + int count = geovisioStatus?.items + .where( + (element) => element.status == "ready", + ) + .length ?? + 0; + print("checkSequenceStat"); + setState(() { + sequenceState = geovisioStatus?.status == "deleted" + ? SequenceState.DELETED + : widget.sequence.geovisio_status == "hidden" + ? SequenceState.HIDDEN + : widget.sequenceCount == null + ? widget.sequence.geovisio_status == "ready" + ? SequenceState.READY + : SequenceState.BLURRING + : widget.sequenceCount != geovisioStatus?.items.length + ? SequenceState.SENDING + : count == widget.sequenceCount + ? SequenceState.READY + : SequenceState.BLURRING; + }); + if (sequenceState == SequenceState.DELETED) { + timer?.cancel(); + } + } + @override void dispose() { timer?.cancel(); @@ -44,6 +73,7 @@ class _SequenceCardState extends State { } finally { setState(() { geovisioStatus = geovisioStatusRefresh; + checkSequenceState(); }); } } @@ -59,33 +89,33 @@ class _SequenceCardState extends State { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.all(10), - width: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all( - Radius.circular(18), - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade200, - spreadRadius: 4, - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: Column( - children: [ - widget.sequence.geovisio_status == "ready" && - widget.sequenceCount == null - ? Picture() - : Loader(), - PictureDetail(), - ], - ), - ); + return sequenceState == SequenceState.DELETED || + sequenceState == SequenceState.HIDDEN + ? Container() + : Container( + margin: const EdgeInsets.all(10), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(18), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + spreadRadius: 4, + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + sequenceState == SequenceState.READY ? Picture() : Loader(), + PictureDetail(), + ], + ), + ); } Widget PictureDetail() { @@ -95,13 +125,8 @@ class _SequenceCardState extends State { children: [ PictureCount(), Shooting(), - widget.sequence.geovisio_status == "ready" - ? Publishing() - : Container(), - widget.sequenceCount == null || - widget.sequence.stats_items?.count == widget.sequenceCount - ? Blurring() - : Container() + sequenceState == SequenceState.READY ? Publishing() : Container(), + sequenceState == SequenceState.BLURRING ? Blurring() : Container() ], )); } @@ -119,8 +144,7 @@ class _SequenceCardState extends State { fontWeight: FontWeight.w800, ), ), - widget.sequence.geovisio_status == "ready" && - widget.sequenceCount == null + sequenceState == SequenceState.READY ? FloatingActionButton( onPressed: openUrl, child: Icon( @@ -180,8 +204,7 @@ class _SequenceCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - widget.sequenceCount == null || - geovisioStatus?.items.length == widget.sequenceCount + sequenceState == SequenceState.BLURRING ? Icon( Icons.check_circle_outline, color: Colors.blue, @@ -191,8 +214,7 @@ class _SequenceCardState extends State { strokeWidth: 4, // thickness of the circle color: Colors.blue, // color of the progress bar ), - Text(widget.sequenceCount == null || - geovisioStatus?.items.length == widget.sequenceCount + Text(sequenceState == SequenceState.BLURRING ? AppLocalizations.of(context)!.sendingCompleted : AppLocalizations.of(context)!.sendingInProgress) ])))); @@ -241,3 +263,5 @@ class _SequenceCardState extends State { )); } } + +enum SequenceState { SENDING, BLURRING, READY, DELETED, HIDDEN } From fa1a947267f6af0d14330f431e59f4c5ada96336 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Tue, 25 Jun 2024 14:42:03 +0200 Subject: [PATCH 08/22] feat: remember selected instance --- lib/constant.dart | 1 - lib/main.dart | 1 + lib/page/instance_page.dart | 22 ++++++++++------- lib/service/api/api.dart | 3 +-- .../api/endpoint/authentication_api.dart | 14 ++++++----- lib/service/api/endpoint/collections_api.dart | 17 ++++++------- lib/user.dart | 24 +++++++++++++++++++ 7 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 lib/user.dart diff --git a/lib/constant.dart b/lib/constant.dart index 2cdee5f..8c72c02 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -5,4 +5,3 @@ import 'package:flutter/material.dart'; const MaterialColor DEFAULT_COLOR = Colors.indigo; const bool API_IS_HTTPS = false; const Color BLUE = Color(0xFF010D37); -String API_HOSTNAME = 'openstreetmap'; diff --git a/lib/main.dart b/lib/main.dart index a5e4207..ee98914 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,6 +38,7 @@ part 'page/upload_pictures_page.dart'; part 'service/routing.dart'; part 'service/permission_helper.dart'; part 'utils/gravity_orientation_detector.dart'; +part 'user.dart'; const String DATE_FORMATTER = 'dd/MM - HH:mm'; diff --git a/lib/page/instance_page.dart b/lib/page/instance_page.dart index a4bb10f..c8f6275 100644 --- a/lib/page/instance_page.dart +++ b/lib/page/instance_page.dart @@ -17,11 +17,9 @@ class _InstanceState extends State { bool isInstanceChosen = false; final cookieManager = WebviewCookieManager(); - int _selectedIndex = -1; - void authentication(String instance) { setState(() { - API_HOSTNAME = instance; + setInstance(instance); url = "https://panoramax.${instance}.fr/api/auth/login"; isInstanceChosen = true; }); @@ -29,20 +27,28 @@ class _InstanceState extends State { void getToken() async { final cookies = - await cookieManager.getCookies('https://panoramax.$API_HOSTNAME.fr'); + await cookieManager.getCookies('https://panoramax.${getInstance()}.fr'); var tokens = await AuthenticationApi.INSTANCE.apiTokensGet(cookies); var token = await AuthenticationApi.INSTANCE.apiTokenGet(tokens.id, cookies); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - print(token.jwt_token); - prefs.setString('token', token.jwt_token); + setToken(token.jwt_token); GetIt.instance() .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); } + void initState() { + super.initState(); + getInstance().then((instance) { + if (instance != null) { + GetIt.instance() + .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -56,7 +62,7 @@ class _InstanceState extends State { ..setNavigationDelegate(NavigationDelegate( onNavigationRequest: (request) { if (request.url == - "https://panoramax.$API_HOSTNAME.fr/") { + "https://panoramax.${getInstance()}.fr/") { getToken(); } return NavigationDecision.navigate; diff --git a/lib/service/api/api.dart b/lib/service/api/api.dart index 441c79c..a161b58 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -2,13 +2,12 @@ library panoramax.api; import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:panoramax_mobile/main.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'model/geo_visio.dart'; import 'model/geo_visio_auth.dart'; import 'dart:io'; -import '../../constant.dart'; - part 'endpoint/collections_api.dart'; part 'endpoint/authentication_api.dart'; diff --git a/lib/service/api/endpoint/authentication_api.dart b/lib/service/api/endpoint/authentication_api.dart index 89faf49..a49548e 100644 --- a/lib/service/api/endpoint/authentication_api.dart +++ b/lib/service/api/endpoint/authentication_api.dart @@ -4,7 +4,8 @@ class AuthenticationApi { static final AuthenticationApi INSTANCE = new AuthenticationApi(); Future apiTokensGet(List cookies) async { - final url = Uri.https("panoramax.$API_HOSTNAME.fr", '/api/users/me/tokens'); + final instance = await getInstance(); + final url = Uri.https("panoramax.$instance.fr", '/api/users/me/tokens'); var session = null; for (var cookie in cookies) { @@ -24,10 +25,12 @@ class AuthenticationApi { } } - Future apiTokenGet(String tokenId, List cookies) async { + Future apiTokenGet( + String tokenId, List cookies) async { // create path and map variables - var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/users/me/tokens/${tokenId}'); + final instance = await getInstance(); + var url = + Uri.https("panoramax.$instance.fr", '/api/users/me/tokens/${tokenId}'); var session = null; for (var cookie in cookies) { @@ -38,7 +41,7 @@ class AuthenticationApi { final response = await http.get(url, headers: {'cookie': session}); - if (response.statusCode >= 200) { + if (response.statusCode >= 200 && response.statusCode < 400) { var geoVisioJWTToken = GeoVisioJWTToken.fromJson(json.decode(response.body)); return geoVisioJWTToken; @@ -47,4 +50,3 @@ class AuthenticationApi { } } } - diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index 752f776..10f29e0 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -32,8 +32,9 @@ class CollectionsApi { } // create path and map variables - var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/collections', queryParams); + final instance = getInstance(); + var url = + Uri.https("panoramax.$instance.fr", '/api/collections', queryParams); var response = await http.get(url); if (response.statusCode >= 200) { @@ -49,10 +50,10 @@ class CollectionsApi { Future apiCollectionsCreate( {required String newCollectionName}) async { - var url = Uri.https("panoramax.$API_HOSTNAME.fr", '/api/collections'); + final instance = await getInstance(); + var url = Uri.https("panoramax.$instance.fr", '/api/collections'); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final token = prefs.getString('token'); + final token = await getToken(); var response = await http.post( url, @@ -75,11 +76,11 @@ class CollectionsApi { {required String collectionId, required int position, required File pictureToUpload}) async { + final instance = await getInstance(); var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/collections/${collectionId}/items'); + "panoramax.$instance.fr", '/api/collections/${collectionId}/items'); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final token = prefs.getString('token'); + final token = await getToken(); var request = http.MultipartRequest('POST', url) ..headers['Content-Type'] = 'application/json; charset=UTF-8' diff --git a/lib/user.dart b/lib/user.dart new file mode 100644 index 0000000..04d4c2c --- /dev/null +++ b/lib/user.dart @@ -0,0 +1,24 @@ +part of panoramax; + +const TAG_INSTANCE = "instance"; +const TAG_TOKEN = "token"; + +Future getInstance() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(TAG_INSTANCE); +} + +void setInstance(String instance) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString(TAG_INSTANCE, instance); +} + +Future getToken() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(TAG_TOKEN); +} + +void setToken(String token) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString(TAG_TOKEN, token); +} From c44acb87bb3bbf9327a0bfa69dfcc899298a0f9b Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Mon, 8 Jul 2024 09:38:52 +0200 Subject: [PATCH 09/22] feat: display only own sequence --- lib/component/sequence_card.dart | 122 ++++++++++++++ lib/l10n/app_en.arb | 8 + lib/l10n/app_fr.arb | 8 + lib/main.dart | 2 + lib/page/homepage.dart | 21 ++- lib/page/instance_page.dart | 27 +-- lib/page/upload_pictures_page.dart | 158 ++++++++++++------ lib/service/api/api.dart | 1 - lib/service/api/endpoint/collections_api.dart | 43 ++++- lib/service/api/model/geo_visio.dart | 62 ++++++- lib/service/api/model/geo_visio.g.dart | 74 +++++++- 11 files changed, 449 insertions(+), 77 deletions(-) create mode 100644 lib/component/sequence_card.dart diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart new file mode 100644 index 0000000..75867cd --- /dev/null +++ b/lib/component/sequence_card.dart @@ -0,0 +1,122 @@ +part of panoramax; + +class SequenceCard extends StatelessWidget { + final GeoVisioLink sequence; + final GeoVisioCollectionImportStatus geovisioStatus; + const SequenceCard(this.sequence, this.geovisioStatus, {super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + height: 230, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(18), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + spreadRadius: 4, + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + geovisioStatus.status == "ready" + ? Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + image: DecorationImage( + image: Image.network(sequence.getThumbUrl()!).image, + fit: BoxFit.cover, + ))) + : Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + ), + child: Container( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + geovisioStatus.status == "preparing" + ? Icon( + Icons.check_circle_outline, + color: Colors.blue, + size: 60, + ) + : CircularProgressIndicator( + strokeWidth: 4, // thickness of the circle + color: Colors.blue, // color of the progress bar + ), + Text(geovisioStatus.status == "preparing" + ? AppLocalizations.of(context)!.sendingCompleted + : AppLocalizations.of(context)!.sendingInProgress) + ])))), + Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${geovisioStatus.items.length} ${AppLocalizations.of(context)!.element}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ), + /*Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${sequence.stats_items?.count} ${AppLocalizations.of(context)!.photo}', + style: GoogleFonts.nunito( + fontSize: 14, + color: Colors.grey[500], + fontWeight: FontWeight.w400, + ), + ) + ], + ), + ),*/ + Container( + margin: const EdgeInsets.fromLTRB(10, 3, 10, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + sequence.created != null + ? DateFormat(DATE_FORMATTER) + .format(DateTime.parse(sequence.created!)) + : "", + style: GoogleFonts.nunito( + fontSize: 14, + color: Colors.grey[500], + fontWeight: FontWeight.w400, + )) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1a6428a..e2c8ef1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8,6 +8,14 @@ "emptyError": "No element found", "loading": "Loading...", + "mySequences": "My sequence", + "pictures": "pictures", + "shooting": "Shooting", + "sendingInProgress": "Sending in progress", + "sendingCompleted": "Sending completed", + "publishing": "Publishing", + "blurringInProgress": "Blurring in progress", + "capture": "Take a picture", "createSequenceWithPicture_tooltip": "Create a new sequence with captured pictures", "noCameraFoundError": "No camera found for this device", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c9d6777..4c56bcb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -8,6 +8,14 @@ "emptyError": "Aucun élément trouvé", "loading": "Chargement...", + "mySequences": "Mes séquences", + "pictures": "photos", + "shooting": "Prise de vue", + "sendingInProgress": "Envoi en cours", + "sendingCompleted": "Envoi terminé", + "publishing": "Publication", + "blurringInProgress": "Floutage en cours", + "capture": "Prendre une photo", "createSequenceWithPicture_tooltip": "Créer une nouvelle séquence avec les photos prises", "noCameraFoundError": "Pas de caméra trouvée pour cet appareil", diff --git a/lib/main.dart b/lib/main.dart index ee98914..ba3c247 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'dart:math'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:camera/camera.dart'; @@ -30,6 +31,7 @@ import 'constant.dart'; part 'component/app_bar.dart'; part 'component/collection_preview.dart'; +part 'component/sequence_card.dart'; part 'page/homepage.dart'; part 'page/capture_page.dart'; part 'page/collection_creation_page.dart'; diff --git a/lib/page/homepage.dart b/lib/page/homepage.dart index 800bde5..56963ab 100644 --- a/lib/page/homepage.dart +++ b/lib/page/homepage.dart @@ -24,7 +24,8 @@ class _HomePageState extends State { isLoading = true; }); try { - refreshedCollections = await CollectionsApi.INSTANCE.apiCollectionsGet(); + refreshedCollections = + await CollectionsApi.INSTANCE.apiCollectionsGetAll(); } catch (e) { print(e); } finally { @@ -39,9 +40,9 @@ class _HomePageState extends State { if (!await PermissionHelper.isPermissionGranted()) { await PermissionHelper.askMissingPermission(); } - await availableCameras().then( - (availableCameras) => GetIt.instance().pushTo(Routes.newSequenceCapture, arguments: availableCameras) - ); + await availableCameras().then((availableCameras) => + GetIt.instance() + .pushTo(Routes.newSequenceCapture, arguments: availableCameras)); } Widget displayBody(isLoading) { @@ -76,7 +77,8 @@ class _HomePageState extends State { child: Semantics( header: true, child: Text(AppLocalizations.of(context)!.yourSequence, - style: GoogleFonts.nunito(fontSize: 25, fontWeight: FontWeight.w400)), + style: GoogleFonts.nunito( + fontSize: 25, fontWeight: FontWeight.w400)), ), ), Expanded( @@ -106,7 +108,8 @@ class CollectionListView extends StatelessWidget { Widget build(BuildContext context) { return ListView.builder( itemCount: collections.length, - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + physics: + const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemBuilder: (BuildContext context, int index) { return CollectionPreview(collections[index]); }, @@ -147,7 +150,8 @@ class NoElementView extends StatelessWidget { Center( child: Text( AppLocalizations.of(context)!.emptyError, - style: GoogleFonts.nunito(fontSize: 18, color: Colors.grey, fontWeight: FontWeight.w400), + style: GoogleFonts.nunito( + fontSize: 18, color: Colors.grey, fontWeight: FontWeight.w400), ), ) ], @@ -168,7 +172,8 @@ class UnknownErrorView extends StatelessWidget { Center( child: Text( AppLocalizations.of(context)!.unknownError, - style: GoogleFonts.nunito(fontSize: 20, color: Colors.red, fontWeight: FontWeight.w400), + style: GoogleFonts.nunito( + fontSize: 20, color: Colors.red, fontWeight: FontWeight.w400), ), ) ], diff --git a/lib/page/instance_page.dart b/lib/page/instance_page.dart index c8f6275..64df6a3 100644 --- a/lib/page/instance_page.dart +++ b/lib/page/instance_page.dart @@ -25,9 +25,10 @@ class _InstanceState extends State { }); } - void getToken() async { + void getJWTToken() async { + final instance = await getInstance(); final cookies = - await cookieManager.getCookies('https://panoramax.${getInstance()}.fr'); + await cookieManager.getCookies('https://panoramax.$instance.fr'); var tokens = await AuthenticationApi.INSTANCE.apiTokensGet(cookies); var token = @@ -41,8 +42,9 @@ class _InstanceState extends State { void initState() { super.initState(); - getInstance().then((instance) { - if (instance != null) { + getInstance().then((instance) async { + final token = await getToken(); + if (instance != null && token != null) { GetIt.instance() .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); } @@ -60,12 +62,17 @@ class _InstanceState extends State { controller: WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( - onNavigationRequest: (request) { - if (request.url == - "https://panoramax.${getInstance()}.fr/") { - getToken(); - } - return NavigationDecision.navigate; + onNavigationRequest: (request) async { + bool shouldNavigate = true; + await getInstance().then((instance) { + if (request.url == "https://panoramax.$instance.fr/") { + getJWTToken(); + shouldNavigate = false; + } + }); + return shouldNavigate + ? NavigationDecision.navigate + : NavigationDecision.prevent; }, )) ..loadRequest(Uri.parse(url!))) diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index 9130034..ccd6e23 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -10,49 +10,82 @@ class UploadPicturesPage extends StatefulWidget { } class _UploadPicturesState extends State { - int _uploadedImagesCount = 0; - bool _isUploading = false; - String? _errorMessage; + late bool isLoading; + GeoVisioCatalog? sequences; + Map mapStatus = Map(); @override void initState() { super.initState(); + isLoading = true; uploadImages(); + getMyCollections(); } - Future uploadImages() async { + Future getMyCollections() async { + GeoVisioCatalog? refreshedSequences; setState(() { - _isUploading = true; - _errorMessage = null; + isLoading = true; }); - try { - final collectionId = await createCollection(); - await sendPictures(collectionId); - setState(() { - _uploadedImagesCount = widget.imgList.length; - _isUploading = false; - }); + refreshedSequences = await CollectionsApi.INSTANCE.getMeCatalog(); } catch (e) { + print(e); + } finally { setState(() { - _errorMessage = e.toString(); - _isUploading = false; + sequences = refreshedSequences; + getStatusOfSequences(); }); + } + } + + Future getStatusOfSequences() async { + Map mapRefresh = Map(); + setState(() { + isLoading = true; + }); + try { + List>? futures = sequences?.links + .where((sequence) => sequence.rel == "child") + .map((sequence) async { + var geovisioStatus = await CollectionsApi.INSTANCE + .getGeovisioStatus(collectionId: sequence.id!); + + mapRefresh[sequence] = geovisioStatus; + }).toList(); + await Future.wait(futures!.cast>()); + } catch (e) { + print(e); } finally { setState(() { - _isUploading = false; + mapStatus = mapRefresh; + isLoading = false; }); } } + Widget displayBodySequences(isLoading) { + if (isLoading) { + return const LoaderIndicatorView(); + } else if (sequences == null || sequences?.links == null) { + return const UnknownErrorView(); + } else if (sequences!.links.isNotEmpty) { + return SequencesListView(mapStatus: mapStatus); + } else { + return const NoElementView(); + } + } + + Future uploadImages() async { + final collectionId = await createCollection(); + await sendPictures(collectionId); + } + Future createCollection() async { try { final collectionName = DateFormat('y_M_d_H_m_s').format(DateTime.now()); final collection = await CollectionsApi.INSTANCE .apiCollectionsCreate(newCollectionName: collectionName); - if (collection == null) { - throw Exception(AppLocalizations.of(context)!.newCollectionFail); - } return collection.id; } catch (e) { rethrow; @@ -77,39 +110,62 @@ class _UploadPicturesState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: BLUE, - body: Center( - child: _isUploading - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - Text( - style: TextStyle(color: Colors.white), - AppLocalizations.of(context)!.newCollectionLoading( - _uploadedImagesCount / widget.imgList.length)), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_errorMessage != null) - Text(AppLocalizations.of(context)! - .newCollectionError(_errorMessage.toString())), - Text( - style: TextStyle(color: Colors.white), - AppLocalizations.of(context)!.newCollectionUploadSuccess), - ElevatedButton( - onPressed: () { - goToCapture(); - }, - child: - Text(AppLocalizations.of(context)!.newCollectionBack), - ), - ], + return RefreshIndicator( + displacement: 250, + strokeWidth: 3, + triggerMode: RefreshIndicatorTriggerMode.onEdge, + onRefresh: () async { + setState(() { + isLoading = true; + getMyCollections(); + }); + }, + child: Scaffold( + appBar: PanoramaxAppBar(context: context), + body: Column( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Semantics( + header: true, + child: Text(AppLocalizations.of(context)!.mySequences, + style: GoogleFonts.nunito( + fontSize: 25, fontWeight: FontWeight.w400)), + ), + ), + Expanded( + child: displayBodySequences(isLoading), ), - ), + ], + ), + )); + } +} + +class SequencesListView extends StatelessWidget { + const SequencesListView({ + super.key, + required this.mapStatus, + }); + + final Map mapStatus; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: mapStatus.length, + physics: + const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + itemBuilder: (BuildContext context, int index) { + GeoVisioLink key = mapStatus.keys.elementAt(index); + GeoVisioCollectionImportStatus value = + mapStatus.values.elementAt(index); + if (key.geovisio_status != "hidden") { + return SequenceCard(key, value); + } else { + return Container(); + } + }, ); } } diff --git a/lib/service/api/api.dart b/lib/service/api/api.dart index a161b58..14f76d4 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -3,7 +3,6 @@ library panoramax.api; import 'dart:async'; import 'package:http/http.dart' as http; import 'package:panoramax_mobile/main.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'model/geo_visio.dart'; import 'model/geo_visio_auth.dart'; diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index 10f29e0..e5cd442 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -7,7 +7,7 @@ class CollectionsApi { /// /// List available collections /// - Future apiCollectionsGet( + Future apiCollectionsGetAll( {int? limit, String? format, List? bbox, @@ -32,7 +32,7 @@ class CollectionsApi { } // create path and map variables - final instance = getInstance(); + final instance = await getInstance(); var url = Uri.https("panoramax.$instance.fr", '/api/collections', queryParams); @@ -95,4 +95,43 @@ class CollectionsApi { throw new Exception('${response.statusCode} - ${response.reasonPhrase}'); } } + + Future getMeCatalog() async { + final instance = await getInstance(); + var url = Uri.https("panoramax.$instance.fr", '/api/users/me/catalog'); + + final token = await getToken(); + + var response = await http.get(url, headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token' + }); + if (response.statusCode >= 200 && response.statusCode < 400) { + var geovisioCatalog = + GeoVisioCatalog.fromJson(json.decode(response.body)); + return geovisioCatalog; + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } + + Future getGeovisioStatus( + {required String collectionId}) async { + final instance = await getInstance(); + var url = Uri.https("panoramax.$instance.fr", + '/api/collections/${collectionId}/geovisio_status'); + + final token = await getToken(); + var response = await http.get(url, headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token' + }); + if (response.statusCode >= 200 && response.statusCode < 400) { + var geovisioStatus = + GeoVisioCollectionImportStatus.fromJson(json.decode(response.body)); + return geovisioStatus; + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } } diff --git a/lib/service/api/model/geo_visio.dart b/lib/service/api/model/geo_visio.dart index 5637e31..92a1dd5 100644 --- a/lib/service/api/model/geo_visio.dart +++ b/lib/service/api/model/geo_visio.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:json_annotation/json_annotation.dart'; part 'geo_visio.g.dart'; @@ -5,18 +7,28 @@ part 'geo_visio.g.dart'; @JsonSerializable() class GeoVisioLink { late String href; - late String rel; + late String? rel; String? type; String? title; String? method; Object? headers; Object? body; bool? merge; + String? created; + @JsonKey(name: "geovisio:status") + String? geovisio_status; + String? id; + @JsonKey(name: "stats:items") + StatsItems? stats_items; factory GeoVisioLink.fromJson(Map json) => _$GeoVisioLinkFromJson(json); Map toJson() => _$GeoVisioLinkToJson(this); + String? getThumbUrl() { + return '$href/thumb.jpg'; + } + GeoVisioLink(); } @@ -86,4 +98,50 @@ class GeoVisioCollections { Map toJson() => _$GeoVisioCollectionsToJson(this); GeoVisioCollections(); -} \ No newline at end of file +} + +@JsonSerializable() +class GeoVisioCatalog { + late String stac_version; + List? stac_extension; + final String type = "Collection"; + late String id; + String? title; + late String description; + late List links; + + factory GeoVisioCatalog.fromJson(Map json) => + _$GeoVisioCatalogFromJson(json); + Map toJson() => _$GeoVisioCatalogToJson(this); + + GeoVisioCatalog(); +} + +@JsonSerializable() +class GeoVisioCollectionImportStatus { + late String status; + late List items; + + factory GeoVisioCollectionImportStatus.fromJson(Map json) => + _$GeoVisioCollectionImportStatusFromJson(json); + Map toJson() => _$GeoVisioCollectionImportStatusToJson(this); + + GeoVisioCollectionImportStatus(); +} + +@JsonSerializable() +class StatusItem { + String? id; + String? status; + bool? processing_in_progress; + int? rank; + int? nb_errors; + String? process_error; + String? processed_at; + + factory StatusItem.fromJson(Map json) => + _$StatusItemFromJson(json); + Map toJson() => _$StatusItemToJson(this); + + StatusItem(); +} diff --git a/lib/service/api/model/geo_visio.g.dart b/lib/service/api/model/geo_visio.g.dart index 4d0f344..0e507e3 100644 --- a/lib/service/api/model/geo_visio.g.dart +++ b/lib/service/api/model/geo_visio.g.dart @@ -8,13 +8,19 @@ part of 'geo_visio.dart'; GeoVisioLink _$GeoVisioLinkFromJson(Map json) => GeoVisioLink() ..href = json['href'] as String - ..rel = json['rel'] as String + ..rel = json['rel'] as String? ..type = json['type'] as String? ..title = json['title'] as String? ..method = json['method'] as String? ..headers = json['headers'] ..body = json['body'] - ..merge = json['merge'] as bool?; + ..merge = json['merge'] as bool? + ..created = json['created'] as String? + ..geovisio_status = json['geovisio:status'] as String? + ..id = json['id'] as String? + ..stats_items = json['stats:items'] == null + ? null + : StatsItems.fromJson(json['stats:items'] as Map); Map _$GeoVisioLinkToJson(GeoVisioLink instance) => { @@ -26,6 +32,10 @@ Map _$GeoVisioLinkToJson(GeoVisioLink instance) => 'headers': instance.headers, 'body': instance.body, 'merge': instance.merge, + 'created': instance.created, + 'geovisio:status': instance.geovisio_status, + 'id': instance.id, + 'stats:items': instance.stats_items, }; GeoVisioProvider _$GeoVisioProviderFromJson(Map json) => @@ -45,7 +55,7 @@ Map _$GeoVisioProviderToJson(GeoVisioProvider instance) => }; StatsItems _$StatsItemsFromJson(Map json) => - StatsItems()..count = json['count'] as int; + StatsItems()..count = (json['count'] as num).toInt(); Map _$StatsItemsToJson(StatsItems instance) => { @@ -112,3 +122,61 @@ Map _$GeoVisioCollectionsToJson( 'links': instance.links, 'collections': instance.collections, }; + +GeoVisioCatalog _$GeoVisioCatalogFromJson(Map json) => + GeoVisioCatalog() + ..stac_version = json['stac_version'] as String + ..stac_extension = (json['stac_extension'] as List?) + ?.map((e) => e as String) + .toList() + ..id = json['id'] as String + ..title = json['title'] as String? + ..description = json['description'] as String + ..links = (json['links'] as List) + .map((e) => GeoVisioLink.fromJson(e as Map)) + .toList(); + +Map _$GeoVisioCatalogToJson(GeoVisioCatalog instance) => + { + 'stac_version': instance.stac_version, + 'stac_extension': instance.stac_extension, + 'id': instance.id, + 'title': instance.title, + 'description': instance.description, + 'links': instance.links, + }; + +GeoVisioCollectionImportStatus _$GeoVisioCollectionImportStatusFromJson( + Map json) => + GeoVisioCollectionImportStatus() + ..status = json['status'] as String + ..items = (json['items'] as List) + .map((e) => StatusItem.fromJson(e as Map)) + .toList(); + +Map _$GeoVisioCollectionImportStatusToJson( + GeoVisioCollectionImportStatus instance) => + { + 'status': instance.status, + 'items': instance.items, + }; + +StatusItem _$StatusItemFromJson(Map json) => StatusItem() + ..id = json['id'] as String? + ..status = json['status'] as String? + ..processing_in_progress = json['processing_in_progress'] as bool? + ..rank = (json['rank'] as num?)?.toInt() + ..nb_errors = (json['nb_errors'] as num?)?.toInt() + ..process_error = json['process_error'] as String? + ..processed_at = json['processed_at'] as String?; + +Map _$StatusItemToJson(StatusItem instance) => + { + 'id': instance.id, + 'status': instance.status, + 'processing_in_progress': instance.processing_in_progress, + 'rank': instance.rank, + 'nb_errors': instance.nb_errors, + 'process_error': instance.process_error, + 'processed_at': instance.processed_at, + }; From 9ed24b7beeec829be8a6fea0f223cb579177e14f Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 21 Jul 2024 07:17:22 +0200 Subject: [PATCH 10/22] display publishing and shooting date --- lib/component/sequence_card.dart | 132 ++++++++++-------- lib/l10n/app_en.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/page/upload_pictures_page.dart | 33 ++--- lib/service/api/endpoint/collections_api.dart | 37 +++++ lib/service/api/model/geo_visio.dart | 23 +++ lib/service/api/model/geo_visio.g.dart | 28 +++- 7 files changed, 181 insertions(+), 76 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index 75867cd..7c06661 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -1,15 +1,33 @@ part of panoramax; -class SequenceCard extends StatelessWidget { +class SequenceCard extends StatefulWidget { + const SequenceCard(this.sequence, {super.key}); + final GeoVisioLink sequence; - final GeoVisioCollectionImportStatus geovisioStatus; - const SequenceCard(this.sequence, this.geovisioStatus, {super.key}); + + @override + State createState() => _SequenceCardState(); +} + +class _SequenceCardState extends State { + late int itemCount; + GeoVisioCollectionImportStatus? geovisioStatus; + + @override + void initState() { + super.initState(); + itemCount = widget.sequence.stats_items!.count; + if (widget.sequence.geovisio_status != "preparing") { + getStatus(); + } + } + + Future getStatus() async {} @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.all(10), - height: 230, width: double.infinity, decoration: BoxDecoration( color: Colors.white, @@ -27,7 +45,7 @@ class SequenceCard extends StatelessWidget { ), child: Column( children: [ - geovisioStatus.status == "ready" + widget.sequence.geovisio_status == "ready" ? Container( height: 140, decoration: BoxDecoration( @@ -36,7 +54,8 @@ class SequenceCard extends StatelessWidget { topRight: Radius.circular(18), ), image: DecorationImage( - image: Image.network(sequence.getThumbUrl()!).image, + image: + Image.network(widget.sequence.getThumbUrl()!).image, fit: BoxFit.cover, ))) : Container( @@ -52,7 +71,7 @@ class SequenceCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - geovisioStatus.status == "preparing" + widget.sequence.geovisio_status == "preparing" ? Icon( Icons.check_circle_outline, color: Colors.blue, @@ -62,61 +81,64 @@ class SequenceCard extends StatelessWidget { strokeWidth: 4, // thickness of the circle color: Colors.blue, // color of the progress bar ), - Text(geovisioStatus.status == "preparing" + Text(widget.sequence.geovisio_status == "preparing" ? AppLocalizations.of(context)!.sendingCompleted : AppLocalizations.of(context)!.sendingInProgress) ])))), Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${geovisioStatus.items.length} ${AppLocalizations.of(context)!.element}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ], - ), - ), - /*Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${sequence.stats_items?.count} ${AppLocalizations.of(context)!.photo}', - style: GoogleFonts.nunito( - fontSize: 14, - color: Colors.grey[500], - fontWeight: FontWeight.w400, + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ], ), - ) - ], - ), - ),*/ - Container( - margin: const EdgeInsets.fromLTRB(10, 3, 10, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - sequence.created != null - ? DateFormat(DATE_FORMATTER) - .format(DateTime.parse(sequence.created!)) - : "", - style: GoogleFonts.nunito( - fontSize: 14, - color: Colors.grey[500], - fontWeight: FontWeight.w400, - )) - ], - ), - ), + Shooting(), + widget.sequence.geovisio_status == "ready" + ? Publishing() + : Container() + ], + )), ], ), ); } + + Widget Shooting() { + String? date = widget.sequence.extent?.temporal?.interval?[0]?[0]; + DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); + return Row( + children: [ + const Icon(Icons.photo_camera), + Padding( + padding: EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)!.shooting)), + Spacer(), + Text(date == null ? "" : dateFormat.format(DateTime.parse(date))) + ], + ); + } + + Widget Publishing() { + DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); + String? date = widget.sequence.created; + return Row( + children: [ + const Icon(Icons.cloud_upload), + Padding( + padding: EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)!.publishing)), + Spacer(), + Text(date == null ? "" : dateFormat.format(DateTime.parse(date))) + ], + ); + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e2c8ef1..2649437 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -9,7 +9,7 @@ "loading": "Loading...", "mySequences": "My sequence", - "pictures": "pictures", + "pictures": "picture(s)", "shooting": "Shooting", "sendingInProgress": "Sending in progress", "sendingCompleted": "Sending completed", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4c56bcb..7613f9d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -9,7 +9,7 @@ "loading": "Chargement...", "mySequences": "Mes séquences", - "pictures": "photos", + "pictures": "photo(s)", "shooting": "Prise de vue", "sendingInProgress": "Envoi en cours", "sendingCompleted": "Envoi terminé", diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index ccd6e23..cec18c1 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -11,35 +11,35 @@ class UploadPicturesPage extends StatefulWidget { class _UploadPicturesState extends State { late bool isLoading; - GeoVisioCatalog? sequences; - Map mapStatus = Map(); + GeoVisioCollection? sequences; @override void initState() { super.initState(); isLoading = true; - uploadImages(); + //uploadImages(); getMyCollections(); } Future getMyCollections() async { - GeoVisioCatalog? refreshedSequences; + GeoVisioCollection? refreshedSequences; setState(() { isLoading = true; }); try { - refreshedSequences = await CollectionsApi.INSTANCE.getMeCatalog(); + refreshedSequences = await CollectionsApi.INSTANCE.getMeCollection(); } catch (e) { print(e); } finally { setState(() { sequences = refreshedSequences; - getStatusOfSequences(); + isLoading = false; + //getStatusOfSequences(); }); } } - Future getStatusOfSequences() async { + /*Future getStatusOfSequences() async { Map mapRefresh = Map(); setState(() { isLoading = true; @@ -62,7 +62,7 @@ class _UploadPicturesState extends State { isLoading = false; }); } - } + }*/ Widget displayBodySequences(isLoading) { if (isLoading) { @@ -70,7 +70,7 @@ class _UploadPicturesState extends State { } else if (sequences == null || sequences?.links == null) { return const UnknownErrorView(); } else if (sequences!.links.isNotEmpty) { - return SequencesListView(mapStatus: mapStatus); + return SequencesListView(links: sequences!.links); } else { return const NoElementView(); } @@ -116,7 +116,6 @@ class _UploadPicturesState extends State { triggerMode: RefreshIndicatorTriggerMode.onEdge, onRefresh: () async { setState(() { - isLoading = true; getMyCollections(); }); }, @@ -145,23 +144,21 @@ class _UploadPicturesState extends State { class SequencesListView extends StatelessWidget { const SequencesListView({ super.key, - required this.mapStatus, + required this.links, }); - final Map mapStatus; + final List links; @override Widget build(BuildContext context) { return ListView.builder( - itemCount: mapStatus.length, + itemCount: links.length, physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemBuilder: (BuildContext context, int index) { - GeoVisioLink key = mapStatus.keys.elementAt(index); - GeoVisioCollectionImportStatus value = - mapStatus.values.elementAt(index); - if (key.geovisio_status != "hidden") { - return SequenceCard(key, value); + if (links[index].rel == "child" && + links[index].geovisio_status != "hidden") { + return SequenceCard(links[index]); } else { return Container(); } diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index e5cd442..4228dfe 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -96,6 +96,43 @@ class CollectionsApi { } } + Future getMeCollection( + {int? limit, List? bbox, String? filter, String? sortby}) async { + // query params + Map queryParams = {}; + if (limit != null) { + queryParams.putIfAbsent("limit", limit as String Function()); + } + if (sortby != null) { + queryParams.putIfAbsent("sortby", sortby as String Function()); + } + if (bbox != null) { + queryParams.putIfAbsent("bbox", bbox as String Function()); + } + if (filter != null) { + queryParams.putIfAbsent("filter", filter as String Function()); + } + + final token = await getToken(); + + final instance = await getInstance(); + var url = Uri.https( + "panoramax.$instance.fr", '/api/users/me/collection', queryParams); + + var response = await http.get(url, headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token' + }); + + if (response.statusCode >= 200) { + var geovisioCollection = + GeoVisioCollection.fromJson(json.decode(response.body)); + return geovisioCollection; + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } + Future getMeCatalog() async { final instance = await getInstance(); var url = Uri.https("panoramax.$instance.fr", '/api/users/me/catalog'); diff --git a/lib/service/api/model/geo_visio.dart b/lib/service/api/model/geo_visio.dart index 92a1dd5..e6f26f6 100644 --- a/lib/service/api/model/geo_visio.dart +++ b/lib/service/api/model/geo_visio.dart @@ -20,6 +20,7 @@ class GeoVisioLink { String? id; @JsonKey(name: "stats:items") StatsItems? stats_items; + GeoVisioExtent? extent; factory GeoVisioLink.fromJson(Map json) => _$GeoVisioLinkFromJson(json); @@ -32,6 +33,28 @@ class GeoVisioLink { GeoVisioLink(); } +@JsonSerializable() +class GeoVisioExtent { + Object? spatial; + Temporal? temporal; + + factory GeoVisioExtent.fromJson(Map json) => + _$GeoVisioExtentFromJson(json); + Map toJson() => _$GeoVisioExtentToJson(this); + + GeoVisioExtent(); +} + +@JsonSerializable() +class Temporal { + List?>? interval; + factory Temporal.fromJson(Map json) => + _$TemporalFromJson(json); + Map toJson() => _$TemporalToJson(this); + + Temporal(); +} + @JsonSerializable() class GeoVisioProvider { late String name; diff --git a/lib/service/api/model/geo_visio.g.dart b/lib/service/api/model/geo_visio.g.dart index 0e507e3..10056bd 100644 --- a/lib/service/api/model/geo_visio.g.dart +++ b/lib/service/api/model/geo_visio.g.dart @@ -20,7 +20,10 @@ GeoVisioLink _$GeoVisioLinkFromJson(Map json) => GeoVisioLink() ..id = json['id'] as String? ..stats_items = json['stats:items'] == null ? null - : StatsItems.fromJson(json['stats:items'] as Map); + : StatsItems.fromJson(json['stats:items'] as Map) + ..extent = json['extent'] == null + ? null + : GeoVisioExtent.fromJson(json['extent'] as Map); Map _$GeoVisioLinkToJson(GeoVisioLink instance) => { @@ -36,6 +39,29 @@ Map _$GeoVisioLinkToJson(GeoVisioLink instance) => 'geovisio:status': instance.geovisio_status, 'id': instance.id, 'stats:items': instance.stats_items, + 'extent': instance.extent, + }; + +GeoVisioExtent _$GeoVisioExtentFromJson(Map json) => + GeoVisioExtent() + ..spatial = json['spatial'] + ..temporal = json['temporal'] == null + ? null + : Temporal.fromJson(json['temporal'] as Map); + +Map _$GeoVisioExtentToJson(GeoVisioExtent instance) => + { + 'spatial': instance.spatial, + 'temporal': instance.temporal, + }; + +Temporal _$TemporalFromJson(Map json) => Temporal() + ..interval = (json['interval'] as List?) + ?.map((e) => (e as List?)?.map((e) => e as String?).toList()) + .toList(); + +Map _$TemporalToJson(Temporal instance) => { + 'interval': instance.interval, }; GeoVisioProvider _$GeoVisioProviderFromJson(Map json) => From 2e1a22b8c2483ff4228a874f30c9e3fae87df779 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 21 Jul 2024 07:31:36 +0200 Subject: [PATCH 11/22] refactor: split the code of sequence card into smaller components --- lib/component/sequence_card.dart | 133 +++++++++++++++++-------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index 7c06661..e02d8b8 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -45,73 +45,38 @@ class _SequenceCardState extends State { ), child: Column( children: [ - widget.sequence.geovisio_status == "ready" - ? Container( - height: 140, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), - ), - image: DecorationImage( - image: - Image.network(widget.sequence.getThumbUrl()!).image, - fit: BoxFit.cover, - ))) - : Container( - height: 140, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), - ), - ), - child: Container( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.sequence.geovisio_status == "preparing" - ? Icon( - Icons.check_circle_outline, - color: Colors.blue, - size: 60, - ) - : CircularProgressIndicator( - strokeWidth: 4, // thickness of the circle - color: Colors.blue, // color of the progress bar - ), - Text(widget.sequence.geovisio_status == "preparing" - ? AppLocalizations.of(context)!.sendingCompleted - : AppLocalizations.of(context)!.sendingInProgress) - ])))), - Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$itemCount ${AppLocalizations.of(context)!.pictures}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ], - ), - Shooting(), - widget.sequence.geovisio_status == "ready" - ? Publishing() - : Container() - ], - )), + widget.sequence.geovisio_status == "ready" ? Picture() : Loader(), + PictureDetail(), ], ), ); } + Widget PictureDetail() { + return Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + Shooting(), + widget.sequence.geovisio_status == "ready" + ? Publishing() + : Container() + ], + )); + } + Widget Shooting() { String? date = widget.sequence.extent?.temporal?.interval?[0]?[0]; DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); @@ -141,4 +106,48 @@ class _SequenceCardState extends State { ], ); } + + Widget Loader() { + return Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + ), + child: Container( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.sequence.geovisio_status == "preparing" + ? Icon( + Icons.check_circle_outline, + color: Colors.blue, + size: 60, + ) + : CircularProgressIndicator( + strokeWidth: 4, // thickness of the circle + color: Colors.blue, // color of the progress bar + ), + Text(widget.sequence.geovisio_status == "preparing" + ? AppLocalizations.of(context)!.sendingCompleted + : AppLocalizations.of(context)!.sendingInProgress) + ])))); + } + + Widget Picture() { + return Container( + height: 140, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + image: DecorationImage( + image: Image.network(widget.sequence.getThumbUrl()!).image, + fit: BoxFit.cover, + ))); + } } From 2c1ff72ddad360cecb33e8ec34310ba9f3becc47 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 21 Jul 2024 09:26:58 +0200 Subject: [PATCH 12/22] feat: add share button on sequence card --- lib/component/sequence_card.dart | 49 ++++++++++++++++++++++++-------- lib/l10n/app_en.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/main.dart | 1 + pubspec.yaml | 2 ++ 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index e02d8b8..e1f6550 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -24,6 +24,15 @@ class _SequenceCardState extends State { Future getStatus() async {} + Future openUrl() async { + final instance = await getInstance(); + final Uri url = + Uri.https("panoramax.$instance.fr", '/sequence/${widget.sequence.id}'); + if (!await launchUrl(url)) { + throw Exception("Could not launch $url"); + } + } + @override Widget build(BuildContext context) { return Container( @@ -57,18 +66,7 @@ class _SequenceCardState extends State { margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), child: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$itemCount ${AppLocalizations.of(context)!.pictures}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ], - ), + PictureCount(), Shooting(), widget.sequence.geovisio_status == "ready" ? Publishing() @@ -77,6 +75,33 @@ class _SequenceCardState extends State { )); } + Widget PictureCount() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + widget.sequence.geovisio_status == "ready" + ? FloatingActionButton( + onPressed: openUrl, + child: Icon( + Icons.share, + //size: 14, + ), + shape: CircleBorder(), + mini: true, + backgroundColor: Colors.grey, + tooltip: AppLocalizations.of(context)!.share) + : Container(), + ], + ); + } + Widget Shooting() { String? date = widget.sequence.extent?.temporal?.interval?[0]?[0]; DateFormat dateFormat = DateFormat.yMMMd('fr_FR').add_Hm(); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2649437..e17bc8e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -15,6 +15,7 @@ "sendingCompleted": "Sending completed", "publishing": "Publishing", "blurringInProgress": "Blurring in progress", + "share": "Share", "capture": "Take a picture", "createSequenceWithPicture_tooltip": "Create a new sequence with captured pictures", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7613f9d..ab3ad0f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -15,6 +15,7 @@ "sendingCompleted": "Envoi terminé", "publishing": "Publication", "blurringInProgress": "Floutage en cours", + "share": "Partager", "capture": "Prendre une photo", "createSequenceWithPicture_tooltip": "Créer une nouvelle séquence avec les photos prises", diff --git a/lib/main.dart b/lib/main.dart index ba3c247..55d0995 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,6 +25,7 @@ import 'package:native_exif/native_exif.dart'; import 'package:flutter_exif_plugin/flutter_exif_plugin.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:sensors/sensors.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'component/loader.dart'; import 'service/api/api.dart'; import 'constant.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 1728041..f0f9cf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: flutter_exif_plugin: ^1.1.0 sensors: ^2.0.3 wakelock_plus: ^1.2.7 + url_launcher: ^6.3.0 + dev_dependencies: flutter_test: From d0d0255370978326ded3bbd856b2e0d871ab424a Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 25 Jul 2024 11:42:34 +0200 Subject: [PATCH 13/22] feat: automatically updates the upload information of sequences --- lib/component/sequence_card.dart | 83 ++++++++++++++++++++++++++---- lib/page/upload_pictures_page.dart | 68 +++++++++++------------- 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index e1f6550..b4133a4 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -1,9 +1,11 @@ part of panoramax; class SequenceCard extends StatefulWidget { - const SequenceCard(this.sequence, {super.key}); + const SequenceCard(this.sequence, {this.sequenceCount, super.key}); final GeoVisioLink sequence; + final int? + sequenceCount; //if sequenceCount is not null, there are photos being uploaded @override State createState() => _SequenceCardState(); @@ -12,17 +14,39 @@ class SequenceCard extends StatefulWidget { class _SequenceCardState extends State { late int itemCount; GeoVisioCollectionImportStatus? geovisioStatus; + Timer? timer; @override void initState() { super.initState(); itemCount = widget.sequence.stats_items!.count; - if (widget.sequence.geovisio_status != "preparing") { - getStatus(); + if (widget.sequence.geovisio_status != "preparing" || + widget.sequenceCount != null) { + timer = Timer.periodic(Duration(seconds: 1), (timer) { + getStatus(); + }); } } - Future getStatus() async {} + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + Future getStatus() async { + GeoVisioCollectionImportStatus? geovisioStatusRefresh; + try { + geovisioStatusRefresh = await CollectionsApi.INSTANCE + .getGeovisioStatus(collectionId: widget.sequence.id!); + } catch (e) { + print(e); + } finally { + setState(() { + geovisioStatus = geovisioStatusRefresh; + }); + } + } Future openUrl() async { final instance = await getInstance(); @@ -54,7 +78,10 @@ class _SequenceCardState extends State { ), child: Column( children: [ - widget.sequence.geovisio_status == "ready" ? Picture() : Loader(), + widget.sequence.geovisio_status == "ready" && + widget.sequenceCount == null + ? Picture() + : Loader(), PictureDetail(), ], ), @@ -70,23 +97,30 @@ class _SequenceCardState extends State { Shooting(), widget.sequence.geovisio_status == "ready" ? Publishing() + : Container(), + widget.sequenceCount == null || + widget.sequence.stats_items?.count == widget.sequenceCount + ? Blurring() : Container() ], )); } Widget PictureCount() { + final count = + widget.sequenceCount == null ? itemCount : widget.sequenceCount; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '$itemCount ${AppLocalizations.of(context)!.pictures}', + '$count ${AppLocalizations.of(context)!.pictures}', style: GoogleFonts.nunito( fontSize: 18, fontWeight: FontWeight.w800, ), ), - widget.sequence.geovisio_status == "ready" + widget.sequence.geovisio_status == "ready" && + widget.sequenceCount == null ? FloatingActionButton( onPressed: openUrl, child: Icon( @@ -146,7 +180,8 @@ class _SequenceCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - widget.sequence.geovisio_status == "preparing" + widget.sequenceCount == null || + geovisioStatus?.items.length == widget.sequenceCount ? Icon( Icons.check_circle_outline, color: Colors.blue, @@ -156,7 +191,8 @@ class _SequenceCardState extends State { strokeWidth: 4, // thickness of the circle color: Colors.blue, // color of the progress bar ), - Text(widget.sequence.geovisio_status == "preparing" + Text(widget.sequenceCount == null || + geovisioStatus?.items.length == widget.sequenceCount ? AppLocalizations.of(context)!.sendingCompleted : AppLocalizations.of(context)!.sendingInProgress) ])))); @@ -175,4 +211,33 @@ class _SequenceCardState extends State { fit: BoxFit.cover, ))); } + + Widget Blurring() { + int count = geovisioStatus?.items + .where( + (element) => element.status == "ready", + ) + .length ?? + 0; + double total = geovisioStatus?.items.length.toDouble() ?? 0; + return Container( + child: Column( + children: [ + LinearProgressIndicator( + value: (total == 0) ? 0 : count / total, //don't divide by 0 ! + semanticsLabel: AppLocalizations.of(context)!.blurringInProgress, + minHeight: 16, + borderRadius: BorderRadius.circular(8), + ), + Row( + children: [ + const Icon(Icons.blur_on_outlined), + Padding( + padding: EdgeInsets.all(8), + child: Text(AppLocalizations.of(context)!.blurringInProgress)) + ], + ) + ], + )); + } } diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index cec18c1..ce85b4e 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -12,12 +12,15 @@ class UploadPicturesPage extends StatefulWidget { class _UploadPicturesState extends State { late bool isLoading; GeoVisioCollection? sequences; + late int sequenceCount; + String? collectionId; @override void initState() { super.initState(); isLoading = true; - //uploadImages(); + sequenceCount = widget.imgList.length; + uploadImages(); getMyCollections(); } @@ -34,68 +37,50 @@ class _UploadPicturesState extends State { setState(() { sequences = refreshedSequences; isLoading = false; - //getStatusOfSequences(); }); } } - /*Future getStatusOfSequences() async { - Map mapRefresh = Map(); - setState(() { - isLoading = true; - }); - try { - List>? futures = sequences?.links - .where((sequence) => sequence.rel == "child") - .map((sequence) async { - var geovisioStatus = await CollectionsApi.INSTANCE - .getGeovisioStatus(collectionId: sequence.id!); - - mapRefresh[sequence] = geovisioStatus; - }).toList(); - await Future.wait(futures!.cast>()); - } catch (e) { - print(e); - } finally { - setState(() { - mapStatus = mapRefresh; - isLoading = false; - }); - } - }*/ - Widget displayBodySequences(isLoading) { if (isLoading) { return const LoaderIndicatorView(); } else if (sequences == null || sequences?.links == null) { return const UnknownErrorView(); } else if (sequences!.links.isNotEmpty) { - return SequencesListView(links: sequences!.links); + return SequencesListView( + links: sequences!.links, + collectionId: collectionId, + lastSequenceCount: sequenceCount); } else { return const NoElementView(); } } Future uploadImages() async { - final collectionId = await createCollection(); - await sendPictures(collectionId); + await createCollection(); + await sendPictures(); } - Future createCollection() async { + Future createCollection() async { try { final collectionName = DateFormat('y_M_d_H_m_s').format(DateTime.now()); final collection = await CollectionsApi.INSTANCE .apiCollectionsCreate(newCollectionName: collectionName); - return collection.id; + setState(() { + collectionId = collection.id; + }); } catch (e) { rethrow; } } - Future sendPictures(String collectionId) async { + Future sendPictures() async { + if (collectionId == null) { + return; + } for (var i = 0; i < widget.imgList.length; i++) { await CollectionsApi.INSTANCE.apiCollectionsUploadPicture( - collectionId: collectionId, + collectionId: collectionId!, position: i + 1, pictureToUpload: widget.imgList[i], ); @@ -142,12 +127,15 @@ class _UploadPicturesState extends State { } class SequencesListView extends StatelessWidget { - const SequencesListView({ - super.key, - required this.links, - }); + const SequencesListView( + {super.key, + required this.links, + required this.collectionId, + required this.lastSequenceCount}); final List links; + final String? collectionId; + final int lastSequenceCount; @override Widget build(BuildContext context) { @@ -158,7 +146,9 @@ class SequencesListView extends StatelessWidget { itemBuilder: (BuildContext context, int index) { if (links[index].rel == "child" && links[index].geovisio_status != "hidden") { - return SequenceCard(links[index]); + return SequenceCard(links[index], + sequenceCount: + links[index].id == collectionId ? lastSequenceCount : null); } else { return Container(); } From 954805c35b71f78cd242bf2814e1db06940ce6e1 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Sun, 28 Jul 2024 21:46:19 +0200 Subject: [PATCH 14/22] fix: fix the status check of the last sequence --- lib/component/sequence_card.dart | 110 +++++++++++++++++++------------ 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index b4133a4..807e8a8 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -15,19 +15,48 @@ class _SequenceCardState extends State { late int itemCount; GeoVisioCollectionImportStatus? geovisioStatus; Timer? timer; + SequenceState sequenceState = SequenceState.SENDING; @override void initState() { super.initState(); itemCount = widget.sequence.stats_items!.count; - if (widget.sequence.geovisio_status != "preparing" || - widget.sequenceCount != null) { - timer = Timer.periodic(Duration(seconds: 1), (timer) { + checkSequenceState(); + if (sequenceState != SequenceState.READY || widget.sequenceCount != null) { + timer = Timer.periodic(Duration(seconds: 5), (timer) { getStatus(); }); } } + void checkSequenceState() { + int count = geovisioStatus?.items + .where( + (element) => element.status == "ready", + ) + .length ?? + 0; + print("checkSequenceStat"); + setState(() { + sequenceState = geovisioStatus?.status == "deleted" + ? SequenceState.DELETED + : widget.sequence.geovisio_status == "hidden" + ? SequenceState.HIDDEN + : widget.sequenceCount == null + ? widget.sequence.geovisio_status == "ready" + ? SequenceState.READY + : SequenceState.BLURRING + : widget.sequenceCount != geovisioStatus?.items.length + ? SequenceState.SENDING + : count == widget.sequenceCount + ? SequenceState.READY + : SequenceState.BLURRING; + }); + if (sequenceState == SequenceState.DELETED) { + timer?.cancel(); + } + } + @override void dispose() { timer?.cancel(); @@ -44,6 +73,7 @@ class _SequenceCardState extends State { } finally { setState(() { geovisioStatus = geovisioStatusRefresh; + checkSequenceState(); }); } } @@ -59,33 +89,33 @@ class _SequenceCardState extends State { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.all(10), - width: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all( - Radius.circular(18), - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade200, - spreadRadius: 4, - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: Column( - children: [ - widget.sequence.geovisio_status == "ready" && - widget.sequenceCount == null - ? Picture() - : Loader(), - PictureDetail(), - ], - ), - ); + return sequenceState == SequenceState.DELETED || + sequenceState == SequenceState.HIDDEN + ? Container() + : Container( + margin: const EdgeInsets.all(10), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(18), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + spreadRadius: 4, + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + sequenceState == SequenceState.READY ? Picture() : Loader(), + PictureDetail(), + ], + ), + ); } Widget PictureDetail() { @@ -95,13 +125,8 @@ class _SequenceCardState extends State { children: [ PictureCount(), Shooting(), - widget.sequence.geovisio_status == "ready" - ? Publishing() - : Container(), - widget.sequenceCount == null || - widget.sequence.stats_items?.count == widget.sequenceCount - ? Blurring() - : Container() + sequenceState == SequenceState.READY ? Publishing() : Container(), + sequenceState == SequenceState.BLURRING ? Blurring() : Container() ], )); } @@ -119,8 +144,7 @@ class _SequenceCardState extends State { fontWeight: FontWeight.w800, ), ), - widget.sequence.geovisio_status == "ready" && - widget.sequenceCount == null + sequenceState == SequenceState.READY ? FloatingActionButton( onPressed: openUrl, child: Icon( @@ -180,8 +204,7 @@ class _SequenceCardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - widget.sequenceCount == null || - geovisioStatus?.items.length == widget.sequenceCount + sequenceState == SequenceState.BLURRING ? Icon( Icons.check_circle_outline, color: Colors.blue, @@ -191,8 +214,7 @@ class _SequenceCardState extends State { strokeWidth: 4, // thickness of the circle color: Colors.blue, // color of the progress bar ), - Text(widget.sequenceCount == null || - geovisioStatus?.items.length == widget.sequenceCount + Text(sequenceState == SequenceState.BLURRING ? AppLocalizations.of(context)!.sendingCompleted : AppLocalizations.of(context)!.sendingInProgress) ])))); @@ -241,3 +263,5 @@ class _SequenceCardState extends State { )); } } + +enum SequenceState { SENDING, BLURRING, READY, DELETED, HIDDEN } From 2b9be8f1ae0ffe375c7f086d950630466cac79c4 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Wed, 21 Aug 2024 14:48:03 +0200 Subject: [PATCH 15/22] display hidden sequence with a special status --- lib/component/sequence_card.dart | 114 +++++++++++++----- lib/l10n/app_en.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/page/upload_pictures_page.dart | 3 +- lib/service/api/api.dart | 1 + lib/service/api/endpoint/collections_api.dart | 18 +++ 6 files changed, 108 insertions(+), 30 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index 807e8a8..ead68be 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -16,19 +16,40 @@ class _SequenceCardState extends State { GeoVisioCollectionImportStatus? geovisioStatus; Timer? timer; SequenceState sequenceState = SequenceState.SENDING; + MemoryImage? image; @override void initState() { super.initState(); itemCount = widget.sequence.stats_items!.count; checkSequenceState(); - if (sequenceState != SequenceState.READY || widget.sequenceCount != null) { + if (sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN) { + getImage(); + } + if ((sequenceState != SequenceState.READY && + sequenceState != SequenceState.HIDDEN) || + widget.sequenceCount != null) { timer = Timer.periodic(Duration(seconds: 5), (timer) { getStatus(); }); } } + void getImage() async { + MemoryImage? imageRefresh; + try { + imageRefresh = await CollectionsApi.INSTANCE + .getThumbernail(collectionId: widget.sequence.id!); + } catch (e) { + print(e); + } finally { + setState(() { + image = imageRefresh; + }); + } + } + void checkSequenceState() { int count = geovisioStatus?.items .where( @@ -89,14 +110,15 @@ class _SequenceCardState extends State { @override Widget build(BuildContext context) { - return sequenceState == SequenceState.DELETED || - sequenceState == SequenceState.HIDDEN + return sequenceState == SequenceState.DELETED ? Container() : Container( margin: const EdgeInsets.all(10), width: double.infinity, decoration: BoxDecoration( - color: Colors.white, + color: sequenceState == SequenceState.HIDDEN + ? Colors.grey.shade400 + : Colors.white, borderRadius: const BorderRadius.all( Radius.circular(18), ), @@ -109,12 +131,13 @@ class _SequenceCardState extends State { ), ], ), - child: Column( - children: [ - sequenceState == SequenceState.READY ? Picture() : Loader(), - PictureDetail(), - ], - ), + child: Column(children: [ + sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN + ? Picture() + : Loader(), + PictureDetail(), + ]), ); } @@ -125,7 +148,10 @@ class _SequenceCardState extends State { children: [ PictureCount(), Shooting(), - sequenceState == SequenceState.READY ? Publishing() : Container(), + sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN + ? Publishing() + : Container(), sequenceState == SequenceState.BLURRING ? Blurring() : Container() ], )); @@ -137,14 +163,17 @@ class _SequenceCardState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '$count ${AppLocalizations.of(context)!.pictures}', - style: GoogleFonts.nunito( - fontSize: 18, - fontWeight: FontWeight.w800, + Row(children: [ + Text( + '$count ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), ), - ), - sequenceState == SequenceState.READY + ]), + sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN ? FloatingActionButton( onPressed: openUrl, child: Icon( @@ -221,17 +250,46 @@ class _SequenceCardState extends State { } Widget Picture() { - return Container( - height: 140, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), + return ClipRect( + child: Stack( + children: [ + if (image != null && image is MemoryImage) + Container( + height: 180, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + ), + image: DecorationImage( + image: image!, + fit: BoxFit.cover, + ))), + if (sequenceState == SequenceState.HIDDEN) + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + color: Colors.transparent, + height: 180, + width: double.infinity, // or a specific width + ), + ), ), - image: DecorationImage( - image: Image.network(widget.sequence.getThumbUrl()!).image, - fit: BoxFit.cover, - ))); + if (sequenceState == SequenceState.HIDDEN) + Container( + height: 180, + child: Center( + child: Text( + AppLocalizations.of(context)!.hidden, + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + color: Colors.white), + ))) + ], + ), + ); } Widget Blurring() { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e17bc8e..0cf1ecf 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -16,6 +16,7 @@ "publishing": "Publishing", "blurringInProgress": "Blurring in progress", "share": "Share", + "hidden": "Hidden sequence", "capture": "Take a picture", "createSequenceWithPicture_tooltip": "Create a new sequence with captured pictures", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index ab3ad0f..a68fa96 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -16,6 +16,7 @@ "publishing": "Publication", "blurringInProgress": "Floutage en cours", "share": "Partager", + "hidden": "Séquence masquée", "capture": "Prendre une photo", "createSequenceWithPicture_tooltip": "Créer une nouvelle séquence avec les photos prises", diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index ce85b4e..069b850 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -144,8 +144,7 @@ class SequencesListView extends StatelessWidget { physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemBuilder: (BuildContext context, int index) { - if (links[index].rel == "child" && - links[index].geovisio_status != "hidden") { + if (links[index].rel == "child") { return SequenceCard(links[index], sequenceCount: links[index].id == collectionId ? lastSequenceCount : null); diff --git a/lib/service/api/api.dart b/lib/service/api/api.dart index 14f76d4..d06c981 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -1,6 +1,7 @@ library panoramax.api; import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:panoramax_mobile/main.dart'; import 'dart:convert'; diff --git a/lib/service/api/endpoint/collections_api.dart b/lib/service/api/endpoint/collections_api.dart index 4228dfe..db56f41 100644 --- a/lib/service/api/endpoint/collections_api.dart +++ b/lib/service/api/endpoint/collections_api.dart @@ -171,4 +171,22 @@ class CollectionsApi { throw new Exception('${response.statusCode} - ${response.body}'); } } + + Future getThumbernail({required String collectionId}) async { + final instance = await getInstance(); + var url = Uri.https( + "panoramax.$instance.fr", '/api/collections/${collectionId}/thumb.jpg'); + + final token = await getToken(); + + var response = await http.get(url, headers: { + 'Content-Type': 'image/jpeg', + 'Authorization': 'Bearer $token' + }); + if (response.statusCode >= 200 && response.statusCode < 400) { + return MemoryImage(response.bodyBytes); + } else { + throw new Exception('${response.statusCode} - ${response.body}'); + } + } } From cc9c23c692b55f37645c9409abf5ee2bac3cafa8 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 22 Aug 2024 08:35:57 +0200 Subject: [PATCH 16/22] feat: open url when an user clicks on sequence card --- lib/component/sequence_card.dart | 54 +++++++++++++++++--------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index ead68be..d812b3d 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -112,33 +112,35 @@ class _SequenceCardState extends State { Widget build(BuildContext context) { return sequenceState == SequenceState.DELETED ? Container() - : Container( - margin: const EdgeInsets.all(10), - width: double.infinity, - decoration: BoxDecoration( - color: sequenceState == SequenceState.HIDDEN - ? Colors.grey.shade400 - : Colors.white, - borderRadius: const BorderRadius.all( - Radius.circular(18), - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade200, - spreadRadius: 4, - blurRadius: 6, - offset: const Offset(0, 3), + : GestureDetector( + onTap: openUrl, + child: Container( + margin: const EdgeInsets.all(10), + width: double.infinity, + decoration: BoxDecoration( + color: sequenceState == SequenceState.HIDDEN + ? Colors.grey.shade400 + : Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(18), ), - ], - ), - child: Column(children: [ - sequenceState == SequenceState.READY || - sequenceState == SequenceState.HIDDEN - ? Picture() - : Loader(), - PictureDetail(), - ]), - ); + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + spreadRadius: 4, + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Column(children: [ + sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN + ? Picture() + : Loader(), + PictureDetail(), + ]), + )); } Widget PictureDetail() { From f3ab4e607eb2dc519a46bb7baa4837122609b193 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 22 Aug 2024 08:36:29 +0200 Subject: [PATCH 17/22] fix image for the uploading sequence --- lib/component/sequence_card.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index d812b3d..8df9444 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -23,10 +23,7 @@ class _SequenceCardState extends State { super.initState(); itemCount = widget.sequence.stats_items!.count; checkSequenceState(); - if (sequenceState == SequenceState.READY || - sequenceState == SequenceState.HIDDEN) { - getImage(); - } + getImage(); if ((sequenceState != SequenceState.READY && sequenceState != SequenceState.HIDDEN) || widget.sequenceCount != null) { From 1ba945325cec3b7090d85849a8ec43ee49671aa6 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 22 Aug 2024 09:43:50 +0200 Subject: [PATCH 18/22] feat: share url when an user clicks on share button --- lib/component/sequence_card.dart | 8 +++++++- lib/main.dart | 1 + pubspec.yaml | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/component/sequence_card.dart b/lib/component/sequence_card.dart index 8df9444..e33a793 100644 --- a/lib/component/sequence_card.dart +++ b/lib/component/sequence_card.dart @@ -105,6 +105,12 @@ class _SequenceCardState extends State { } } + Future shareUrl() async { + final instance = await getInstance(); + final url = "panoramax.$instance.fr/sequence/${widget.sequence.id}"; + await Share.share(url); + } + @override Widget build(BuildContext context) { return sequenceState == SequenceState.DELETED @@ -174,7 +180,7 @@ class _SequenceCardState extends State { sequenceState == SequenceState.READY || sequenceState == SequenceState.HIDDEN ? FloatingActionButton( - onPressed: openUrl, + onPressed: shareUrl, child: Icon( Icons.share, //size: 14, diff --git a/lib/main.dart b/lib/main.dart index 55d0995..c8db247 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:flutter_exif_plugin/flutter_exif_plugin.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:sensors/sensors.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:share/share.dart'; import 'component/loader.dart'; import 'service/api/api.dart'; import 'constant.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index f0f9cf1..194ef00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: sensors: ^2.0.3 wakelock_plus: ^1.2.7 url_launcher: ^6.3.0 + share: ^2.0.4 dev_dependencies: From 816c9fdf89f2417e0b670eae20b54c63e9445a9f Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 22 Aug 2024 12:12:59 +0200 Subject: [PATCH 19/22] feat: disable back button and add a new capture button --- lib/component/app_bar.dart | 10 ++-- lib/page/upload_pictures_page.dart | 73 +++++++++++++++++++----------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/lib/component/app_bar.dart b/lib/component/app_bar.dart index c56fec2..5d664a4 100644 --- a/lib/component/app_bar.dart +++ b/lib/component/app_bar.dart @@ -1,10 +1,10 @@ part of panoramax; -PreferredSizeWidget PanoramaxAppBar({context, title = "Panoramax"}) { +PreferredSizeWidget PanoramaxAppBar( + {context, title = "Panoramax", backEnabled = true}) { return AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text( - title - ), + title: Text(title), + automaticallyImplyLeading: backEnabled, ); -} \ No newline at end of file +} diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index 069b850..26745d2 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -88,6 +88,9 @@ class _UploadPicturesState extends State { } Future goToCapture() async { + if (!await PermissionHelper.isPermissionGranted()) { + await PermissionHelper.askMissingPermission(); + } await availableCameras().then((availableCameras) => GetIt.instance() .pushTo(Routes.newSequenceCapture, arguments: availableCameras)); @@ -95,34 +98,50 @@ class _UploadPicturesState extends State { @override Widget build(BuildContext context) { - return RefreshIndicator( - displacement: 250, - strokeWidth: 3, - triggerMode: RefreshIndicatorTriggerMode.onEdge, - onRefresh: () async { - setState(() { - getMyCollections(); - }); - }, - child: Scaffold( - appBar: PanoramaxAppBar(context: context), - body: Column( - children: [ - Container( - margin: const EdgeInsets.fromLTRB(10, 10, 10, 10), - child: Semantics( - header: true, - child: Text(AppLocalizations.of(context)!.mySequences, - style: GoogleFonts.nunito( - fontSize: 25, fontWeight: FontWeight.w400)), - ), - ), - Expanded( - child: displayBodySequences(isLoading), + return PopScope( + canPop: false, + child: RefreshIndicator( + displacement: 250, + strokeWidth: 3, + triggerMode: RefreshIndicatorTriggerMode.onEdge, + onRefresh: () async { + setState(() { + getMyCollections(); + }); + }, + child: Scaffold( + appBar: PanoramaxAppBar(context: context, backEnabled: false), + body: Column( + children: [ + Padding( + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Semantics( + header: true, + child: Text( + AppLocalizations.of(context)!.mySequences, + style: GoogleFonts.nunito( + fontSize: 25, + fontWeight: FontWeight.w400)), + ), + FloatingActionButton( + onPressed: goToCapture, + child: Icon(Icons.add_a_photo), + shape: CircleBorder(), + mini: true, + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + tooltip: AppLocalizations.of(context)! + .createSequence_tooltip) + ])), + Expanded( + child: displayBodySequences(isLoading), + ), + ], ), - ], - ), - )); + ))); } } From f443b31bd40545bac3863bd6581de20d93dbaefc Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Thu, 22 Aug 2024 13:14:45 +0200 Subject: [PATCH 20/22] feat: display user sequence if is connected --- lib/page/homepage.dart | 147 ++++------------------------- lib/page/upload_pictures_page.dart | 4 +- 2 files changed, 19 insertions(+), 132 deletions(-) diff --git a/lib/page/homepage.dart b/lib/page/homepage.dart index 56963ab..f2485e4 100644 --- a/lib/page/homepage.dart +++ b/lib/page/homepage.dart @@ -8,34 +8,28 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - late bool isLoading; - GeoVisioCollections? geoVisionCollections; - @override void initState() { super.initState(); - isLoading = true; - getCollections(); + redirectUser(); } - Future getCollections() async { - GeoVisioCollections? refreshedCollections; - setState(() { - isLoading = true; - }); - try { - refreshedCollections = - await CollectionsApi.INSTANCE.apiCollectionsGetAll(); - } catch (e) { - print(e); - } finally { - setState(() { - isLoading = false; - geoVisionCollections = refreshedCollections; - }); + Future redirectUser() async { + final instance = await getInstance(); + //user is connected + if (instance != null) { + _goToSequence(); + } else { + //user is disconnected + _createCollection(); } } + void _goToSequence() { + GetIt.instance() + .pushTo(Routes.newSequenceUpload, arguments: List.empty()); + } + Future _createCollection() async { if (!await PermissionHelper.isPermissionGranted()) { await PermissionHelper.askMissingPermission(); @@ -45,75 +39,10 @@ class _HomePageState extends State { .pushTo(Routes.newSequenceCapture, arguments: availableCameras)); } - Widget displayBody(isLoading) { - if (isLoading) { - return const LoaderIndicatorView(); - } else if (geoVisionCollections == null) { - return const UnknownErrorView(); - } else if (geoVisionCollections!.collections.isNotEmpty) { - return CollectionListView(collections: geoVisionCollections!.collections); - } else { - return const NoElementView(); - } - } - - @override - Widget build(BuildContext context) { - return RefreshIndicator( - displacement: 250, - strokeWidth: 3, - triggerMode: RefreshIndicatorTriggerMode.onEdge, - onRefresh: () async { - setState(() { - getCollections(); - }); - }, - child: Scaffold( - appBar: PanoramaxAppBar(context: context), - body: Column( - 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)), - ), - ), - Expanded( - child: displayBody(isLoading), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: _createCollection, - tooltip: AppLocalizations.of(context)!.createSequence_tooltip, - child: const Icon(Icons.add), - ), - ), - ); - } -} - -class CollectionListView extends StatelessWidget { - const CollectionListView({ - super.key, - required this.collections, - }); - - final List collections; - @override Widget build(BuildContext context) { - return ListView.builder( - itemCount: collections.length, - physics: - const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - itemBuilder: (BuildContext context, int index) { - return CollectionPreview(collections[index]); - }, - ); + return Scaffold( + appBar: PanoramaxAppBar(context: context), body: LoaderIndicatorView()); } } @@ -136,47 +65,3 @@ class LoaderIndicatorView extends StatelessWidget { ); } } - -class NoElementView extends StatelessWidget { - const NoElementView({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - AppLocalizations.of(context)!.emptyError, - style: GoogleFonts.nunito( - fontSize: 18, color: Colors.grey, fontWeight: FontWeight.w400), - ), - ) - ], - ); - } -} - -class UnknownErrorView extends StatelessWidget { - const UnknownErrorView({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - AppLocalizations.of(context)!.unknownError, - style: GoogleFonts.nunito( - fontSize: 20, color: Colors.red, fontWeight: FontWeight.w400), - ), - ) - ], - ); - } -} diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index 26745d2..4aaed47 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -20,7 +20,9 @@ class _UploadPicturesState extends State { super.initState(); isLoading = true; sequenceCount = widget.imgList.length; - uploadImages(); + if (sequenceCount > 0) { + uploadImages(); + } getMyCollections(); } From b6742889b79d48de676b95060bb59d0dd5895bd4 Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Fri, 23 Aug 2024 14:16:10 +0200 Subject: [PATCH 21/22] fix build errors --- lib/page/upload_pictures_page.dart | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/page/upload_pictures_page.dart b/lib/page/upload_pictures_page.dart index 4aaed47..6f281bf 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -176,3 +176,47 @@ class SequencesListView extends StatelessWidget { ); } } + +class NoElementView extends StatelessWidget { + const NoElementView({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + AppLocalizations.of(context)!.emptyError, + style: GoogleFonts.nunito( + fontSize: 18, color: Colors.grey, fontWeight: FontWeight.w400), + ), + ) + ], + ); + } +} + +class UnknownErrorView extends StatelessWidget { + const UnknownErrorView({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + AppLocalizations.of(context)!.unknownError, + style: GoogleFonts.nunito( + fontSize: 20, color: Colors.red, fontWeight: FontWeight.w400), + ), + ) + ], + ); + } +} From b8408e398edc8641f53d5aad92e062906f29fb9f Mon Sep 17 00:00:00 2001 From: Aline Bonnet Date: Fri, 23 Aug 2024 16:49:53 +0200 Subject: [PATCH 22/22] fix camera orientation by downgrade camera version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 65b72fb..56eae5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: google_fonts: ^6.2.1 flutter_localization: ^0.2.0 intl: any - camera: ^0.11.0+2 + camera: ^0.10.5+9 equatable: ^2.0.5 loading_animation_widget: ^1.2.1 flutter_launcher_icons: ^0.13.1