From 08c90fce7497493ee21567d0c0e3852ed3c6bb9f Mon Sep 17 00:00:00 2001 From: Sai Naw Wun Date: Mon, 9 Nov 2020 05:53:25 +0630 Subject: [PATCH 1/3] add version text --- assets/local/localization_en.json | 4 +- lib/data/provider/auth_fb.dart | 2 - lib/domain/constants.dart | 2 + lib/domain/entities/package.dart | 5 +- lib/pages/main/home_page.dart | 1 + lib/pages/main/splash_page.dart | 7 +- lib/pages/package/model/package_model.dart | 49 +++- lib/pages/receiving/receiving_editor.dart | 13 ++ lib/pages/widgets/barcode_scanner.dart | 8 + lib/pages/widgets/bottom_widgets.dart | 48 ++-- lib/pages/widgets/multi_img_file.dart | 255 ++++++++++++--------- pubspec.yaml | 2 +- 12 files changed, 258 insertions(+), 138 deletions(-) diff --git a/assets/local/localization_en.json b/assets/local/localization_en.json index c60d571..4365bd3 100644 --- a/assets/local/localization_en.json +++ b/assets/local/localization_en.json @@ -511,14 +511,14 @@ "receiving.info":"Receiving", "receiving.new":"New receiving", "receiving.create":"New Receiving", - "receiving.update":"Update Reveiving", + "receiving.update":"Update Receiving", "receiving.tracking.id":"Tracking ID", "receiving.remark":"Remark", "receiving.fcs.id":"FCS ID", "receiving.name":"Customer name", "receiving.phone":"Phone number", "receiving.create_btn":"Complete receiving", - "receiving.update_btn":"Update reveiving", + "receiving.update_btn":"Update receiving", "receiving.delete.confirm":"Delete this receiving?", "receiving.return.btn":"Return package", "receiving.return.confirm":"Return package?", diff --git a/lib/data/provider/auth_fb.dart b/lib/data/provider/auth_fb.dart index 1eaa500..15d9dc1 100644 --- a/lib/data/provider/auth_fb.dart +++ b/lib/data/provider/auth_fb.dart @@ -88,7 +88,6 @@ class AuthFb { } Future signInWithPhoneNumber(String smsCode) async { - User user; try { final AuthCredential credential = PhoneAuthProvider.getCredential( verificationId: _verificationId, @@ -102,7 +101,6 @@ class AuthFb { } on Exception catch (e) { return Future.error(SigninException(e.toString())); } - if (user == null) Future.error(SigninException("No current user!")); return Future.value(fcs.AuthResult(authStatus: AuthStatus.AUTH_VERIFIED)); } diff --git a/lib/domain/constants.dart b/lib/domain/constants.dart index fe1d6e4..a5ba92b 100644 --- a/lib/domain/constants.dart +++ b/lib/domain/constants.dart @@ -1,3 +1,5 @@ +const uploadPhotoLimit = 10; + const config_collection = "configs"; const user_collection = "users"; const invitations_collection = "invitations"; diff --git a/lib/domain/entities/package.dart b/lib/domain/entities/package.dart index 7a606d6..70f9af2 100644 --- a/lib/domain/entities/package.dart +++ b/lib/domain/entities/package.dart @@ -96,8 +96,9 @@ class Package { desc: map['desc'], status: map['status'], deliveryAddress: _da, - currentStatusDate: - _currentStatusDate != null ? _currentStatusDate.toDate() : null, + currentStatusDate: _currentStatusDate != null + ? _currentStatusDate.toDate().toLocal() + : null, photoUrls: _photoUrls, shipmentHistory: _shipmentStatus); } diff --git a/lib/pages/main/home_page.dart b/lib/pages/main/home_page.dart index 13529e7..237a20f 100644 --- a/lib/pages/main/home_page.dart +++ b/lib/pages/main/home_page.dart @@ -204,6 +204,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { User user = Provider.of(context).user; + if (user == null) { Future.microtask( () => Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false)); diff --git a/lib/pages/main/splash_page.dart b/lib/pages/main/splash_page.dart index 4651fc0..eb5edfb 100644 --- a/lib/pages/main/splash_page.dart +++ b/lib/pages/main/splash_page.dart @@ -66,9 +66,12 @@ class _SplashScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - new Image.asset( - "assets/logo.jpg", + Container( + height: 180, width: 180, + child: new Image.asset( + "assets/logo.jpg", + ), ), SizedBox(height: 50), Column( diff --git a/lib/pages/package/model/package_model.dart b/lib/pages/package/model/package_model.dart index efa1134..8d44707 100644 --- a/lib/pages/package/model/package_model.dart +++ b/lib/pages/package/model/package_model.dart @@ -108,7 +108,7 @@ class PackageModel extends BaseModel { if (forCustomer) { q = q.where("user_id", isEqualTo: user.id); } - q = q.orderBy("tracking_id", descending: false); + q = q.orderBy("update_time", descending: true); listener = q.snapshots().listen((QuerySnapshot snapshot) { _packages.clear(); _packages = snapshot.documents.map((documentSnapshot) { @@ -139,6 +139,25 @@ class PackageModel extends BaseModel { return null; } + Future getPackageByTrackingID(String trackingID) async { + if (user == null) return null; + String path = "/$packages_collection"; + try { + var snaps = await Firestore.instance + .collection("$path") + .where("tracking_id", isEqualTo: trackingID) + .getDocuments(source: Source.server); + if (snaps.documents.length == 1) { + var snap = snaps.documents[0]; + var package = Package.fromMap(snap.data, snap.documentID); + return package; + } + } catch (e) { + log.warning("Error!! $e"); + } + return null; + } + Future lookupPackage(String trackingID) async { if (user == null) return null; String path = "/$packages_collection"; @@ -201,8 +220,15 @@ class PackageModel extends BaseModel { return Services.instance.userService.searchUser(term); } - Future> searchPackage(String term) { - return Services.instance.packageService.searchPackage(term); + Future> searchPackage(String term) async { + List packages = + await Services.instance.packageService.searchPackage(term); + + Package pkg = await getPackageByTrackingID(term); + if (pkg != null && !packages.contains(pkg)) { + packages.insert(0, pkg); + } + return packages; } Future createPackages(User user, List packages) { @@ -216,7 +242,8 @@ class PackageModel extends BaseModel { package.fcsID = user.fcsID; } if (files != null) { - if (files.length > 5) throw Exception("Exceed number of file upload"); + if (files.length > uploadPhotoLimit) + throw Exception("Exceed number of file upload"); package.photoUrls = package.photoUrls == null ? [] : package.photoUrls; for (File f in files) { String path = Path.join(pkg_files_path); @@ -240,7 +267,12 @@ class PackageModel extends BaseModel { } if (files != null) { - if (files.length > 5) throw Exception("Exceed number of file upload"); + var count = (package.photoUrls?.length ?? 0) + + files.length - + (deletedUrls?.length ?? 0); + + if (count > uploadPhotoLimit) + throw Exception("Exceed number of file upload"); package.photoUrls = package.photoUrls == null ? [] : package.photoUrls; for (File f in files) { String path = Path.join(pkg_files_path); @@ -265,7 +297,12 @@ class PackageModel extends BaseModel { } if (files != null) { - if (files.length > 5) throw Exception("Exceed number of file upload"); + var count = (package.photoUrls?.length ?? 0) + + files.length - + (deletedUrls?.length ?? 0); + + if (count > uploadPhotoLimit) + throw Exception("Exceed number of file upload"); package.photoUrls = package.photoUrls == null ? [] : package.photoUrls; for (File f in files) { String path = Path.join(pkg_files_path); diff --git a/lib/pages/receiving/receiving_editor.dart b/lib/pages/receiving/receiving_editor.dart index e0dd8ad..305ab6a 100644 --- a/lib/pages/receiving/receiving_editor.dart +++ b/lib/pages/receiving/receiving_editor.dart @@ -52,6 +52,17 @@ class _ReceivingEditorState extends State { } else { package = new Package(); } + _trackingIDCtl.addListener(() { + var text = _trackingIDCtl.text; + if (text.contains(RegExp(r'[a-z ]'))) { + text = text.toUpperCase().replaceAll(" ", ""); + _trackingIDCtl.value = _trackingIDCtl.value.copyWith( + text: text, + selection: + TextSelection(baseOffset: text.length, extentOffset: text.length), + ); + } + }); } @override @@ -158,7 +169,9 @@ class _ReceivingEditorState extends State { height: 10, ), remarkBox, + Divider(), img, + Divider(), SizedBox( height: 10, ), diff --git a/lib/pages/widgets/barcode_scanner.dart b/lib/pages/widgets/barcode_scanner.dart index 3bbed5d..2706a21 100644 --- a/lib/pages/widgets/barcode_scanner.dart +++ b/lib/pages/widgets/barcode_scanner.dart @@ -9,6 +9,14 @@ Future scanBarcode() async { if (barcode.contains(gs)) { var codes = barcode.split(gs); barcode = codes.length >= 2 ? codes[1] : barcode; + } else if (barcode.startsWith("96")) { + if (barcode.length == 34) { + int start = barcode.length - 12; + barcode = barcode.substring(start); + } else if (barcode.length == 22) { + int start = barcode.length - 15; + barcode = barcode.substring(start); + } } return barcode; } catch (e) { diff --git a/lib/pages/widgets/bottom_widgets.dart b/lib/pages/widgets/bottom_widgets.dart index e5c9710..c64ff11 100644 --- a/lib/pages/widgets/bottom_widgets.dart +++ b/lib/pages/widgets/bottom_widgets.dart @@ -1,32 +1,46 @@ import 'package:fcs/pages/contact/contact_page.dart'; +import 'package:fcs/pages/main/model/main_model.dart'; import 'package:fcs/pages/term/term_page.dart'; -import 'package:fcs/pages/widgets/bottom_up_page_route.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_icons/flutter_icons.dart'; +import 'package:provider/provider.dart'; class BottomWidgets extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - InkWell( - onTap: () { - Navigator.of(context) - .push(CupertinoPageRoute(builder: (context) => ContactPage())); - }, - child: _buildSmallButton( - context, "contact.btn", SimpleLineIcons.support), + var pkgInfo = Provider.of(context).packageInfo; + final versionBox = Text( + "v${pkgInfo.version}+${pkgInfo.buildNumber}", + style: TextStyle(color: Colors.white30), + ); + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + InkWell( + onTap: () { + Navigator.of(context).push( + CupertinoPageRoute(builder: (context) => ContactPage())); + }, + child: _buildSmallButton( + context, "contact.btn", SimpleLineIcons.support), + ), + InkWell( + onTap: () { + Navigator.of(context) + .push(CupertinoPageRoute(builder: (context) => TermPage())); + }, + child: _buildSmallButton(context, "term.btn", Icons.info_outline), + ), + ], ), - InkWell( - onTap: () { - Navigator.of(context) - .push(CupertinoPageRoute(builder: (context) => TermPage())); - }, - child: _buildSmallButton(context, "term.btn", Icons.info_outline), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: versionBox, ), ], ); diff --git a/lib/pages/widgets/multi_img_file.dart b/lib/pages/widgets/multi_img_file.dart index fb85cd4..70d7cb4 100644 --- a/lib/pages/widgets/multi_img_file.dart +++ b/lib/pages/widgets/multi_img_file.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:fcs/helpers/theme.dart'; +import 'package:fcs/pages/widgets/callbacks.dart'; import 'package:fcs/pages/widgets/right_left_page_rout.dart'; import 'package:fcs/pages/widgets/show_img.dart'; import 'package:fcs/pages/widgets/show_multiple_img.dart'; @@ -48,117 +49,142 @@ class _MultiImageFileState extends State { @override Widget build(BuildContext context) { - return Container( - height: 130, - width: 500, - child: ListView.separated( - separatorBuilder: (context, index) => Divider( - color: Colors.black, - ), - itemCount: - widget.enabled ? fileContainers.length + 1 : fileContainers.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - if (index == fileContainers.length) { - return InkWell( - onTap: () async { - bool camera = false, gallery = false; - await _dialog( - context, () => camera = true, () => gallery = true); - if (camera || gallery) { - var selectedFile = await ImagePicker().getImage( - source: camera ? ImageSource.camera : ImageSource.gallery, - imageQuality: 80, - maxWidth: 1000); - if (selectedFile != null) { - _fileAdded(DisplayImageSource(), File(selectedFile.path)); - } - } - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - width: 200, - height: 130, - decoration: BoxDecoration( - border: Border.all( - color: primaryColor, - width: 2.0, - ), - ), - child: Icon(SimpleLineIcons.plus), - ), - ), - ); - } else { - return InkWell( - onTap: () => Navigator.push( - context, - RightLeftPageRoute(ShowMultiImage( - displayImageSources: fileContainers, - initialPage: index, - ))), - child: Stack(alignment: Alignment.topLeft, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - width: 200, - height: 130, - decoration: BoxDecoration( - border: Border.all( - color: primaryColor, - width: 2.0, - ), - ), - child: fileContainers[index].file == null - ? CachedNetworkImage( - width: 50, - height: 50, - imageUrl: fileContainers[index].url, - placeholder: (context, url) => Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator()), - ], - ), - errorWidget: (context, url, error) => - Icon(Icons.error), - ) - // Image.network(fileContainers[index].url, - // width: 50, height: 50) - : Image.file(fileContainers[index].file, - width: 50, height: 50), - ), - ), - widget.enabled - ? Positioned( - top: 0, - right: 0, - child: Container( - height: 50, + return Column( + children: [ + widget.enabled + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () => {_openImagePicker(false)}, + child: Stack( + children: [ + Container( width: 50, - child: IconButton( - icon: Icon( - Icons.close, - color: primaryColor, - ), - onPressed: () => - {_fileRemove(fileContainers[index])}), + height: 50, + child: Icon( + MaterialCommunityIcons.image, + color: primaryColor, + size: 35, + ), ), - ) - : Container(), - ]), - ); - } - }, - ), + Positioned( + right: 0, + top: 0, + child: actionIcon( + color: Colors.green, iconData: Icons.add)) + ], + ), + ), + InkWell( + onTap: () => {_openImagePicker(true)}, + child: Stack( + children: [ + Container( + width: 50, + height: 50, + child: Icon( + MaterialCommunityIcons.camera, + color: primaryColor, + size: 35, + ), + ), + Positioned( + right: 0, + top: 0, + child: actionIcon( + color: Colors.green, iconData: Icons.add)) + ], + ), + ), + ], + ) + : Container(), + Container( + height: 100, + child: ListView.separated( + separatorBuilder: (context, index) => Divider( + color: Colors.black, + ), + itemCount: fileContainers.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return InkWell( + onTap: () => Navigator.push( + context, + RightLeftPageRoute(ShowMultiImage( + displayImageSources: fileContainers, + initialPage: index, + ))), + child: Stack(alignment: Alignment.topLeft, children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + border: Border.all( + color: primaryColor, + width: 1.0, + ), + ), + child: fileContainers[index].file == null + ? CachedNetworkImage( + fit: BoxFit.cover, + width: 50, + height: 50, + imageUrl: fileContainers[index].url, + placeholder: (context, url) => Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator()), + ], + ), + errorWidget: (context, url, error) => + Icon(Icons.error), + ) + : FittedBox( + fit: BoxFit.cover, + child: Image.file( + fileContainers[index].file, + ), + ), + ), + ), + widget.enabled + ? Positioned( + top: 10, + right: 0, + child: actionIcon( + color: Colors.red, + iconData: Icons.remove, + onTap: () => + {_fileRemove(fileContainers[index])}), + ) + : Container(), + ]), + ); + }, + ), + ), + ], ); } + _openImagePicker(bool camera) async { + var selectedFile = await ImagePicker().getImage( + source: camera ? ImageSource.camera : ImageSource.gallery, + imageQuality: 80, + maxWidth: 1000); + if (selectedFile != null) { + _fileAdded(DisplayImageSource(), File(selectedFile.path)); + } + } + _fileAdded(DisplayImageSource fileContainer, File selectedFile) { fileContainer.file = selectedFile; setState(() { @@ -281,4 +307,21 @@ class _MultiImageFileState extends State { }, ); } + + Widget actionIcon({OnTap onTap, Color color, IconData iconData}) { + return InkWell( + onTap: onTap, + child: ClipOval( + child: Container( + color: color, + height: 20, + width: 20, + child: Icon( + iconData, + color: Colors.white, + size: 15, + )), + ), + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index a4fcf54..3e93c9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: fcs description: FCS Logistics publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0+4 +version: 1.0.5+7 environment: sdk: ">=2.7.0 <3.0.0" From ca21f48dab8301a46d4620d4bf646015a3d1e9b9 Mon Sep 17 00:00:00 2001 From: Sai Naw Wun Date: Fri, 13 Nov 2020 02:38:16 +0630 Subject: [PATCH 2/3] add pagination --- lib/pages/main/model/base_model.dart | 4 + lib/pages/package/model/package_model.dart | 166 +++++++++++---------- lib/pages/package/package_list.dart | 60 ++------ lib/pages/processing/processing_list.dart | 45 +++--- lib/pages/receiving/receiving_list.dart | 65 ++++---- lib/pagination/paginator_listener.dart | 163 ++++++++++++++++++++ lib/pagination/paginator_listview.dart | 99 ++++++++++++ 7 files changed, 412 insertions(+), 190 deletions(-) create mode 100644 lib/pagination/paginator_listener.dart create mode 100644 lib/pagination/paginator_listview.dart diff --git a/lib/pages/main/model/base_model.dart b/lib/pages/main/model/base_model.dart index ac8b8b5..a0b82d0 100644 --- a/lib/pages/main/model/base_model.dart +++ b/lib/pages/main/model/base_model.dart @@ -20,6 +20,10 @@ abstract class BaseModel extends ChangeNotifier { this.setting = setting; } + void notify() { + notifyListeners(); + } + void logout() {} // request makes http request diff --git a/lib/pages/package/model/package_model.dart b/lib/pages/package/model/package_model.dart index 8d44707..2dd4fd1 100644 --- a/lib/pages/package/model/package_model.dart +++ b/lib/pages/package/model/package_model.dart @@ -8,116 +8,126 @@ import 'package:fcs/domain/entities/package.dart'; import 'package:fcs/domain/entities/user.dart'; import 'package:fcs/domain/vo/delivery_address.dart'; import 'package:fcs/helpers/firebase_helper.dart'; -import 'package:fcs/helpers/paginator.dart'; import 'package:fcs/pages/main/model/base_model.dart'; +import 'package:fcs/pagination/paginator_listener.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as Path; class PackageModel extends BaseModel { final log = Logger('PackageModel'); - StreamSubscription listener; + PaginatorListener packages; + PaginatorListener customerPackages; + PaginatorListener activePackages; - List get packages => _menuSelectedIndex == 1 - ? _packages - : List.from(_delivered.values); - - List _packages = []; - - Paginator _delivered; bool isLoading = false; int _menuSelectedIndex = 1; set menuSelectedIndex(int index) { _menuSelectedIndex = index; + + _loadPackages(_menuSelectedIndex == 2); + _loadCustomerPackages(_menuSelectedIndex == 2); + notifyListeners(); } get menuSelectedIndex => _menuSelectedIndex; - initData(bool forCustomer) { + void privilegeChanged() { + if (user != null) { + _initData(); + } + } + + Future _initData() async { logout(); _menuSelectedIndex = 1; - _loadPackages(forCustomer); - _delivered = _getDelivered(forCustomer); - _delivered.load(); + packages = PaginatorListener( + (data, id) => Package.fromMap(data, id), onChange: () { + notifyListeners(); + }, rowPerLoad: 30, insertNewByListener: true); + customerPackages = PaginatorListener( + (data, id) => Package.fromMap(data, id), onChange: () { + notifyListeners(); + }, rowPerLoad: 30, insertNewByListener: true); + activePackages = PaginatorListener( + (data, id) => Package.fromMap(data, id), onChange: () { + notifyListeners(); + }, rowPerLoad: 30, insertNewByListener: true); + + _loadPackages(_menuSelectedIndex == 2); + _loadCustomerPackages(_menuSelectedIndex == 2); + _loadActivePackages(); } @override logout() async { - if (_delivered != null) _delivered.close(); - if (listener != null) await listener.cancel(); - _packages = []; + if (customerPackages != null) customerPackages.close(); + if (packages != null) packages.close(); + if (activePackages != null) activePackages.close(); } - Future loadMore({bool isCustomer}) async { - if (_delivered.ended || menuSelectedIndex == 1) - return; // when delivered menu is not selected return - isLoading = true; - notifyListeners(); - await _delivered.load(onFinished: () { - isLoading = false; - notifyListeners(); - }); - } - - Future refresh({bool isCustomer}) async { - if (menuSelectedIndex == 1) - return; // when delivered menu is not selected return - await _delivered.refresh(onFinished: () { - notifyListeners(); - }); - } - - Paginator _getDelivered(bool isCustomer) { - if (!isCustomer) { - if (user == null || - !((user.hasPackages() || - user.hasReceiving() || - user.hasProcessing()))) throw "No privilege"; - } - var pageQuery = Firestore.instance - .collection("/$packages_collection") - .where("is_delivered", isEqualTo: true) - .where("is_deleted", isEqualTo: false); - if (isCustomer) { - pageQuery = pageQuery.where("user_id", isEqualTo: user.id); - } - pageQuery = pageQuery.orderBy("status_date", descending: true); - var paginator = new Paginator(pageQuery, rowPerLoad: 20, toObj: (data, id) { - return Package.fromMap(data, id); - }); - return paginator; - } - - Future _loadPackages(bool forCustomer) async { + Future _loadPackages(bool isDelivered) async { if (user == null) return; - if (!forCustomer && - !((user.hasPackages() || user.hasReceiving() || user.hasProcessing()))) + if (!((user.hasPackages() || user.hasReceiving() || user.hasProcessing()))) return; String path = "/$packages_collection"; - if (listener != null) listener.cancel(); - _packages = []; try { - var q = Firestore.instance - .collection("$path") - .where("is_delivered", isEqualTo: false) - .where("is_deleted", isEqualTo: false); + Query listenerQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered); + Query pageQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered); - if (forCustomer) { - q = q.where("user_id", isEqualTo: user.id); - } - q = q.orderBy("update_time", descending: true); - listener = q.snapshots().listen((QuerySnapshot snapshot) { - _packages.clear(); - _packages = snapshot.documents.map((documentSnapshot) { - var package = Package.fromMap( - documentSnapshot.data, documentSnapshot.documentID); - return package; - }).toList(); - notifyListeners(); - }); + pageQuery = pageQuery.orderBy("update_time", descending: true); + packages.refresh(listeningQuery: listenerQuery, pageQuery: pageQuery); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future _loadCustomerPackages(bool isDelivered) async { + if (user == null) return; + String path = "/$packages_collection"; + + try { + Query listenerQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered) + .where("user_id", isEqualTo: user.id); + Query pageQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: isDelivered) + .where("user_id", isEqualTo: user.id) + .orderBy("update_time", descending: true); + + customerPackages.refresh( + listeningQuery: listenerQuery, pageQuery: pageQuery); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future _loadActivePackages() async { + if (user == null) return; + if (!((user.hasPackages() || user.hasReceiving() || user.hasProcessing()))) + return; + String path = "/$packages_collection"; + + try { + Query listenerQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: false); + Query pageQuery = Firestore.instance + .collection(path) + .where("is_delivered", isEqualTo: false); + + pageQuery = pageQuery.orderBy("update_time", descending: true); + activePackages.refresh( + listeningQuery: listenerQuery, pageQuery: pageQuery); } catch (e) { log.warning("Error!! $e"); } diff --git a/lib/pages/package/package_list.dart b/lib/pages/package/package_list.dart index 6d3c3f6..c28ce22 100644 --- a/lib/pages/package/package_list.dart +++ b/lib/pages/package/package_list.dart @@ -8,6 +8,7 @@ import 'package:fcs/pages/widgets/local_popup_menu_button.dart'; import 'package:fcs/pages/widgets/local_popupmenu.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:fcs/pages/widgets/progress.dart'; +import 'package:fcs/pagination/paginator_listview.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -22,26 +23,18 @@ class PackageList extends StatefulWidget { class _PackageListState extends State { bool _isLoading = false; - var _controller = ScrollController(); @override void initState() { super.initState(); - - _controller.addListener(() async { - if (_controller.position.pixels == _controller.position.maxScrollExtent) { - Provider.of(context, listen: false) - .loadMore(isCustomer: widget.forCustomer); - } - }); - Provider.of(context, listen: false) - .initData(widget.forCustomer); } @override Widget build(BuildContext context) { var packageModel = Provider.of(context); - var packages = packageModel.packages; + var packages = widget.forCustomer + ? packageModel.customerPackages + : packageModel.packages; final popupMenu = LocalPopupMenuButton( popmenus: [ @@ -90,43 +83,14 @@ class _PackageListState extends State { popupMenu ], ), - body: Column( - children: [ - Expanded( - child: RefreshIndicator( - child: ListView.separated( - controller: _controller, - separatorBuilder: (context, index) => Divider( - color: Colors.grey, - height: 1, - ), - scrollDirection: Axis.vertical, - itemCount: packages.length, - itemBuilder: (BuildContext context, int index) { - return PackageListRow( - key: ValueKey(packages[index].id), - package: packages[index], - isCustomer: widget.forCustomer, - ); - }), - onRefresh: () => - packageModel.refresh(isCustomer: widget.forCustomer), - ), - ), - packageModel.isLoading - ? Container( - padding: EdgeInsets.all(8), - color: primaryColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Loading...", - style: TextStyle(color: Colors.white)), - ], - ), - ) - : Container(), - ], + body: PaginatorListView( + paginatorListener: packages, + rowBuilder: (p) => PackageListRow( + key: ValueKey(p.id), + package: p, + isCustomer: widget.forCustomer, + ), + color: primaryColor, )), ); } diff --git a/lib/pages/processing/processing_list.dart b/lib/pages/processing/processing_list.dart index 9a1d5f0..6ab3497 100644 --- a/lib/pages/processing/processing_list.dart +++ b/lib/pages/processing/processing_list.dart @@ -5,6 +5,7 @@ import 'package:fcs/pages/package/model/package_model.dart'; import 'package:fcs/pages/package_search/package_serach.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:fcs/pages/widgets/progress.dart'; +import 'package:fcs/pagination/paginator_listview.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -33,7 +34,7 @@ class _ProcessingListState extends State { @override Widget build(BuildContext context) { var packageModel = Provider.of(context); - bool isCustomer = context.select((MainModel m) => m.isCustomer()); + var packages = packageModel.activePackages; return LocalProgress( inAsyncCall: _isLoading, @@ -52,33 +53,25 @@ class _ProcessingListState extends State { color: Colors.white, ), actions: [ - isCustomer - ? Container() - : IconButton( - icon: Icon( - Icons.search, - color: Colors.white, - ), - iconSize: 30, - onPressed: () => searchPackage(context, - callbackPackageSelect: _searchCallback), - ), + IconButton( + icon: Icon( + Icons.search, + color: Colors.white, + ), + iconSize: 30, + onPressed: () => searchPackage(context, + callbackPackageSelect: _searchCallback), + ), ], ), - body: new ListView.separated( - separatorBuilder: (context, index) => Divider( - color: Colors.black, - height: 1, - ), - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: packageModel.packages.length, - itemBuilder: (BuildContext context, int index) { - return ProcessingListRow( - key: ValueKey(packageModel.packages[index].id), - package: packageModel.packages[index], - ); - })), + body: PaginatorListView( + paginatorListener: packages, + rowBuilder: (p) => ProcessingListRow( + key: ValueKey(p.id), + package: p, + ), + color: primaryColor, + )), ); } diff --git a/lib/pages/receiving/receiving_list.dart b/lib/pages/receiving/receiving_list.dart index 4d6ecee..d8e9777 100644 --- a/lib/pages/receiving/receiving_list.dart +++ b/lib/pages/receiving/receiving_list.dart @@ -6,6 +6,7 @@ import 'package:fcs/pages/package_search/package_serach.dart'; import 'package:fcs/pages/widgets/bottom_up_page_route.dart'; import 'package:fcs/pages/widgets/local_text.dart'; import 'package:fcs/pages/widgets/progress.dart'; +import 'package:fcs/pagination/paginator_listview.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -25,7 +26,6 @@ class _ReceivingListState extends State { @override void initState() { super.initState(); - Provider.of(context, listen: false).initData(false); } @override @@ -36,7 +36,7 @@ class _ReceivingListState extends State { @override Widget build(BuildContext context) { var packageModel = Provider.of(context); - bool isCustomer = context.select((MainModel m) => m.isCustomer()); + var packages = packageModel.activePackages; return LocalProgress( inAsyncCall: _isLoading, @@ -55,44 +55,33 @@ class _ReceivingListState extends State { color: Colors.white, ), actions: [ - isCustomer - ? Container() - : IconButton( - icon: Icon( - Icons.search, - color: Colors.white, - ), - iconSize: 30, - onPressed: () => searchPackage(context, - callbackPackageSelect: _searchCallback), - ), + IconButton( + icon: Icon( + Icons.search, + color: Colors.white, + ), + iconSize: 30, + onPressed: () => searchPackage(context, + callbackPackageSelect: _searchCallback), + ), ], ), - floatingActionButton: isCustomer - ? Container() - : FloatingActionButton.extended( - onPressed: () { - _newReceiving(); - }, - icon: Icon(Icons.add), - label: - LocalText(context, "receiving.new", color: Colors.white), - backgroundColor: primaryColor, - ), - body: new ListView.separated( - separatorBuilder: (context, index) => Divider( - color: Colors.black, - height: 1, - ), - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: packageModel.packages.length, - itemBuilder: (BuildContext context, int index) { - return ReceivingListRow( - key: ValueKey(packageModel.packages[index].id), - package: packageModel.packages[index], - ); - })), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + _newReceiving(); + }, + icon: Icon(Icons.add), + label: LocalText(context, "receiving.new", color: Colors.white), + backgroundColor: primaryColor, + ), + body: PaginatorListView( + paginatorListener: packages, + rowBuilder: (p) => ReceivingListRow( + key: ValueKey(p.id), + package: p, + ), + color: primaryColor, + )), ); } diff --git a/lib/pagination/paginator_listener.dart b/lib/pagination/paginator_listener.dart new file mode 100644 index 0000000..9907930 --- /dev/null +++ b/lib/pagination/paginator_listener.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/pages/widgets/callbacks.dart'; +import 'package:logging/logging.dart'; + +typedef ToObj = Function(Map data, String id); + +/* + * PaginatorListener load data in page + * and listen to document change based on 'update_time' and 'delete_time' fields + * of the document + */ +class PaginatorListener { + final log = Logger('PaginatorListener'); + + List ids = []; + List data = []; + DocumentSnapshot prev; + int rowPerLoad = 10; + bool ended = false; + bool isLoading = false; + bool insertNewByListener = false; + ToObj toObj; + CallBack onChange; + + StreamSubscription listener; + Query listeningQuery; + Query pageQuery; + + PaginatorListener(this.toObj, + {this.onChange, this.rowPerLoad = 10, this.insertNewByListener = false}); + + Future refresh({Query listeningQuery, Query pageQuery}) { + this.listeningQuery = listeningQuery ?? this.listeningQuery; + this.pageQuery = pageQuery ?? this.pageQuery; + _clearState(); + _initListener(); + return _load(); + } + + void _clearState() { + prev = null; + ids = []; + data = []; + ended = false; + isLoading = false; + if (listener != null) listener.cancel(); + listener = null; + } + + void close() { + _clearState(); + } + + final String updateTimeField = 'update_time'; + final String deleteTimeField = 'delete_time'; + final String isDeletedField = 'is_deleted'; + void _initListener() { + Query _query = + listeningQuery.orderBy(updateTimeField, descending: true).limit(1); + _query.getDocuments(source: Source.server).then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + int updateTime = 0; + if (count == 1) { + updateTime = snapshot.documents[0].data[updateTimeField]; + } + + Query _queryListener = listeningQuery + .where(updateTimeField, isGreaterThan: updateTime) + .orderBy(updateTimeField, descending: true); + + if (listener != null) listener.cancel(); + listener = + _queryListener.snapshots(includeMetadataChanges: true).listen((qs) { + qs.documentChanges.forEach((c) { + switch (c.type) { + case DocumentChangeType.added: + _update(c.document.documentID, c.document.data); + break; + case DocumentChangeType.modified: + _update(c.document.documentID, c.document.data); + break; + case DocumentChangeType.removed: + _remove(c.document.documentID, c.document.data); + break; + default: + } + if (onChange != null) onChange(); + }); + }); + }); + } + + void _update(String id, Map map) { + T t = toObj(map, id); + if (ids.contains(id)) { + var deleted = map[deleteTimeField]; + var isDeleted = map[isDeletedField]; + if ((deleted ?? 0) > 0 || (isDeleted ?? false)) { + data.removeAt(ids.indexOf(id)); + ids.remove(id); + } else { + data.removeAt(ids.indexOf(id)); + data.insert(ids.indexOf(id), t); + } + } else if (insertNewByListener) { + data.insert(0, t); + ids.insert(0, id); + } + } + + void _add(String id, Map map) { + T t = toObj(map, id); + if (!ids.contains(id)) { + data.add(t); + ids.add(id); + } + } + + void _remove(String id, Map map) { + if (ids.contains(id)) { + data.removeAt(ids.indexOf(id)); + ids.remove(id); + } + } + + Future loadMore() { + return this._load(); + } + + Future _load({CallBack onStarted, CallBack onFinished}) async { + if (ended) return ended; + Query _query = + prev != null ? pageQuery.startAfterDocument(prev) : pageQuery; + try { + isLoading = true; + if (onStarted != null) { + onStarted(); + } + if (onChange != null) onChange(); + await _query + .where(isDeletedField, isEqualTo: false) + .limit(rowPerLoad) + .getDocuments(source: Source.server) + .then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + ended = count < rowPerLoad; + prev = count > 0 ? snapshot.documents[count - 1] : prev; + snapshot.documents.forEach((e) { + _add(e.documentID, e.data); + }); + }); + } catch (e) { + log.warning("Error!! $e"); + } finally { + isLoading = false; + if (onFinished != null) onFinished(); + if (onChange != null) onChange(); + } + return ended; + } +} diff --git a/lib/pagination/paginator_listview.dart b/lib/pagination/paginator_listview.dart new file mode 100644 index 0000000..f0a7d39 --- /dev/null +++ b/lib/pagination/paginator_listview.dart @@ -0,0 +1,99 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'paginator_listener.dart'; + +typedef RowBuilder = Widget Function(dynamic); +typedef OnScroll = void Function(bool down); + +class PaginatorListView extends StatelessWidget { + final PaginatorListener paginatorListener; + final RowBuilder rowBuilder; + final OnScroll onScroll; + final ScrollController _scrollController; + final Color color; + + PaginatorListView( + {Key key, + this.paginatorListener, + this.rowBuilder, + this.onScroll, + this.color = Colors.blueAccent}) + : _scrollController = ScrollController(), + assert(paginatorListener != null), + assert(rowBuilder != null), + super(key: key) { + _scrollController.addListener(() async { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + paginatorListener.loadMore(); + } + if (onScroll != null) { + var down = _scrollController.position.userScrollDirection == + ScrollDirection.forward; + onScroll(down); + } + }); + } + + @override + Widget build(BuildContext context) { + bool ended = paginatorListener.ended; + int count = paginatorListener.data.length; + if (ended) count++; + + return Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () { + return paginatorListener.refresh(); + }, + child: ListView.separated( + separatorBuilder: (context, index) => + Divider(height: 1, color: Colors.black), + controller: _scrollController, + scrollDirection: Axis.vertical, + physics: const AlwaysScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: count, + itemBuilder: (BuildContext context, int index) { + if (ended && index == count - 1) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Center(child: Text("No more data")), + ); + } + T t = paginatorListener.data[index]; + return rowBuilder(t); + }), + ), + ), + paginatorListener.isLoading ? _loadingRow() : Container() + ], + ); + } + + Widget _loadingRow() { + return Container( + padding: EdgeInsets.all(8), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation(color)), + Text( + "Loading...", + style: TextStyle(color: color), + ) + ], + ), + ], + ), + ); + } +} From 96330a80dcb92e523a7ef4836da49e2ba5f6237b Mon Sep 17 00:00:00 2001 From: Sai Naw Wun Date: Fri, 13 Nov 2020 03:09:53 +0630 Subject: [PATCH 3/3] speed up file uploads --- lib/helpers/firebase_helper.dart | 11 ++++++++++ lib/pages/package/model/package_model.dart | 24 +++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/helpers/firebase_helper.dart b/lib/helpers/firebase_helper.dart index 2febc03..f6d95c5 100644 --- a/lib/helpers/firebase_helper.dart +++ b/lib/helpers/firebase_helper.dart @@ -23,6 +23,17 @@ Future getClaims({bool refreshIdToken = false}) async { return idToken.claims; } +// returns list of url +Future> uploadFiles(String path, List files, + {String fileName}) async { + List> fu = []; + for (File f in files) { + Future u = uploadStorage(path, f); + fu.add(u); + } + return Future.wait(fu); +} + Future uploadStorage(String path, File file, {String fileName}) async { if (fileName == null) { fileName = Uuid().v4(); diff --git a/lib/pages/package/model/package_model.dart b/lib/pages/package/model/package_model.dart index 2dd4fd1..f3f5470 100644 --- a/lib/pages/package/model/package_model.dart +++ b/lib/pages/package/model/package_model.dart @@ -255,11 +255,11 @@ class PackageModel extends BaseModel { if (files.length > uploadPhotoLimit) throw Exception("Exceed number of file upload"); package.photoUrls = package.photoUrls == null ? [] : package.photoUrls; - for (File f in files) { - String path = Path.join(pkg_files_path); - String url = await uploadStorage(path, f); + String path = Path.join(pkg_files_path); + List urls = await uploadFiles(path, files); + urls.forEach((url) { package.photoUrls.add(url); - } + }); } return Services.instance.packageService.createReceiving(package); } @@ -284,11 +284,11 @@ class PackageModel extends BaseModel { if (count > uploadPhotoLimit) throw Exception("Exceed number of file upload"); package.photoUrls = package.photoUrls == null ? [] : package.photoUrls; - for (File f in files) { - String path = Path.join(pkg_files_path); - String url = await uploadStorage(path, f); + String path = Path.join(pkg_files_path); + List urls = await uploadFiles(path, files); + urls.forEach((url) { package.photoUrls.add(url); - } + }); } await Services.instance.packageService.updateReceiving(package); } @@ -314,11 +314,11 @@ class PackageModel extends BaseModel { if (count > uploadPhotoLimit) throw Exception("Exceed number of file upload"); package.photoUrls = package.photoUrls == null ? [] : package.photoUrls; - for (File f in files) { - String path = Path.join(pkg_files_path); - String url = await uploadStorage(path, f); + String path = Path.join(pkg_files_path); + List urls = await uploadFiles(path, files); + urls.forEach((url) { package.photoUrls.add(url); - } + }); package.photoUrls.removeWhere((e) => deletedUrls.contains(e)); } await Services.instance.packageService.updateProcessing(package);