diff --git a/.gitignore b/.gitignore index ecb1544..44ceb4c 100644 --- a/.gitignore +++ b/.gitignore @@ -201,6 +201,8 @@ export_ipa/ # Avoid credentials private_keys/ +lib/src/api_keys.dart +ios/Runner/APIKey.plist # Avoid python virtual environment .venv/ diff --git a/README.md b/README.md index 9a54568..1a3ec46 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ocean_view +# CalCOFI OceanView An app that incentivizes ocean goers to report their wild observations. @@ -15,6 +15,12 @@ For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. +### API Keys +Files with API keys are stored [here](https://drive.google.com/drive/folders/1KAYjrNAFgREmylkoRMiw6AwLAyZ0gwQy?usp=sharing). Please follow these steps to set up them accordingly. +1. Vision API: `api_keys.dart` needs to be put in `lib/src/api_keys.dart`. +2. Google Map API for Android: Copy `MAP_API_KEYS` in `local.properties` and paste it in `/android/local.properties`. +3. Google Map API for iOS: `APIKey.plist` needs to be put in `/ios/Runner/APIKey.plist`. + ## Lint Code - Please run `dart format .` in the project root folder everytime before pushing commits. It will automatically lint the code to follow Dart guidelines. diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1f1dbac..2120642 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,13 +9,14 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B9B4F200D14CA0402720FA0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7C1174BF10AEC556EB94460 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 83F0EB9ACB7954BE38C9BC10 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3785899B6D83BF28E64390AE /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; AC83416F29ADE80F00E395B7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AC83416E29ADE80F00E395B7 /* GoogleService-Info.plist */; }; + AC9B2C612B4FC515009D168B /* APIKey.plist in Resources */ = {isa = PBXBuildFile; fileRef = AC9B2C602B4FC515009D168B /* APIKey.plist */; }; + AC9B2C632B4FC7B2009D168B /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9B2C622B4FC7B2009D168B /* KeyManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -38,7 +39,6 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 79F757806BA770C3C933E383 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 821ECB7493E4EDA0E715B203 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -49,6 +49,8 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AC83416E29ADE80F00E395B7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + AC9B2C602B4FC515009D168B /* APIKey.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = APIKey.plist; sourceTree = ""; }; + AC9B2C622B4FC7B2009D168B /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = ""; }; BCFFB43291E209C69588D559 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; C2468002F99F9AFC852AF161 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -115,6 +117,8 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, AC83416E29ADE80F00E395B7 /* GoogleService-Info.plist */, + AC9B2C602B4FC515009D168B /* APIKey.plist */, + AC9B2C622B4FC7B2009D168B /* KeyManager.swift */, ); path = Runner; sourceTree = ""; @@ -197,6 +201,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, AC83416F29ADE80F00E395B7 /* GoogleService-Info.plist in Resources */, + AC9B2C612B4FC515009D168B /* APIKey.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -300,6 +305,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + AC9B2C632B4FC7B2009D168B /* KeyManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index a613f60..3a0723a 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -10,8 +10,10 @@ import GoogleMaps ) -> Bool { GeneratedPluginRegistrant.register(with: self) - // TODO: Add your API key - GMSServices.provideAPIKey("AIzaSyBpDLenOUhz8cWFT0Oc7gWD4MneSUboyVc") + // Add Google Map API key + if let APIKEY = KeyManager().getValue(key: "mapApiKey") as? String { + GMSServices.provideAPIKey(APIKEY) + } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/KeyManager.swift b/ios/Runner/KeyManager.swift new file mode 100644 index 0000000..b7c6811 --- /dev/null +++ b/ios/Runner/KeyManager.swift @@ -0,0 +1,17 @@ +import Foundation +struct KeyManager { + private let keyFilePath = Bundle.main.path(forResource: "APIKey", ofType: "plist") + func getKeys() -> NSDictionary? { + guard let keyFilePath = keyFilePath else { + return nil + } + return NSDictionary(contentsOfFile: keyFilePath) + } + + func getValue(key: String) -> AnyObject? { + guard let keys = getKeys() else { + return nil + } + return keys[key]! as AnyObject + } +} diff --git a/lib/models/observation.dart b/lib/models/observation.dart index ccd9d7a..823e19e 100644 --- a/lib/models/observation.dart +++ b/lib/models/observation.dart @@ -17,6 +17,7 @@ class Observation { int? confidentiality; // Share with scientists / Keep private / now int int? confidence; // 1-3 can also be 0 for no confidence String? url; // Url of photo on Firebase, generated when uploading + String? imagePath; // image path on Firebase Storage, generated when uploading dynamic stopwatchStart; // Datetime of stopwatch start Observation( @@ -31,6 +32,7 @@ class Observation { this.confidence, this.status, this.url, + this.imagePath, this.stopwatchStart, this.confidentiality}); diff --git a/lib/screens/authenticate/register.dart b/lib/screens/authenticate/register.dart index fe08cb7..8e8fdd3 100644 --- a/lib/screens/authenticate/register.dart +++ b/lib/screens/authenticate/register.dart @@ -62,7 +62,7 @@ class _RegisterState extends State { @override Widget build(BuildContext context) { return loading - ? Loading() + ? Loading('Registering...') : Scaffold( backgroundColor: topBarColor, appBar: AppBar( diff --git a/lib/screens/authenticate/sign_in.dart b/lib/screens/authenticate/sign_in.dart index 1e399da..5bd76ce 100644 --- a/lib/screens/authenticate/sign_in.dart +++ b/lib/screens/authenticate/sign_in.dart @@ -28,7 +28,7 @@ class _SignInState extends State { @override Widget build(BuildContext context) { return loading - ? Loading() + ? Loading('Signing in...') : Scaffold( backgroundColor: topBarColor, appBar: AppBar( diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 99b51de..563a0cf 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -4,7 +4,7 @@ import 'package:ocean_view/models/userstats.dart'; import 'package:ocean_view/screens/map/map_page.dart'; import 'package:ocean_view/screens/upload/upload_page.dart'; -import 'package:ocean_view/screens/activity_page.dart'; +import 'package:ocean_view/screens/welcome_page.dart'; import 'package:ocean_view/screens/me/me_page.dart'; import 'package:ocean_view/screens/me/user_page.dart'; import 'package:ocean_view/services/auth.dart'; @@ -33,7 +33,7 @@ class _HomeState extends State { List _widgetOptions = [ MapPage(key: UniqueKey()), UploadPage(key: UniqueKey()), - ActivityPage(key: UniqueKey()), + WelcomePage(key: UniqueKey()), UserPage(key: UniqueKey()), MePage(key: UniqueKey()), ]; @@ -115,7 +115,7 @@ class _HomeState extends State { ), // This trailing comma makes auto-formatting nicer for build methods. ); } else { - return Loading(); + return Loading('Fetching user info...'); } }); } diff --git a/lib/screens/me/me_observation.dart b/lib/screens/me/me_observation.dart index dd168c5..bda98f7 100644 --- a/lib/screens/me/me_observation.dart +++ b/lib/screens/me/me_observation.dart @@ -124,16 +124,16 @@ class MeObservation extends StatelessWidget { String state = await DatabaseService(uid: user!.uid) .deleteObservation(this.observation); - if (state == 'Observation deleted') { + if (state == 'Unable to delete document') { + state = 'Unable to delete observation'; + } else { + state = 'Observation deleted'; uStats?.numobs = uStats.numobs! - 1; await DatabaseService(uid: user.uid) .updateUserStats(uStats as UserStats); - final snackBar = SnackBar(content: Text(state)); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - } else { - throw (state); } - + final snackBar = SnackBar(content: Text(state)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); // Back to previous page Navigator.pop(context, ['Delete']); }, diff --git a/lib/screens/observation_page.dart b/lib/screens/observation_page.dart index 53b421b..392dcbb 100644 --- a/lib/screens/observation_page.dart +++ b/lib/screens/observation_page.dart @@ -58,7 +58,7 @@ Widget _buildPopupDialog(BuildContext context, String wtitle, String msg) { ); } -// dialog is shown when name is empty and upload button is pressed +// dialog is shown when latin name is empty and upload button is pressed Widget _twoOptionsDialog(BuildContext context, String wtitle, String msg, _ObservationPageState mystate) { return new AlertDialog( @@ -129,6 +129,8 @@ class _ObservationPageState extends State { UserStats? uStats; DateTime? selectedDate; int index = 0; + bool _isButtonActive = + false; // upload button is not active when name field is empty and observation is uploading bool doSave = true; Future _loadMetaData() async { @@ -161,6 +163,10 @@ class _ObservationPageState extends State { this._nameController = (this.observation!.name != null) ? TextEditingController(text: this.observation!.name) : TextEditingController(text: ''); + this._nameController.addListener(() { + final isButtonActive = this._nameController.text.isNotEmpty; + setState(() => this._isButtonActive = isButtonActive); + }); this._latinNameController = (this.observation!.latinName != null) ? TextEditingController(text: this.observation!.latinName) : TextEditingController(text: ''); @@ -612,101 +618,111 @@ class _ObservationPageState extends State { const SizedBox(height: 10), ElevatedButton( child: Text(this.buttonName), - onPressed: () async { - this.doSave = true; - if (_nameController.text.isEmpty) { - this.doSave = false; - await showDialog( - context: context, - builder: (BuildContext context) => - _buildPopupDialog( - context, 'Name missing', snameHelp), - ); - } else if (_latinNameController.text.isEmpty) { - this.doSave = false; - await showDialog( - context: context, - builder: (BuildContext context) => - _twoOptionsDialog( - context, - 'Species Name missing', - speciesHelp, - this), - ); - } - if (this.doSave) { - this.observation!.name = _nameController.text; - this.observation!.latinName = - _latinNameController.text; - this.observation!.confidence = this._confidence; - if (this.mode == 'single') { - TaskState state = - await DatabaseService(uid: user.uid) - .addObservation(this.observation!, - File(widget.file.path)); - - if (state == TaskState.success) { - userSt.numobs = userSt.numobs! + 1; - await DatabaseService(uid: user.uid) - .updateUserStats(userSt); - final snackBar = - SnackBar(content: Text('Success')); - ScaffoldMessenger.of(context) - .showSnackBar(snackBar); - } else { - print( - 'Error from image repo ${state.toString()}'); - throw ('This file is not an image'); - } - - // Back to previous page - Navigator.pop(context); - } else if (this.mode == 'session') { - print( - 'Stopwatch: ${this.observation!.stopwatchStart}'); - print('Add'); - - // Add image to local directory - await LocalStoreService().saveImage(context, - File(_imageFile.path), '$index.png'); - - // Add observation to local directory - await LocalStoreService().saveObservation( - this.observation!, '$index.txt'); - - Navigator.pop( - context, [this.observation, this._image]); - } else if (this.mode == 'me') { - print('Update'); - String state = - await DatabaseService(uid: user.uid) - .updateObservation(this.observation!); - - if (state == "success") { - final snackBar = - SnackBar(content: Text('Success')); - ScaffoldMessenger.of(context) - .showSnackBar(snackBar); - } else { - print( - 'Error from image repo ${state.toString()}'); - throw ('This file is not an image'); + onPressed: _isButtonActive + ? () async { + // show prompt to user when name or latin name field is empty + this.doSave = true; + if (_nameController.text.isEmpty) { + this.doSave = false; + await showDialog( + context: context, + builder: (BuildContext context) => + _buildPopupDialog(context, + 'Name missing', snameHelp), + ); + } else if (_latinNameController + .text.isEmpty) { + this.doSave = false; + await showDialog( + context: context, + builder: (BuildContext context) => + _twoOptionsDialog( + context, + 'Species Name missing', + speciesHelp, + this), + ); + } + // continue uploading if name fields are not empty or user chooses not to fill latin name + if (this.doSave) { + setState(() => _isButtonActive = false); + this.observation!.name = + _nameController.text; + this.observation!.latinName = + _latinNameController.text; + this.observation!.confidence = + this._confidence; + if (this.mode == 'single') { + TaskState state = + await DatabaseService(uid: user.uid) + .addObservation(this.observation!, + File(widget.file.path)); + + if (state == TaskState.success) { + userSt.numobs = userSt.numobs! + 1; + await DatabaseService(uid: user.uid) + .updateUserStats(userSt); + final snackBar = + SnackBar(content: Text('Success')); + ScaffoldMessenger.of(context) + .showSnackBar(snackBar); + } else { + print( + 'Error from image repo ${state.toString()}'); + throw ('This file is not an image'); + } + + // Back to previous page + Navigator.pop(context); + } else if (this.mode == 'session') { + print( + 'Stopwatch: ${this.observation!.stopwatchStart}'); + print('Add'); + + // Add image to local directory + await LocalStoreService().saveImage( + context, + File(_imageFile.path), + '$index.png'); + + // Add observation to local directory + await LocalStoreService().saveObservation( + this.observation!, '$index.txt'); + + Navigator.pop(context, + [this.observation, this._image]); + } else if (this.mode == 'me') { + print('Update'); + String state = await DatabaseService( + uid: user.uid) + .updateObservation(this.observation!); + + if (state == 'success') { + final snackBar = + SnackBar(content: Text('Success')); + ScaffoldMessenger.of(context) + .showSnackBar(snackBar); + } else { + print( + 'Error from image repo ${state.toString()}'); + throw ('This file is not an image'); + } + + // Back to two previous pages + // Since previous page won't update the information, + // second previous page would fetch new observation + // from cloud and get updated information + int count = 0; + Navigator.of(context) + .popUntil((_) => count++ >= 2); + // Navigator.pop(context); + } + } else { + //Navigator.pop(context); + print('Do nothing'); + } } - - // Back to two previous pages - // Since previous page won't update the information, - // second previous page would fetch new observation - // from cloud and get updated information - int count = 0; - Navigator.of(context) - .popUntil((_) => count++ >= 2); - // Navigator.pop(context); - } - } else { - //Navigator.pop(context); - print('Do nothing'); - } - }), + : null), ], )), ], diff --git a/lib/screens/profile_page.dart b/lib/screens/profile_page.dart index 6c568a1..4027eda 100644 --- a/lib/screens/profile_page.dart +++ b/lib/screens/profile_page.dart @@ -126,7 +126,7 @@ class _sharingFormState extends State { ]), ); } else { - return Loading(); + return Loading('Fetching user info...'); } }); } diff --git a/lib/screens/upload/upload_classification.dart b/lib/screens/upload/upload_classification.dart index 4ead667..a0d004d 100644 --- a/lib/screens/upload/upload_classification.dart +++ b/lib/screens/upload/upload_classification.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import 'package:ocean_view/shared/loading.dart'; +import 'package:ocean_view/src/api_keys.dart'; import 'package:ocean_view/src/prediction.dart'; import 'package:path/path.dart' as path; @@ -28,10 +29,6 @@ class _UploadClassificationState extends State { late List _results; bool loading = true; - Map headers = { - 'x-rapidapi-key': '5b2f443d6cmsh9e04ef3014bde3dp176b6ajsnea7f884ff2e9', - 'x-rapidapi-host': 'visionapi.p.rapidapi.com' - }; String apiUrl = 'https://visionapi.p.rapidapi.com/v1/rapidapi/score_image'; // Send request to VisionAPI @@ -42,7 +39,7 @@ class _UploadClassificationState extends State { var request = new http.MultipartRequest('POST', uri); Map mapContent = {'content-type': 'mutipart/form-data'}; - request.headers.addAll(headers); + request.headers.addAll(VISION_API_HEADER); request.headers.addAll(mapContent); var multipartFile = new http.MultipartFile('image', stream, length, filename: path.basename(widget.imageFile.path), @@ -122,7 +119,7 @@ class _UploadClassificationState extends State { @override Widget build(BuildContext context) { return loading - ? Loading() + ? Loading('Searching...') : Scaffold( appBar: AppBar( leading: IconButton( diff --git a/lib/screens/activity_page.dart b/lib/screens/welcome_page.dart similarity index 93% rename from lib/screens/activity_page.dart rename to lib/screens/welcome_page.dart index c33e9e4..191e16d 100644 --- a/lib/screens/activity_page.dart +++ b/lib/screens/welcome_page.dart @@ -4,17 +4,17 @@ import 'package:ocean_view/shared/custom_widgets.dart'; import 'package:url_launcher/url_launcher_string.dart'; /* - Page for activity, not finished + Initial page when loggining in, it shows introduction of this app and CalCOFI */ -class ActivityPage extends StatefulWidget { - const ActivityPage({required Key key}) : super(key: key); +class WelcomePage extends StatefulWidget { + const WelcomePage({required Key key}) : super(key: key); @override - _ActivityPageState createState() => _ActivityPageState(); + _WelcomePageState createState() => _WelcomePageState(); } -class _ActivityPageState extends State { +class _WelcomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/services/database.dart b/lib/services/database.dart index a956b2c..541b883 100644 --- a/lib/services/database.dart +++ b/lib/services/database.dart @@ -61,6 +61,7 @@ class DatabaseService { obsMap['confidentiality'] = observation.confidentiality ?? CONFIDENTIALITY; obsMap['confidence'] = observation.confidence ?? CONFIDENCE; obsMap['url'] = observation.url ?? URL; + obsMap['imagePath'] = observation.imagePath ?? IMAGEPATH; obsMap['stopwatchStart'] = observation.stopwatchStart ?? STOPWATCHSTART; return obsMap; @@ -71,9 +72,7 @@ class DatabaseService { } // Upload image to Firebase Storage - Future> _uploadImage(io.File file) async { - String filePath = 'images/${uid}/${DateTime.now()}.png'; - + Future> _uploadImage(io.File file, String filePath) async { // Upload image to Storage TaskSnapshot snapshot = await _storage.ref().child(filePath).putFile(file); final String downloadUrl = await snapshot.ref.getDownloadURL(); @@ -84,11 +83,13 @@ class DatabaseService { // Add single new observation Future addObservation( Observation observation, io.File file) async { - List messages = await _uploadImage(file); + String imagePath = 'images/${uid}/${DateTime.now()}.png'; + List messages = await _uploadImage(file, imagePath); if (messages[0] == TaskState.success) { observation.uid = uid; observation.url = messages[1]; + observation.imagePath = imagePath; Map obsMap = _getMapFromObs(observation); @@ -111,13 +112,15 @@ class DatabaseService { // Loop over each observations for (int i = 0; i < observations.length; i++) { // Upload image to storage + String imagePath = 'images/${uid}/${DateTime.now()}.png'; file = await LocalStoreService().loadImage('$i.png'); - messages = await _uploadImage(file); + messages = await _uploadImage(file, imagePath); // Only upload observation if image is successfully uploaded if (messages[0] == TaskState.success) { observations[i].uid = uid; observations[i].url = messages[1]; + observations[i].imagePath = imagePath; documentReference = observationCollection.doc(); writeBatch.set(documentReference, _getMapFromObs(observations[i])); @@ -147,15 +150,24 @@ class DatabaseService { return state; } - // Delete observation + // Delete observation document and image Future deleteObservation(Observation observation) async { String state = 'Null'; + // delete document await observationCollection .doc(observation.documentID) .delete() - .then((value) => state = 'Observation deleted') - .catchError((error) => state = 'Unable to delete observation'); + .then((value) => state = 'Document deleted') + .catchError((error) => state = 'Unable to delete document'); + + // delete corresponding image + await _storage + .ref() + .child(observation.imagePath ?? IMAGEPATH) + .delete() + .then((value) => state = 'Image deleted') + .catchError((error) => state = 'Unable to delete image'); return state; } @@ -164,29 +176,29 @@ class DatabaseService { List _observationsFromSnapshots(QuerySnapshot snapshot) { return snapshot.docs.map((doc) { print(doc.get('time').seconds); + Map data = doc.data() as Map; return Observation( documentID: doc.id, - uid: doc.get('uid'), - name: doc.get('name'), - latinName: doc.get('latinName') ?? LATINNAME, - length: doc.get('length'), - weight: doc.get('weight'), - time: (doc.get('time') != null && doc.get('time') != TIME) - ? DateTime.fromMillisecondsSinceEpoch( - doc.get('time').seconds * 1000) + uid: data['uid'], + name: data['name'], + latinName: data['latinName'] ?? LATINNAME, + length: data['length'], + weight: data['weight'], + time: (data['time'] != null && data['time'] != TIME) + ? DateTime.fromMillisecondsSinceEpoch(data['time'].seconds * 1000) : TIME, - location: (doc.get('location') != null) - ? LatLng( - doc.get('location').latitude, doc.get('location').longitude) + location: (data['location'] != null) + ? LatLng(data['location'].latitude, data['location'].longitude) : LatLng(LATITUDE, LONGITUDE), - status: doc.get('status') ?? STATUS, - confidentiality: doc.get('confidentiality') ?? CONFIDENTIALITY, - confidence: doc.get('confidence') ?? CONFIDENCE, - url: doc.get('url'), - stopwatchStart: (doc.get('stopwatchStart') != null && - doc.get('stopwatchStart') != STOPWATCHSTART) + status: data['status'] ?? STATUS, + confidentiality: data['confidentiality'] ?? CONFIDENTIALITY, + confidence: data['confidence'] ?? CONFIDENCE, + url: data['url'] ?? URL, + imagePath: data['imagePath'] ?? IMAGEPATH, + stopwatchStart: (data['stopwatchStart'] != null && + data['stopwatchStart'] != STOPWATCHSTART) ? DateTime.fromMillisecondsSinceEpoch( - doc.get('stopwatchStart').seconds * 1000) + data['stopwatchStart'].seconds * 1000) : STOPWATCHSTART, ); }).toList(); diff --git a/lib/shared/constants.dart b/lib/shared/constants.dart index 8cc0a6c..db9398e 100644 --- a/lib/shared/constants.dart +++ b/lib/shared/constants.dart @@ -96,6 +96,7 @@ const double LONGITUDE = 0; const int CONFIDENTIALITY = 1; const int CONFIDENCE = 2; const String URL = 'None'; +const String IMAGEPATH = 'None'; const String STOPWATCHSTART = 'None'; Map CONFIDENCE_MAP = { 1: 'Low', diff --git a/lib/shared/loading.dart b/lib/shared/loading.dart index 981071d..0cdb392 100644 --- a/lib/shared/loading.dart +++ b/lib/shared/loading.dart @@ -8,7 +8,9 @@ import 'package:ocean_view/shared/constants.dart'; */ class Loading extends StatelessWidget { - const Loading({Key? key}) : super(key: key); + Loading(this.text); + + final String text; @override Widget build(BuildContext context) { @@ -25,12 +27,12 @@ class Loading extends StatelessWidget { centerTitle: true, ), body: Container( - //Center( - child: Stack( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ Center( child: Text( - 'Searching...', + text, textAlign: TextAlign.center, style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold), ), diff --git a/lib/src/aphia_parse.dart b/lib/src/aphia_parse.dart index f7b643a..ff895b3 100644 --- a/lib/src/aphia_parse.dart +++ b/lib/src/aphia_parse.dart @@ -29,8 +29,6 @@ class _AphiaParseDemoState extends State { var _vernacular = []; bool _loading = true; - //bool _loading = true; - @override void initState() { super.initState(); @@ -51,7 +49,7 @@ class _AphiaParseDemoState extends State { Widget build(BuildContext context) { print(widget.svalue); return _loading - ? Loading() + ? Loading('Searching...') : Scaffold( appBar: AppBar( title: