diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies new file mode 100644 index 0000000..762f47e --- /dev/null +++ b/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"cloud_firestore","dependencies":["firebase_core"]},{"name":"firebase_auth","dependencies":["firebase_core"]},{"name":"firebase_core","dependencies":[]},{"name":"firebase_storage","dependencies":["firebase_core"]},{"name":"image_picker","dependencies":[]},{"name":"path_provider","dependencies":[]}]} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 87062d4..308cfd8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -34,8 +34,9 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.simple_firebase_auth" - minSdkVersion 16 + minSdkVersion 17 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -59,3 +60,5 @@ dependencies { androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } + +apply plugin: 'com.google.gms.google-services' // Google Play services Gradle plugin diff --git a/android/build.gradle b/android/build.gradle index bb8a303..054d765 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,6 +6,8 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.google.gms:google-services:3.2.1' // Google Services plugin + } } diff --git a/android/gradle.properties b/android/gradle.properties index 8bd86f6..4d3226a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1 +1,3 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/ios/Flutter/Flutter.podspec b/ios/Flutter/Flutter.podspec new file mode 100644 index 0000000..5ca3041 --- /dev/null +++ b/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# NOTE: This podspec is NOT to be published. It is only used as a local source! +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'High-performance, high-fidelity mobile apps.' + s.description = <<-DESC +Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. + DESC + s.homepage = 'https://flutter.io' + s.license = { :type => 'MIT' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '8.0' + s.vendored_frameworks = 'Flutter.framework' +end diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 0000000..9fd2e6b --- /dev/null +++ b/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/aaronksaunders/dev/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/aaronksaunders/dev/projects/flutter/simple_firebase_auth" +export "FLUTTER_TARGET=/Users/aaronksaunders/dev/projects/flutter/simple_firebase_auth/lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "SYMROOT=${SOURCE_ROOT}/../build/ios" +export "FLUTTER_FRAMEWORK_DIR=/Users/aaronksaunders/dev/flutter/bin/cache/artifacts/engine/ios" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "TRACK_WIDGET_CREATION=true" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e1e8026..e6b13ee 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,17 +9,20 @@ PODS: - Firebase/Core - Firebase/Firestore (~> 6.0) - Flutter - - Firebase/Auth (6.1.0): + - Firebase/Auth (6.3.0): - Firebase/CoreOnly - - FirebaseAuth (~> 6.1.0) - - Firebase/Core (6.1.0): + - FirebaseAuth (~> 6.1.2) + - Firebase/Core (6.3.0): - Firebase/CoreOnly - - FirebaseAnalytics (= 6.0.1) - - Firebase/CoreOnly (6.1.0): - - FirebaseCore (= 6.0.1) - - Firebase/Firestore (6.1.0): + - FirebaseAnalytics (= 6.0.2) + - Firebase/CoreOnly (6.3.0): + - FirebaseCore (= 6.0.3) + - Firebase/Firestore (6.3.0): - Firebase/CoreOnly - - FirebaseFirestore (~> 1.3.1) + - FirebaseFirestore (~> 1.4.0) + - Firebase/Storage (6.3.0): + - Firebase/CoreOnly + - FirebaseStorage (~> 3.2.1) - firebase_auth (0.0.1): - Firebase/Auth (~> 6.0) - Firebase/Core @@ -27,102 +30,116 @@ PODS: - firebase_core (0.0.1): - Firebase/Core - Flutter - - FirebaseAnalytics (6.0.1): + - firebase_storage (0.0.1): + - Firebase/Storage + - Flutter + - FirebaseAnalytics (6.0.2): - FirebaseCore (~> 6.0) - - FirebaseInstanceID (~> 4.1) - - GoogleAppMeasurement (= 6.0.1) + - FirebaseInstanceID (~> 4.2) + - GoogleAppMeasurement (= 6.0.2) - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - GoogleUtilities/MethodSwizzler (~> 6.0) - GoogleUtilities/Network (~> 6.0) - "GoogleUtilities/NSData+zlib (~> 6.0)" - nanopb (~> 0.3) - - FirebaseAuth (6.1.0): + - FirebaseAuth (6.1.2): - FirebaseAuthInterop (~> 1.0) - FirebaseCore (~> 6.0) - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/Environment (~> 6.0) + - GoogleUtilities/AppDelegateSwizzler (~> 6.2) + - GoogleUtilities/Environment (~> 6.2) - GTMSessionFetcher/Core (~> 1.1) - FirebaseAuthInterop (1.0.0) - - FirebaseCore (6.0.1): + - FirebaseCore (6.0.3): - GoogleUtilities/Environment (~> 6.0) - GoogleUtilities/Logger (~> 6.0) - - FirebaseFirestore (1.3.1): + - FirebaseFirestore (1.4.0): - FirebaseAuthInterop (~> 1.0) - FirebaseCore (~> 6.0) - - FirebaseFirestore/abseil-cpp (= 1.3.1) - - "gRPC-C++ (= 0.0.8)" + - FirebaseFirestore/abseil-cpp (= 1.4.0) + - "gRPC-C++ (= 0.0.9)" - leveldb-library (~> 1.20) - nanopb (~> 0.3.901) - Protobuf (~> 3.1) - - FirebaseFirestore/abseil-cpp (1.3.1): + - FirebaseFirestore/abseil-cpp (1.4.0): - FirebaseAuthInterop (~> 1.0) - FirebaseCore (~> 6.0) - - "gRPC-C++ (= 0.0.8)" + - "gRPC-C++ (= 0.0.9)" - leveldb-library (~> 1.20) - nanopb (~> 0.3.901) - Protobuf (~> 3.1) - - FirebaseInstanceID (4.1.0): + - FirebaseInstanceID (4.2.0): - FirebaseCore (~> 6.0) - GoogleUtilities/Environment (~> 6.0) - GoogleUtilities/UserDefaults (~> 6.0) + - FirebaseStorage (3.2.1): + - FirebaseAuthInterop (~> 1.0) + - FirebaseCore (~> 6.0) + - GTMSessionFetcher/Core (~> 1.1) - Flutter (1.0.0) - - GoogleAppMeasurement (6.0.1): + - GoogleAppMeasurement (6.0.2): - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - GoogleUtilities/MethodSwizzler (~> 6.0) - GoogleUtilities/Network (~> 6.0) - "GoogleUtilities/NSData+zlib (~> 6.0)" - nanopb (~> 0.3) - - GoogleUtilities/AppDelegateSwizzler (6.2.0): + - GoogleUtilities/AppDelegateSwizzler (6.2.1): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (6.2.0) - - GoogleUtilities/Logger (6.2.0): + - GoogleUtilities/Environment (6.2.1) + - GoogleUtilities/Logger (6.2.1): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (6.2.0): + - GoogleUtilities/MethodSwizzler (6.2.1): - GoogleUtilities/Logger - - GoogleUtilities/Network (6.2.0): + - GoogleUtilities/Network (6.2.1): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (6.2.0)" - - GoogleUtilities/Reachability (6.2.0): + - "GoogleUtilities/NSData+zlib (6.2.1)" + - GoogleUtilities/Reachability (6.2.1): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (6.2.0): + - GoogleUtilities/UserDefaults (6.2.1): - GoogleUtilities/Logger - - "gRPC-C++ (0.0.8)": - - "gRPC-C++/Implementation (= 0.0.8)" - - "gRPC-C++/Interface (= 0.0.8)" - - "gRPC-C++/Implementation (0.0.8)": - - "gRPC-C++/Interface (= 0.0.8)" - - gRPC-Core (= 1.19.0) + - "gRPC-C++ (0.0.9)": + - "gRPC-C++/Implementation (= 0.0.9)" + - "gRPC-C++/Interface (= 0.0.9)" + - "gRPC-C++/Implementation (0.0.9)": + - "gRPC-C++/Interface (= 0.0.9)" + - gRPC-Core (= 1.21.0) - nanopb (~> 0.3) - - "gRPC-C++/Interface (0.0.8)" - - gRPC-Core (1.19.0): - - gRPC-Core/Implementation (= 1.19.0) - - gRPC-Core/Interface (= 1.19.0) - - gRPC-Core/Implementation (1.19.0): + - "gRPC-C++/Interface (0.0.9)" + - gRPC-Core (1.21.0): + - gRPC-Core/Implementation (= 1.21.0) + - gRPC-Core/Interface (= 1.21.0) + - gRPC-Core/Implementation (1.21.0): - BoringSSL-GRPC (= 0.0.3) - - gRPC-Core/Interface (= 1.19.0) + - gRPC-Core/Interface (= 1.21.0) - nanopb (~> 0.3) - - gRPC-Core/Interface (1.19.0) + - gRPC-Core/Interface (1.21.0) - GTMSessionFetcher/Core (1.2.2) + - image_picker (0.0.1): + - Flutter - leveldb-library (1.20) - nanopb (0.3.901): - nanopb/decode (= 0.3.901) - nanopb/encode (= 0.3.901) - nanopb/decode (0.3.901) - nanopb/encode (0.3.901) + - path_provider (0.0.1): + - Flutter - Protobuf (3.8.0) DEPENDENCIES: - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_storage (from `.symlinks/plugins/firebase_storage/ios`) - Flutter (from `.symlinks/flutter/ios`) + - image_picker (from `.symlinks/plugins/image_picker/ios`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) SPEC REPOS: - https://github.com/cocoapods/specs.git: + https://github.com/CocoaPods/Specs.git: - BoringSSL-GRPC - Firebase - FirebaseAnalytics @@ -131,6 +148,7 @@ SPEC REPOS: - FirebaseCore - FirebaseFirestore - FirebaseInstanceID + - FirebaseStorage - GoogleAppMeasurement - GoogleUtilities - "gRPC-C++" @@ -147,31 +165,41 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_auth/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" + firebase_storage: + :path: ".symlinks/plugins/firebase_storage/ios" Flutter: :path: ".symlinks/flutter/ios" + image_picker: + :path: ".symlinks/plugins/image_picker/ios" + path_provider: + :path: ".symlinks/plugins/path_provider/ios" SPEC CHECKSUMS: BoringSSL-GRPC: db8764df3204ccea016e1c8dd15d9a9ad63ff318 - cloud_firestore: c6f34148c1dfbb57a6147f9bb49c1c9f5c27036e - Firebase: 8d77bb33624ae9b62d745d82ec023de5f70f7e4f - firebase_auth: 7a2cc520766f90212b845a5b35316601707bfa25 - firebase_core: ce5006bb48508ee4e71e0f429a3f519bb8ee2961 - FirebaseAnalytics: 629301c2b9925f3537d4093a17a72751ae5b7084 - FirebaseAuth: 4281d6d98a90881e00710fa8d9e37c2ccdfc7d80 + cloud_firestore: bea98f51ff83b368d2984d3a6b9a6ac8dd89bf63 + Firebase: 8432d732974498afd5987e9001a05f90f1a3d625 + firebase_auth: ba238762663f6b3503cc5468c50d652df1306185 + firebase_core: 44f31f51532812598f0f9f4af0caa705b9edfc19 + firebase_storage: 96665eb4cf0a68e51ac8568f13d7841c3ed881bd + FirebaseAnalytics: 470ddab7253b21ad5a40bebd4a9903d7ae19386a + FirebaseAuth: 7fa2cc010b93770831a4e7a60651b52c3b8d111a FirebaseAuthInterop: 0ffa57668be100582bb7643d4fcb7615496c41fc - FirebaseCore: 66bdef3b310a026880e2a5bc8aa586ab62ce4543 - FirebaseFirestore: 8feac73d330bf7ed6945518e5ffc0aeb164a07f6 - FirebaseInstanceID: 27bed93a59b6685f5c3e0c028a878a764fd75c33 - Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a - GoogleAppMeasurement: 51d8d9ea48f0ca44484d29cfbdef976fbd4fc336 - GoogleUtilities: 996e0db07153674fd1b54b220fda3a3dc3547cba - "gRPC-C++": 98be881723177d8c4faf5fdffacb582c7b4971a2 - gRPC-Core: bd9472c8daa2e414b9f8038ba667bf56ce0e02b8 + FirebaseCore: 68f8a7f50cdae542715d4e86afa37c4067217dcb + FirebaseFirestore: 75768d91aad491eccdc02202b8161cff9872300e + FirebaseInstanceID: f20243a1d828e0e9a3798b995174dedc16f1b32a + FirebaseStorage: 926d41552072b9fee67aa645760f05f87b7ce604 + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + GoogleAppMeasurement: a35a645835bae31b6bdc0576396bc23908f12a22 + GoogleUtilities: c7a0b08bda3bf808be823ed151f0e28ac6866e71 + "gRPC-C++": 9dfe7b44821e7b3e44aacad2af29d2c21f7cde83 + gRPC-Core: c9aef9a261a1247e881b18059b84d597293c9947 GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23 + image_picker: 16e5fec1fbc87fd3b297c53e4048521eaf17cd06 leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48 + path_provider: f96fff6166a8867510d2c25fdcc346327cc4b259 Protobuf: 3f617b9a6e73605565086864c9bc26b2bf2dd5a3 PODFILE CHECKSUM: aff02bfeed411c636180d6812254b2daeea14d09 -COCOAPODS: 1.6.0 +COCOAPODS: 1.8.4 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5678597..d7d2511 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -232,17 +232,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/gRPC-C++/gRPCCertificates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/gRPC-C++/gRPCCertificates-Cpp.bundle", ); name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - ); outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Cpp.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -268,15 +264,11 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4beeb56..fc14ca6 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,12 @@ + NSPhotoLibraryUsageDescription + This app requires access to the photo library. + NSMicrophoneUsageDescription + This app does not require access to the microphone. + NSCameraUsageDescription + This app requires access to the camera. CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/lib/ImageService.dart b/lib/ImageService.dart new file mode 100644 index 0000000..9d2bfbc --- /dev/null +++ b/lib/ImageService.dart @@ -0,0 +1,115 @@ +import 'dart:typed_data'; + +import 'package:flutter/cupertino.dart'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:image/image.dart' as ImagePlugin; +import 'package:path_provider/path_provider.dart'; + +class ImageService with ChangeNotifier { + Stream get imageDataStream { + return Firestore.instance.collection('Images').snapshots(); + } + + Future createThumbFileFromImageFile( + File _imageFile, + ) async { + // load file into memory so we can resize it for the thumb + var image = + ImagePlugin.decodeImage(File(_imageFile.path).readAsBytesSync()); + // resize into thumb + var thumbnail = ImagePlugin.copyResize(image, width: 240); + + // create file path to save thumbnail + var docPath = (await getApplicationDocumentsDirectory()).path; + var tstamp = DateTime.now().millisecondsSinceEpoch.toString(); + var fileName = '$docPath/thumbnail-test-$tstamp.png'; + + // save the thumbnail + var f = new File(fileName); + + f.writeAsBytesSync(ImagePlugin.encodePng(thumbnail)); + return Future.value(f); + } + + /// + /// [ _imageInfo ] this is a map containing two file objects the file + /// to upload and the thumbnail version of the file which can be + /// used for rendering in a list + /// + /// [ _progress ] is a function call back that return the `StorageTaskSnapshot` + /// object from the Firebase task; this can be used for providing and + /// update on the upload process + /// + Future uploadTheFile(Map _imageInfo, + Function _updateCallback(StorageTaskSnapshot _progress)) async { + var downloadURL; + var value; + + // get current user + var currentUser = await FirebaseAuth.instance.currentUser(); + var uniqueStr = + DateTime.now().millisecondsSinceEpoch.toString() + currentUser.uid; + + // convert thumb to base64 string to save with info for faster + // screen updates + List imageBytes = await _imageInfo['imageThumb'].readAsBytes(); + String base64Image = base64Encode(imageBytes); + + // upload to storage image to storage + final StorageReference ref = FirebaseStorage.instance + .ref() + .child('images-' + currentUser.uid) + .child(uniqueStr); + + final StorageUploadTask uploadTask = ref.putFile( + _imageInfo['image'], + StorageMetadata( + contentLanguage: 'en', + //customMetadata: {'activity': 'test'}, + ), + ); + + uploadTask.events.listen((_event) async { + print(_event); + if (_event.type == StorageTaskEventType.progress) { + print( + _event.snapshot.bytesTransferred / _event.snapshot.totalByteCount); + _updateCallback(_event.snapshot); + } else if (_event.type == StorageTaskEventType.success) { + print(_event.snapshot.ref); + downloadURL = await _event.snapshot.ref.getDownloadURL(); + + // upload to image collection + value = await Firestore.instance.collection('Images').add({ + 'image': downloadURL, + 'thumb': base64Image, + 'owner_id': currentUser.uid, + 'owner': currentUser.email, + 'subject': "test image upload " + uniqueStr + }); + } else if (_event.type == StorageTaskEventType.failure) { + print(_event.snapshot.error); + } + }); + + return await uploadTask.onComplete; + } + + makeThumbFromDoc(DocumentSnapshot document) { + if (document['thumb'] != null) { + Uint8List thumbBytes = base64Decode(document['thumb']); + return SizedBox( + width: 100.0, height: 100.0, child: Image.memory(thumbBytes)); + } else { + return SizedBox( + width: 100.0, + height: 100.0, + ); + } + } +} diff --git a/lib/auth.dart b/lib/auth.dart index 96375f0..38da0e8 100644 --- a/lib/auth.dart +++ b/lib/auth.dart @@ -46,7 +46,7 @@ class AuthService with ChangeNotifier { // since something changed, let's notify the listeners... notifyListeners(); return result; - } catch (e) { + } catch (e) { throw new AuthException(e.code, e.message); } } diff --git a/lib/components/FileUploadButtonBar.dart b/lib/components/FileUploadButtonBar.dart new file mode 100644 index 0000000..16e9938 --- /dev/null +++ b/lib/components/FileUploadButtonBar.dart @@ -0,0 +1,59 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +import 'package:image_picker/image_picker.dart'; +// import 'package:image/image.dart' as ImagePlugin; +// import 'package:path_provider/path_provider.dart'; +import 'package:simple_firebase_auth/ImageService.dart'; + +class FileUploadButtonBar extends StatelessWidget { + const FileUploadButtonBar({ + Key key, + @required this.onChangeFile, + @required this.imageFile, + @required this.onUploadFile, + }) : super(key: key); + + final void Function(Map imageInfo) onChangeFile; + final void Function() onUploadFile; + final File imageFile; + + @override + Widget build(BuildContext context) { + return ButtonBar(mainAxisSize: MainAxisSize.min, children: [ + RaisedButton( + child: Icon(Icons.camera_alt), + onPressed: () async { + // pick the image + var pickedImage = + await ImagePicker.pickImage(source: ImageSource.gallery); + if (pickedImage == null) return; + + final thumbFile = + await ImageService().createThumbFileFromImageFile(pickedImage); + + // delete any old thumb... + if (imageFile != null) await imageFile.delete(); + + // return the information to parent + onChangeFile({'thumb': thumbFile, 'file': File(pickedImage.path)}); + }), + RaisedButton( + child: Icon(Icons.file_upload), + onPressed: imageFile != null + ? () async { + print("upload file"); + onUploadFile(); + } + : null), + RaisedButton( + child: Icon(Icons.clear), + onPressed: imageFile != null + ? () async { + await imageFile.delete(); + onChangeFile({'thumb': null, 'file': null}); + } + : null), + ]); + } +} diff --git a/lib/components/ImageList.dart b/lib/components/ImageList.dart new file mode 100644 index 0000000..449ca3c --- /dev/null +++ b/lib/components/ImageList.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +import 'package:simple_firebase_auth/ImageService.dart'; + +class ImageList extends StatefulWidget { + const ImageList({ + Key key, + @required this.onItemClick, + @required this.listStream, + }) : super(key: key); + + final void Function(String, String) onItemClick; + final Stream listStream; + + @override + _ImageListState createState() => _ImageListState(); +} + +class _ImageListState extends State { + @override + Widget build(BuildContext context) { + print("build - ImageList"); + + return StreamBuilder( + stream: widget.listStream, + builder: (context, AsyncSnapshot snapshot) { + print(snapshot.connectionState); + + // display any errors... + if (snapshot.hasError) { + return new Text('Error: ${snapshot.error}'); + } + // wait for a response from the stream + if (snapshot.hasData) { + print(snapshot.connectionState); + print(snapshot.hasData); + final documents = snapshot.data.documents; + // if no data, display message + if (documents.length == 0) { + return Text('No Items Retrieved'); + } + // if data the create the list items + else { + return ListView( + shrinkWrap: false, + children: documents.map((DocumentSnapshot document) { + return new ImageListItem( + document: document, + widget: widget, + ); + }).toList()); + } + } + + if (snapshot.connectionState != ConnectionState.done) { + return Align( + widthFactor: 200.0, + alignment: Alignment.topCenter, + child: Container( + width: 60.0, + height: 60.0, + child: CircularProgressIndicator()), + ); + } + }); + } +} + +class ImageListItem extends StatelessWidget { + const ImageListItem({ + Key key, + @required this.document, + @required this.widget, + }) : super(key: key); + + final document; + final ImageList widget; + + @override + Widget build(BuildContext context) { + final thumb = ImageService().makeThumbFromDoc(document); + final heroTag = 'imageHero-${document.documentID}'; + + return new ListTile( + key: Key(document.documentID), + title: new Text(document['subject']), + subtitle: new Text(document['owner']), + leading: Hero(tag: heroTag, child: thumb), + onTap: () { + widget.onItemClick(document['image'], heroTag); + }, + ); + } +} + +// data model +class ImageItem { + String subject; + String owner; + String id; + + ImageItem({this.subject, this.owner, this.id}); +} diff --git a/lib/home_page.dart b/lib/home_page.dart index ec8b3b2..872d694 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -1,35 +1,54 @@ +import 'dart:io'; + +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'ImageService.dart'; import 'auth.dart'; +import 'components/FileUploadButtonBar.dart'; +import 'components/ImageList.dart'; class HomePage extends StatefulWidget { final FirebaseUser currentUser; + Stream courseDocStream; - HomePage(this.currentUser); + HomePage({ + Key key, + @required this.currentUser, + }) : super(key: key); @override _HomePageState createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends State + with AutomaticKeepAliveClientMixin { + @override + void initState() { + super.initState(); + widget.courseDocStream = ImageService().imageDataStream; + print("initState - home_page"); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Home Flutter Firebase"), - //actions: [LogoutButton()], + actions: [ + IconButton( + icon: Icon(Icons.exit_to_app), + onPressed: () async { + await Provider.of(context).logout(); + }) + ], ), body: Center( child: Column( children: [ - SizedBox(height: 20.0), - Text( - 'Home Page Flutter Firebase Content', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - SizedBox(height: 20.0), + SizedBox(height: 10.0), Text( 'Welcome ${widget.currentUser.email}', style: TextStyle( @@ -37,17 +56,112 @@ class _HomePageState extends State { fontWeight: FontWeight.bold, fontStyle: FontStyle.italic), ), - SizedBox(height: 20.0), - RaisedButton( - child: Text("LOGOUT"), - onPressed: () async { - await Provider.of(context).logout(); - - //Navigator.pushReplacementNamed(context, "/"); - }) + ImageSelectAndUpload(), + Expanded( + child: ImageList( + listStream: widget.courseDocStream, + onItemClick: (_item, _tag) { + print(_item); + Navigator.push(context, MaterialPageRoute(builder: (_) { + return DetailScreen(_item, _tag); + })); + })) ], ), ), ); } + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; +} + +class ImageSelectAndUpload extends StatefulWidget { + const ImageSelectAndUpload({ + Key key, + }) : super(key: key); + + @override + _ImageSelectAndUploadState createState() => _ImageSelectAndUploadState(); +} + +class _ImageSelectAndUploadState extends State { + File imageThumb; + File image; + + @override + Widget build(BuildContext context) { + return Column(children: [ + // Box to hold the thumb of the image, if nothing then + // show empty container + AnimatedContainer( + height: (imageThumb != null ? 120.0 : 0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: imageThumb != null + ? SizedBox(height: 100.0, child: Image.file(imageThumb)) + : new Container(), + ), + duration: Duration(milliseconds: 300), + ), + // button bar to get the image + FileUploadButtonBar( + imageFile: imageThumb, + onUploadFile: () async { + final v = await ImageService().uploadTheFile({ + 'imageThumb': imageThumb, + 'image': image + }, // information for file upload + (_progress) { + print(_progress.bytesTransferred / _progress.totalByteCount); + }); + + print(v); + + // clear UI + setState(() { + imageThumb = null; + image = null; + }); + }, + onChangeFile: (Map _imageInfo) { + setState(() { + imageThumb = _imageInfo['thumb']; + image = _imageInfo['file']; + }); + }, + ) + ]); + } +} + +class DetailScreen extends StatelessWidget { + final String item; + final String tag; + + DetailScreen(this.item, this.tag); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GestureDetector( + child: Center( + child: FutureBuilder( + future: precacheImage(NetworkImage(this.item), context), + builder: (context, snapshot) { + return Hero( + tag: this.tag, + child: Image.network( + this.item, + ), + ); + }), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ); + } } diff --git a/lib/main.dart b/lib/main.dart index ec756f7..761372d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,14 +25,16 @@ class MyApp extends StatelessWidget { future: Provider.of(context).getUser(), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { - // log error to console - if (snapshot.error != null) { + // log error to console + if (snapshot.error != null) { print("error"); return Text(snapshot.error.toString()); } // redirect to the proper page - return snapshot.hasData ? HomePage(snapshot.data) : LoginPage(); + return snapshot.hasData + ? HomePage(currentUser: snapshot.data) + : LoginPage(); } else { // show loading indicator return LoadingCircle(); diff --git a/pubspec.lock b/pubspec.lock index 7f6fc46..d5a9f1e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,20 +1,34 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.4.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" charcode: dependency: transitive description: @@ -28,7 +42,7 @@ packages: name: cloud_firestore url: "https://pub.dartlang.org" source: hosted - version: "0.11.0+2" + version: "0.12.5+2" collection: dependency: transitive description: @@ -36,6 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" cupertino_icons: dependency: "direct main" description: @@ -49,14 +77,21 @@ packages: name: firebase_auth url: "https://pub.dartlang.org" source: hosted - version: "0.11.1" + version: "0.11.1+7" firebase_core: dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.0+1" + version: "0.4.0+6" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" flutter: dependency: "direct main" description: flutter @@ -67,34 +102,62 @@ packages: description: flutter source: sdk version: "0.0.0" + image: + dependency: "direct main" + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0+10" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.6" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.1.8" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.6.4" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.8.0+1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" provider: dependency: "direct main" description: @@ -108,7 +171,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.5" sky_engine: dependency: transitive description: flutter @@ -141,7 +204,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" term_glyph: dependency: transitive description: @@ -155,7 +218,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.2.11" typed_data: dependency: transitive description: @@ -170,6 +233,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" sdks: - dart: ">=2.2.0 <3.0.0" - flutter: ">=1.2.0 <2.0.0" + dart: ">=2.4.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index ab4c4d6..36ae34a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,9 +24,13 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 provider: 2.0.1 - firebase_core: 0.4.0+1 - firebase_auth: 0.11.1 - cloud_firestore: 0.11.0+2 + firebase_core: 0.4.0+6 + firebase_auth: 0.11.1+7 + cloud_firestore: 0.12.5+2 + firebase_storage: 3.0.2 + image_picker: 0.6.0+10 + image: 2.1.4 + path_provider: 1.1.0 dev_dependencies: flutter_test: diff --git a/readme3.md b/readme3.md new file mode 100644 index 0000000..8c2ae6c --- /dev/null +++ b/readme3.md @@ -0,0 +1,63 @@ +

+ +

+ +### New Packages used + +- cloud_firestore: 0.12.5+2 - to save data to firestorm +- image_picker: 0.6.0+10 - pick an image from the phone gallery or from camera +- image: 2.1.4 - used to resize the image to create a thumbnail +- path_provider: 1.1.0 - get device path information for manipulating the image file + +### New Widgets Created + +- ImageSelectAndUpload - contains a button bar, see below, and an Image Widget and manages the state of the selected image and associated thumbnail. +- FileUploadButtonBar - a button bar containing a button for selecting the file, uploading the file and one for reseting the currently selected file. +- ImageList - list specific information regarding the images in the list +

+ +

+ +### Application Flow + +1. query images collection in firebase to get documents +2. Pass document to ImageList Widget to render +3. Select an image from the camera or the gallery +4. Create a thumbnail of the image to show user, probably will resize the Image for uploading +5. Capture additional information to store with image +6. Upload the images using firebase +7. Then create an additional object in the “Image” collection where we store additional information on the Image +8. Reset the application UI + +## Things to Check On Android If It Doesn't Work + +`gradle.properties` + +``` +android.useAndroidX=true +android.enableJetifier=true +``` + +`build.gradle` + +``` +defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.simple_firebase_auth" + minSdkVersion 17 + targetSdkVersion 28 + multiDexEnabled true <== THIS IS NEW + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" +} +``` + +`pubspec.yaml` - Make Sure Your Versions Are Compatible + +``` +firebase_core: 0.4.0+6 +firebase_auth: 0.11.1+7 +cloud_firestore: 0.12.5+2 +firebase_storage: 3.0.2 +``` diff --git a/screenshots/Screenshot_20190701-235104.jpg b/screenshots/Screenshot_20190701-235104.jpg new file mode 100644 index 0000000..8ec96e9 Binary files /dev/null and b/screenshots/Screenshot_20190701-235104.jpg differ diff --git a/screenshots/Widget Overview Diagram.jpg b/screenshots/Widget Overview Diagram.jpg new file mode 100644 index 0000000..a9d0e13 Binary files /dev/null and b/screenshots/Widget Overview Diagram.jpg differ diff --git a/screenshots/collection-screenshot.png b/screenshots/collection-screenshot.png new file mode 100644 index 0000000..58cf645 Binary files /dev/null and b/screenshots/collection-screenshot.png differ diff --git a/screenshots/storage-screenshot.png b/screenshots/storage-screenshot.png new file mode 100644 index 0000000..a8de437 Binary files /dev/null and b/screenshots/storage-screenshot.png differ