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/component/sequence_card.dart b/lib/component/sequence_card.dart new file mode 100644 index 0000000..e33a793 --- /dev/null +++ b/lib/component/sequence_card.dart @@ -0,0 +1,330 @@ +part of panoramax; + +class SequenceCard extends StatefulWidget { + 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(); +} + +class _SequenceCardState extends State { + late int itemCount; + GeoVisioCollectionImportStatus? geovisioStatus; + Timer? timer; + SequenceState sequenceState = SequenceState.SENDING; + MemoryImage? image; + + @override + void initState() { + super.initState(); + itemCount = widget.sequence.stats_items!.count; + checkSequenceState(); + 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( + (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(); + 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; + checkSequenceState(); + }); + } + } + + 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"); + } + } + + 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 + ? Container() + : 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), + ), + 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() { + return Container( + margin: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Column( + children: [ + PictureCount(), + Shooting(), + sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN + ? Publishing() + : Container(), + sequenceState == SequenceState.BLURRING ? Blurring() : Container() + ], + )); + } + + Widget PictureCount() { + final count = + widget.sequenceCount == null ? itemCount : widget.sequenceCount; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Text( + '$count ${AppLocalizations.of(context)!.pictures}', + style: GoogleFonts.nunito( + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ]), + sequenceState == SequenceState.READY || + sequenceState == SequenceState.HIDDEN + ? FloatingActionButton( + onPressed: shareUrl, + 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(); + 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))) + ], + ); + } + + 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: [ + sequenceState == SequenceState.BLURRING + ? 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(sequenceState == SequenceState.BLURRING + ? AppLocalizations.of(context)!.sendingCompleted + : AppLocalizations.of(context)!.sendingInProgress) + ])))); + } + + Widget Picture() { + 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 + ), + ), + ), + 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() { + 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)) + ], + ) + ], + )); + } +} + +enum SequenceState { SENDING, BLURRING, READY, DELETED, HIDDEN } 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/l10n/app_en.arb b/lib/l10n/app_en.arb index 1a6428a..0cf1ecf 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8,6 +8,16 @@ "emptyError": "No element found", "loading": "Loading...", + "mySequences": "My sequence", + "pictures": "picture(s)", + "shooting": "Shooting", + "sendingInProgress": "Sending in progress", + "sendingCompleted": "Sending completed", + "publishing": "Publishing", + "blurringInProgress": "Blurring in progress", + "share": "Share", + "hidden": "Hidden sequence", + "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..a68fa96 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -8,6 +8,16 @@ "emptyError": "Aucun élément trouvé", "loading": "Chargement...", + "mySequences": "Mes séquences", + "pictures": "photo(s)", + "shooting": "Prise de vue", + "sendingInProgress": "Envoi en cours", + "sendingCompleted": "Envoi terminé", + "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", "noCameraFoundError": "Pas de caméra trouvée pour cet appareil", diff --git a/lib/main.dart b/lib/main.dart index 571e741..a7accb8 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'; @@ -24,6 +25,8 @@ 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 'package:share/share.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'component/loader.dart'; import 'service/api/api.dart'; @@ -31,6 +34,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'; @@ -39,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/homepage.dart b/lib/page/homepage.dart index 07f5c7c..f2485e4 100644 --- a/lib/page/homepage.dart +++ b/lib/page/homepage.dart @@ -8,83 +8,26 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - late bool isLoading; - GeoVisioCollections? geoVisionCollections; - - late StreamSubscription _intentSub; - List? _sharedFiles; - @override void initState() { super.initState(); - isLoading = true; - getCollections(); - listenSendingIntent(); - } - - @override - void dispose() { - _intentSub.cancel(); - super.dispose(); - } - - void listenSendingIntent() { - // Listen to media sharing coming from outside the app while the app is in the memory. - _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen((value) { - _sharedFiles = value; - if (_sharedFiles != null && _sharedFiles!.isNotEmpty) { - final fileList = sharedFilesToImages(_sharedFiles!); - GetIt.instance() - .pushTo(Routes.newSequenceSend, arguments: fileList); - } - }, onError: (err) { - print("getIntentDataStream error: $err"); - }); - - // Get the media sharing coming from outside the app while the app is closed. - ReceiveSharingIntent.instance.getInitialMedia().then((value) { - _sharedFiles = value; - // Tell the library that we are done processing the intent. - ReceiveSharingIntent.instance.reset(); - if (_sharedFiles != null && _sharedFiles!.isNotEmpty) { - final fileList = sharedFilesToImages(_sharedFiles!); - GetIt.instance() - .pushTo(Routes.newSequenceSend, arguments: fileList); - } - }); - } - - List sharedFilesToImages(List list) { - return list - .where((element) => isImage(element)) - .map((item) => File(item.path)) - .toList(); + redirectUser(); } - bool isImage(SharedMediaFile file) { - final fileExtension = file.path.split('.').last.toLowerCase(); - return (fileExtension == 'jpg' || - fileExtension == 'jpeg' || - fileExtension == 'png' || - fileExtension == 'gif' || - fileExtension == 'bmp'); + Future redirectUser() async { + final instance = await getInstance(); + //user is connected + if (instance != null) { + _goToSequence(); + } else { + //user is disconnected + _createCollection(); + } } - Future getCollections() async { - GeoVisioCollections? refreshedCollections; - setState(() { - isLoading = true; - }); - try { - refreshedCollections = await CollectionsApi.INSTANCE.apiCollectionsGet(); - } catch (e) { - print(e); - } finally { - setState(() { - isLoading = false; - geoVisionCollections = refreshedCollections; - }); - } + void _goToSequence() { + GetIt.instance() + .pushTo(Routes.newSequenceUpload, arguments: List.empty()); } Future _createCollection() async { @@ -96,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()); } } @@ -187,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/instance_page.dart b/lib/page/instance_page.dart index a4bb10f..64df6a3 100644 --- a/lib/page/instance_page.dart +++ b/lib/page/instance_page.dart @@ -17,32 +17,40 @@ 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; }); } - void getToken() async { + void getJWTToken() async { + final instance = await getInstance(); final cookies = - await cookieManager.getCookies('https://panoramax.$API_HOSTNAME.fr'); + await cookieManager.getCookies('https://panoramax.$instance.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) async { + final token = await getToken(); + if (instance != null && token != null) { + GetIt.instance() + .pushTo(Routes.newSequenceUpload, arguments: widget.imgList); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -54,12 +62,17 @@ class _InstanceState extends State { controller: WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( - onNavigationRequest: (request) { - if (request.url == - "https://panoramax.$API_HOSTNAME.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..6f281bf 100644 --- a/lib/page/upload_pictures_page.dart +++ b/lib/page/upload_pictures_page.dart @@ -10,59 +10,79 @@ class UploadPicturesPage extends StatefulWidget { } class _UploadPicturesState extends State { - int _uploadedImagesCount = 0; - bool _isUploading = false; - String? _errorMessage; + late bool isLoading; + GeoVisioCollection? sequences; + late int sequenceCount; + String? collectionId; @override void initState() { super.initState(); - uploadImages(); + isLoading = true; + sequenceCount = widget.imgList.length; + if (sequenceCount > 0) { + uploadImages(); + } + getMyCollections(); } - Future uploadImages() async { + Future getMyCollections() async { + GeoVisioCollection? 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.getMeCollection(); } catch (e) { - setState(() { - _errorMessage = e.toString(); - _isUploading = false; - }); + print(e); } finally { setState(() { - _isUploading = false; + sequences = refreshedSequences; + isLoading = false; }); } } - Future createCollection() async { + 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, + collectionId: collectionId, + lastSequenceCount: sequenceCount); + } else { + return const NoElementView(); + } + } + + Future uploadImages() async { + await createCollection(); + await sendPictures(); + } + + 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; + 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], ); @@ -70,6 +90,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)); @@ -77,39 +100,123 @@ 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 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), ), ], ), - ), + ))); + } +} + +class SequencesListView extends StatelessWidget { + 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) { + return ListView.builder( + itemCount: links.length, + physics: + const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + itemBuilder: (BuildContext context, int index) { + if (links[index].rel == "child") { + return SequenceCard(links[index], + sequenceCount: + links[index].id == collectionId ? lastSequenceCount : null); + } else { + return Container(); + } + }, + ); + } +} + +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/service/api/api.dart b/lib/service/api/api.dart index 441c79c..d06c981 100644 --- a/lib/service/api/api.dart +++ b/lib/service/api/api.dart @@ -1,14 +1,13 @@ library panoramax.api; import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:panoramax_mobile/main.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..db56f41 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,8 +32,9 @@ class CollectionsApi { } // create path and map variables - var url = Uri.https( - "panoramax.$API_HOSTNAME.fr", '/api/collections', queryParams); + final instance = await 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' @@ -94,4 +95,98 @@ class CollectionsApi { throw new Exception('${response.statusCode} - ${response.reasonPhrase}'); } } + + 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'); + + 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}'); + } + } + + 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}'); + } + } } diff --git a/lib/service/api/model/geo_visio.dart b/lib/service/api/model/geo_visio.dart index 5637e31..e6f26f6 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,21 +7,54 @@ 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; + GeoVisioExtent? extent; factory GeoVisioLink.fromJson(Map json) => _$GeoVisioLinkFromJson(json); Map toJson() => _$GeoVisioLinkToJson(this); + String? getThumbUrl() { + return '$href/thumb.jpg'; + } + 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; @@ -86,4 +121,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..10056bd 100644 --- a/lib/service/api/model/geo_visio.g.dart +++ b/lib/service/api/model/geo_visio.g.dart @@ -8,13 +8,22 @@ 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) + ..extent = json['extent'] == null + ? null + : GeoVisioExtent.fromJson(json['extent'] as Map); Map _$GeoVisioLinkToJson(GeoVisioLink instance) => { @@ -26,6 +35,33 @@ 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, + '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) => @@ -45,7 +81,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 +148,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, + }; 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); +} diff --git a/pubspec.yaml b/pubspec.yaml index dcfb29f..c60ceb2 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 @@ -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 + share: ^2.0.4 android_intent: ^2.0.2 receive_sharing_intent: ^1.8.0