import 'dart:math'; import 'package:badges/badges.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_wisetronic/events/eventbus.dart'; import 'package:flutter_wisetronic/events/events.dart'; import 'package:flutter_wisetronic/generated/l10n.dart'; import 'package:flutter_wisetronic/models/business.dart'; import 'package:flutter_wisetronic/models/cart_info.dart'; import 'package:flutter_wisetronic/models/cart_line_item.dart'; import 'package:flutter_wisetronic/models/category_products.dart'; import 'package:flutter_wisetronic/models/comment.dart'; import 'package:flutter_wisetronic/models/product.dart'; import 'package:flutter_wisetronic/models/product_image.dart'; import 'package:flutter_wisetronic/pages/product_detail_page.dart'; import 'package:flutter_wisetronic/store/store.dart'; import 'package:flutter_wisetronic/utils/http_util.dart'; import 'package:flutter_wisetronic/utils/shop_scroll_controller.dart'; import 'package:flutter_wisetronic/utils/shop_scroll_coordinator.dart'; import 'package:flutter_wisetronic/utils/utils.dart'; import 'package:flutter_wisetronic/widgets/general/add_remove_button.dart'; import 'package:flutter_wisetronic/widgets/general/animation_point_manager.dart'; import 'package:flutter_wisetronic/widgets/general/carousel.dart'; import 'package:flutter_wisetronic/widgets/general/read_more_text.dart'; import 'package:flutter_wisetronic/widgets/general/show_price.dart'; import 'package:flutter_wisetronic/widgets/general/sliding_up_panel.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:smooth_star_rating/smooth_star_rating.dart'; import '../../constants.dart'; import '../../utils/util_io.dart' if (dart.library.html) '../../utils/util_web.dart'; import 'product_item.dart'; import 'product_search.dart'; import 'shopping_cart_bar.dart'; MediaQueryData mediaQuery; double statusBarHeight; double screenWidth; double screenHeight; class Shop extends StatefulWidget { final int businessId; const Shop({Key key, this.businessId}) : super(key: key); @override State createState() => ShopState(); } class ShopState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { GlobalKey _scaffoldKey = new GlobalKey(); Business _business; List _categoryProducts; List _featuredProducts; List _hotSaleProducts; List _prompts = []; bool checkCloseFlag = false; bool displayProductByCategoryClick = false; String displayProductByCategoryClickIndicator = ''; int categoryId = 0; int _productCurrentPage = 1; static const num _categoryHeight = 50.0; static const num _categoryDescHeight = 50.0; static const num _productHeight = Constants.PRODUCT_ITEM_HEIGHT_H; int _categoryIndex = 0; bool _categoryIndexChange = false; bool _isLoading = false; bool refresh = false; ShopScrollCoordinator _shopCoordinator; ShopScrollController _pageScrollController; TabController _tabController; final double _sliverAppBarInitHeight = 150.0; final double _tabBarHeight = 50.0; double _sliverAppBarMaxHeight; ShopScrollController _listScrollController1; ShopScrollController _listScrollController2; ShopScrollController _listScrollController3; AnimationPointManager _animationPointManager = AnimationPointManager(); GlobalKey stackKey = GlobalKey(); GlobalKey endKey = GlobalKey(); List comments; int _commentPage = 1; int _commentPageCount = 1; bool _commentLoadingFinish = false; RefreshController _commentRefreshController = RefreshController(initialRefresh: true); PanelController panelController = PanelController(); SlidingUpPanel _slidUpShoppingCart; SliverPersistentHeader promotHeader; bool _animationFinish = true; Carousel slidingGellery; // StreamSubscription onProductWillAddToCartSubscription; // StreamSubscription onProductWillRemoveFromCartSubscription; void _onCommentRefresh() { if (_commentPage - 1 > 1) { _commentPage -= 1; } else { _commentPage = 1; } _commentRefreshController.resetNoData(); _loadComment(true); } void _onCommentLoadMore() { if (_commentPageCount > _commentPage) { _commentPage += 1; _loadComment(false); } else { _commentRefreshController.loadNoData(); } } _loadComment(bool isRefresh) { _commentLoadingFinish = false; HttpUtil.httpGet( 'v1/get-comments', queryParameters: { 'page': _commentPage.toString(), 'size': Constants.ORDERS_PER_PAGE.toString(), }, businessId: _business.id, ).then((data) { if (isRefresh) { _commentRefreshController.refreshCompleted(); } else { _commentRefreshController.loadComplete(); } _commentPage = int.parse(data['_meta']['currentPage'].toString()); _commentPageCount = int.parse(data['_meta']['pageCount'].toString()); if (mounted) { setState(() { comments = (data['items'] as List).map((e) => Comment.fromJson(e)).toList(); }); } }).catchError((error) { Utils.showMessageDialog(context, error); }); } itemAddToCart(GlobalKey startKey) { if (_animationFinish == false) { return; } _animationFinish = false; print("start key " + startKey.toString()); print("stack key " + stackKey.toString()); print("end key " + endKey.toString()); _animationPointManager.addParabolicAniamtion( vsync: this, stackKey: stackKey, startKey: startKey, endKey: endKey, color: Colors.red, statusListener: (AnimationStatus status) { if (mounted) { if (status == AnimationStatus.completed) { // buyOnTap(); _animationFinish = true; setState(() {}); } } }, duration: Duration(milliseconds: 800), ); } @override Widget build(BuildContext context) { if (_business == null) { return new Scaffold( body: Center( child: SpinKitWave( color: Colors.lightBlueAccent, size: 40.0, ), ), ); } mediaQuery ??= MediaQuery.of(context); screenWidth ??= mediaQuery.size.width; screenHeight ??= mediaQuery.size.height; statusBarHeight ??= mediaQuery.padding.top; _sliverAppBarMaxHeight ??= screenHeight - 150.0; _pageScrollController ??= _shopCoordinator .pageScrollController(_sliverAppBarMaxHeight - _sliverAppBarInitHeight); _shopCoordinator.pinnedHeaderSliverHeightBuilder ??= () { return statusBarHeight + kToolbarHeight + _tabBarHeight; }; refresh = false; _slidUpShoppingCart = SlidingUpPanel( controller: panelController, minHeight: 50.0, maxHeight: 250.0, isDraggable: true, backdropEnabled: true, panel: ShoppingCartBar( business: _business, endKey: endKey, onEmptyCartListener: () { Future.delayed(Duration(seconds: 1), () { panelController.close(); }); }, onPanelOpenCloseRequest: () { if (panelController.isPanelOpen) { panelController.close(); } else { panelController.open(); } }, ), ); Row promotRow = Row( mainAxisAlignment: MainAxisAlignment.start, children: [], ); for (var i = 0; i < _business.promoProducts.length; i++) { promotRow.children.add(Container( padding: EdgeInsets.only( left: 10.0, top: 10.0, right: 10.0, bottom: 10.0, ), margin: EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(5.0)), color: Colors.white, border: Border( top: BorderSide( width: 1.0, color: Colors.black12, ), bottom: BorderSide( width: 1.0, color: Colors.black12, ), left: BorderSide( width: 1.0, color: Colors.black12, ), right: BorderSide( width: 1.0, color: Colors.black12, ), ), ), width: 160.0, height: 220.0, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( child: Container( child: Util.showImage( _business.promoProducts[i].imagePath, width: 110.0, ), ), onTap: () { _showProductDetail(_business.promoProducts[i]); }, ), Container( child: Text( _business.promoProducts[i].name, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14.0), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ShowPrice( _business.promoProducts[i].price, currencySign: '\$', regularPrice: _business.promoProducts[i].regularPrice, ), Container( child: AddRemoveButton( product: _business.promoProducts[i], business: _business, addOnly: true, ), ) ], ) ], ), )); } if (promotRow.children.length > 0) { promotHeader = SliverPersistentHeader( pinned: false, floating: true, delegate: _SliverAppBarDelegate2( minHeight: 256.0, maxHeight: 260.0, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only(left: 10.0, top: 5.0), child: Text( S.of(context).promotions, style: TextStyle(fontSize: 15.0), ), ), Container( padding: EdgeInsets.only(right: 10.0), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: promotRow, ), ), ], ), ), ); } else { promotHeader = SliverPersistentHeader( pinned: false, floating: true, delegate: _SliverAppBarDelegate2( minHeight: 0.0, maxHeight: 0.0, child: Center( child: Text(''), )), ); } Widget listener = Listener( onPointerUp: _shopCoordinator.onPointerUp, child: CustomScrollView( controller: _pageScrollController, physics: ClampingScrollPhysics(), slivers: [ SliverAppBar( pinned: true, floating: true, snap: true, leading: IconButton( icon: Icon(Icons.arrow_back_ios), onPressed: () { if (_animationFinish) { Navigator.of(context).maybePop(); } }), titleSpacing: 0.0, actions: [ Padding( padding: ButtonTheme.of(context).padding, child: GestureDetector( child: Icon(Icons.search), onTap: () { Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { return ProductSearch(_business); })); }, ), ), Padding( padding: ButtonTheme.of(context).padding, child: new GestureDetector( child: new Icon(Icons.phone), onTap: () { Utils.launchURL( 'tel:${Utils.getFirstNumberFromString(_business.phone)}'); }, ), ), ], title: Text( _business != null ? _business.name : '', style: TextStyle( color: Colors.white, ), ), backgroundColor: Theme.of(context).primaryColor, expandedHeight: _sliverAppBarMaxHeight, flexibleSpace: FlexibleSpaceBar( background: _buildAppbar(context), collapseMode: CollapseMode.none, ), ), promotHeader, SliverPersistentHeader( pinned: true, floating: false, delegate: _SliverAppBarDelegate2( minHeight: _tabBarHeight, maxHeight: _tabBarHeight, child: Container( color: Colors.white, child: TabBar( labelColor: new Color(0xFF3190E8), unselectedLabelColor: new Color(0xFF666666), indicatorColor: new Color(0xFF3190E8), indicatorSize: TabBarIndicatorSize.label, labelStyle: new TextStyle( fontSize: 16.0, ), controller: _tabController, tabs: [ Tab( text: S.of(context).products, ), Tab( child: Badge( badgeContent: Text( '${_business.commentsCount}', style: TextStyle(color: Colors.white, fontSize: 11.0), ), badgeColor: Colors.lightBlueAccent, child: Text(S.of(context).comments), ), ), ], ), ), ), ), SliverFillRemaining( child: TabBarView( controller: _tabController, children: [ _buildMainContent(), _buildCommentList(), ], ), ), // _slidUpShoppingCart, ], ), ); Widget mainBody = listener; Widget widget = Scaffold( key: _scaffoldKey, body: WillPopScope( child: mainBody, onWillPop: () async { if (_animationFinish) { return true; } return false; }, ), ); List children = [ Positioned( top: 0.0, right: 0.0, bottom: 50.0, left: 0.0, child: widget, ), _slidUpShoppingCart, ]; print("pointers: " + _animationPointManager.list.length.toString()); children += _animationPointManager.list; Stack stack = Stack( key: stackKey, children: [], ); stack.children.addAll(children); if (!_business.isPublic) { stack.children.add( Positioned( top: 0, right: 0, left: screenWidth - 100, bottom: screenHeight - 100, child: Image.asset( 'assets/images/under_renovation.png', width: 200.0, height: 200.0, fit: BoxFit.fill, ), ), ); } return stack; } SnackBar commentImageSnackBar(String url) { return SnackBar( elevation: 0.0, action: SnackBarAction( label: S.of(context).close, onPressed: () { ScaffoldMessenger.of(context).hideCurrentSnackBar(); }, ), content: Container( height: 260.0, child: Util.showImage( '$url', width: 180.0, height: 180.0, fit: BoxFit.fill, ), ), ); } CupertinoActionSheet commentImageActionSheet(String url) { return CupertinoActionSheet( message: Container( height: 280.0, child: Util.showImage( '$url', fit: BoxFit.fill, ), ), cancelButton: CupertinoActionSheetAction( child: Text(S.of(context).close), onPressed: () { Navigator.of(context).maybePop(); }, ), ); } Widget _buildCommentList() { Widget commentWidget = Center( child: Text(S.of(context).no_comments_yet), ); if (comments != null && comments.length > 0) { commentWidget = ListView.builder( controller: _listScrollController3, itemCount: comments.length, itemBuilder: (BuildContext context, int position) { Comment comment = comments[position]; Row imageRow = Row( mainAxisAlignment: MainAxisAlignment.start, children: [], ); if (comment.images.length > 0) { for (ProductImage image in comment.images) { imageRow.children.add( GestureDetector( child: Container( padding: EdgeInsets.all(5.0), child: Util.showImage( 'https:${image.image}', width: 40.0, height: 40.0, fit: BoxFit.fill, ), ), onTap: () { // _scaffoldKey.currentState.showSnackBar(commentImageSnackBar('https:${image.image}')); showCupertinoModalPopup( context: context, builder: (BuildContext context) { return commentImageActionSheet( 'https:${image.image}'); }); }, ), ); } } Widget replyWidget = SizedBox.shrink(); if (comment.replyFromStore != null && comment.replyFromStore.isNotEmpty) { replyWidget = Container( child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only(top: 10.0, bottom: 5.0), child: Text( S.of(context).response_from_store, style: TextStyle( fontSize: 12.0, color: Colors.black38, ), ), ), Container( padding: EdgeInsets.only(bottom: 16.0), child: ReadMoreText( comment.replyFromStore, style: TextStyle( fontSize: 13.0, color: Colors.black54, ), trimLines: 2, trimLength: 40, colorClickableText: Colors.black, trimMode: TrimMode.Line, trimCollapsedText: S.of(context).show_more, trimExpandedText: S.of(context).show_less, ), ) ], ), ); } return Container( padding: EdgeInsets.only( left: 16.0, right: 16.0, top: 16.0, bottom: 16.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only(bottom: 10.0), margin: EdgeInsets.only(bottom: 5.0), width: double.infinity, child: ReadMoreText( comment.content, trimLines: 3, trimLength: 66, colorClickableText: Colors.blue, trimMode: TrimMode.Length, trimCollapsedText: S.of(context).show_more, trimExpandedText: S.of(context).show_less, ), decoration: BoxDecoration( border: Border( bottom: BorderSide( width: 0.5, color: Colors.black12, ), ), ), ), Container( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( child: SmoothStarRating( starCount: 5, rating: comment.rating.toDouble(), size: 12.0, filledIconData: Icons.star, color: Colors.green, ), ), Container( child: Text( Utils.safeString(comment.contact) + ' ' + Utils.utcDatetimeStringToLocalDatetimeString( context, comment.createdAt), style: TextStyle( fontSize: 12.0, color: Colors.black26, ), ), ), ], ), ), Container( child: imageRow, ), replyWidget, ], ), decoration: BoxDecoration( border: Border( bottom: BorderSide( width: 10.0, color: Colors.black12, ), ), ), ); }); } return SmartRefresher( enablePullUp: true, enablePullDown: true, header: WaterDropHeader(), footer: CustomFooter( builder: (BuildContext context, LoadStatus mode) { Widget footer; if (mode == LoadStatus.idle) { footer = Text(S.of(context).pull_up_to_load_more); } else if (mode == LoadStatus.loading) { footer = CircularProgressIndicator(); } else if (mode == LoadStatus.failed) { footer = Text(S.of(context).load_failed_retry); } else if (mode == LoadStatus.canLoading) { footer = Text(S.of(context).release_to_load_more); } else if (mode == LoadStatus.noMore) { footer = Text(S.of(context).no_more_record); } else { footer = Text('...'); } return Container( height: 55.0, child: Center( child: footer, ), ); }, ), controller: _commentRefreshController, onRefresh: _onCommentRefresh, onLoading: _onCommentLoadMore, child: commentWidget, ); } Widget _buildAppbar(BuildContext context) { var addressRow = new Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [], ); addressRow.children.add(new Icon( Icons.location_on, size: 14.0, color: const Color(0xFFEEEEEE), )); var addressColumn = new Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [], ); addressColumn.children.add(new Text( _business.address.addressLine1 + (_business.address.addressLine2.isNotEmpty ? ' ' + _business.address.addressLine2 : ''), style: new TextStyle(fontSize: 13.0, color: const Color(0xFFEEEEEE)), )); addressColumn.children.add(new Text( _business.address.city + ', ' + _business.address.state + ', ' + _business.address.zip, style: new TextStyle(fontSize: 13.0, color: const Color(0xFFEEEEEE)), )); addressRow.children.add(addressColumn); var distanceRow = new Row( crossAxisAlignment: CrossAxisAlignment.center, children: [], ); if (_business.distanceInfo != null) { distanceRow.children.add(new Icon( Icons.directions_car, size: 14.0, color: Colors.white, )); distanceRow.children.add(new Text( _business.distanceInfo.distance != null ? _business.distanceInfo.distance.text : '***' + ' / ', style: new TextStyle(color: const Color(0xFFEEEEEE), fontSize: 13.0), )); var duration = Duration( seconds: (_business.distanceInfo.duration != null ? _business.distanceInfo.duration.value : 30) + _business.shippingTime * 60); var hours = duration.inHours.remainder(60); var minutes = duration.inMinutes.remainder(60); distanceRow.children.add(new Text( hours > 0 ? S.of(context).hour_token(hours) : '', style: new TextStyle(color: const Color(0xFFEEEEEE), fontSize: 13.0), )); distanceRow.children.add(new Text( minutes > 0 ? (hours > 0 ? ' ' : '') + S.of(context).minute_token(minutes) : '', style: new TextStyle(color: const Color(0xFFEEEEEE), fontSize: 13.0), )); distanceRow.children.add( new Text( ' / ' + S.of(context).min_order_amount_token(_business.minPrice) + ' ', style: new TextStyle(fontSize: 13.0, color: const Color(0xFFEEEEEE)), ), ); } Widget widget = new PreferredSize( preferredSize: new Size.fromHeight(90.0), child: new Container( height: 90.0, padding: new EdgeInsets.all(10.0), child: new Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ new Container( width: 72.0, height: 72.0, margin: new EdgeInsets.only(right: 10.0), child: Stack( children: [ Positioned( top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, child: GestureDetector( child: Util.showImage( '${_business.picUrl}', width: 72.0, height: 72.0, fit: BoxFit.fill, ), onTap: () { // Utils.addRemoveFavorite(_business.id); }, ), ), Positioned( top: 0.0, left: 0.0, right: -50.0, bottom: -50.0, child: Icon( Icons.favorite, color: Colors.transparent, ), ), ], ), ), new Expanded( child: new Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ addressRow, new Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ new Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ new Icon( Icons.phone, size: 14.0, color: Colors.white, ), new Text( _business.phone, style: new TextStyle( fontSize: 13.0, color: const Color(0xFFEEEEEE)), ) ], ), new Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ new Icon( Icons.av_timer, size: 14.0, color: Colors.white, ), new Text( _business.openingTime[0].openTime + ':00 - ' + _business.openingTime[0].closeTime + ':00', style: new TextStyle( fontSize: 13.0, color: const Color(0xFFEEEEEE)), ) ], ) ], ), distanceRow, ], ), ) ], ), ), ); List galleryPages = _buildBanners(context); slidingGellery = Carousel( height: 168.0, pages: galleryPages, autoPlay: true, ); Widget column = new Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _business.bulletin.isNotEmpty ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only(left: 10.0, right: 5.0, bottom: 10.0), child: Icon( Icons.announcement, size: 14.0, color: Colors.white, )), Container( width: mediaQuery.size.width - 30.0, padding: EdgeInsets.only(right: 10.0, bottom: 10.0), child: new Text( _business.bulletin.isEmpty ? '' : _business.bulletin, softWrap: true, style: new TextStyle( fontSize: 12.0, color: const Color(0xFFDDDDDD)), // overflow: TextOverflow.ellipsis, ), ), ], ) : SizedBox.shrink(), _business.description.isNotEmpty ? Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( padding: EdgeInsets.only(left: 10.0, right: 5.0, bottom: 10.0), child: Icon( Icons.store, size: 14.0, color: Colors.white, )), Container( padding: EdgeInsets.only(right: 10.0, bottom: 10.0), child: new Text( S.of(context).store_introduction, style: new TextStyle( fontSize: 13.0, color: const Color(0xFFDDDDDD), ), overflow: TextOverflow.ellipsis, ), ), ], ), Container( padding: EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 5.0), child: Text( _business.description, style: TextStyle( color: Colors.lightGreen, fontSize: 12.0, ), ), ), ], ) : SizedBox.shrink(), galleryPages.isNotEmpty ? Container( padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), child: slidingGellery, ) : SizedBox.shrink(), _business.policy.isNotEmpty ? Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( padding: EdgeInsets.only(left: 10.0, right: 5.0, bottom: 10.0), child: Icon( Icons.store, size: 14.0, color: Colors.white, )), Container( padding: EdgeInsets.only(right: 10.0, bottom: 10.0), child: new Text( S.of(context).store_policy, style: new TextStyle( fontSize: 13.0, color: const Color(0xFFDDDDDD), ), overflow: TextOverflow.ellipsis, ), ), ], ), Container( padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), child: SingleChildScrollView( child: Text( _business.policy, softWrap: true, style: TextStyle( fontSize: 10.0, color: const Color(0xFFEEEEEE), ), ), ), ), ], ) : SizedBox.shrink(), ], ); return Stack( children: [ Positioned( top: 0, left: 0, right: 0, bottom: 0, child: Util.showImage( _business.bannerImageUrl, fit: BoxFit.cover, ), ), Positioned( top: 0, left: 0, right: 0, bottom: 0, child: Container( color: Color(0x88000000), ), ), Positioned( top: kToolbarHeight + 20.0, left: 0, right: 0, child: widget, ), Positioned( top: kToolbarHeight + 20.0 + 90.0, left: 0, right: 0, bottom: 0, child: Container( child: SingleChildScrollView( child: column, ), ), ), ], ); } List _buildBanners(BuildContext context) { var pages = []; for (var i = 0; i < _business.slideImages.length; i++) { pages.add(new GestureDetector( child: new Container( child: Util.showImage( _business.slideImages[i].imageUrl, fit: BoxFit.cover, ), ), onTap: () {}, )); } return pages; } Widget _buildMainContent() { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildCategories(), Expanded( child: _buildProducts(), ), ], ); } Widget _buildCategories() { return new Container( width: 100.0, color: new Color(0xFFF5F5F5), child: new SizedBox.expand( child: new ListView.builder( physics: ClampingScrollPhysics(), controller: _listScrollController2, itemCount: _categoryProducts == null ? 0 : _categoryProducts.length, itemBuilder: (BuildContext context, int i) { CategoryProducts cp = _categoryProducts[i]; int qtyInCategory = 0; CartInfo cartInfo = Utils.getCartInfoByBusiness(store.state.cartInfos, _business); if (cartInfo != null && cartInfo.businessInfo.id == _business.id && cartInfo.productList != null) { for (var i = 0; i < cartInfo.productList.length; i++) { if (cartInfo.productList[i].product.categoryId == cp.id) { qtyInCategory += cartInfo.productList[i].quantity.round(); } } } Row categoryRow = Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [], ); categoryRow.children.add( Expanded( flex: 1, child: Container( margin: EdgeInsets.only(top: 5.0, bottom: 5.0), child: Text( cp.name, style: TextStyle( fontSize: 13.0, fontWeight: (cp.id == Constants.FEATURED_PRODUCT_ID || cp.id == Constants.HOT_SALE_ID) ? FontWeight.bold : FontWeight.normal, color: (cp.id == Constants.HOT_SALE_ID) ? Colors.redAccent : ((cp.id == Constants.FEATURED_PRODUCT_ID) ? Colors.lightGreen : Colors.black87)), // overflow: kIsWeb ? null : TextOverflow.ellipsis, overflow: TextOverflow.ellipsis, softWrap: true, maxLines: 2, ), ), ), ); if (qtyInCategory > 0) { categoryRow.children.add( new Badge( badgeContent: Text( '$qtyInCategory', style: TextStyle( color: Colors.white, fontSize: 11.0, ), ), animationType: BadgeAnimationType.scale, ), ); } else { categoryRow.children.add(SizedBox.shrink()); } return new GestureDetector( child: new Container( height: 70.0, padding: new EdgeInsets.symmetric(horizontal: 10.0), decoration: new BoxDecoration( color: _categoryIndex == i ? Colors.white : null, border: new Border( bottom: new BorderSide(color: new Color(0xFFEBEBEB)))), child: categoryRow, ), onTap: () => _selectCategory(i), ); }), ), ); } void _selectCategory(int index) { if (displayProductByCategoryClick && _categoryProducts[index].id > 0) { categoryId = _categoryProducts[index].id; loadProducts(); return; } double height = 0.0; for (int i = 0; i < index; ++i) { height += _categoryDescHeight + _categoryProducts[i].products.length * _productHeight; } if (height > _listScrollController1.position.maxScrollExtent) { height = _listScrollController1.position.maxScrollExtent; } _categoryIndexChange = true; _listScrollController1 .animateTo(height, duration: new Duration( microseconds: 200, ), curve: Curves.linear) .then((value) { _categoryIndexChange = false; }); print( 'height: $height, index: $index, ${_categoryProducts[0].products.length}'); if (mounted) { setState(() { _categoryIndex = index; }); } } CategoryProducts getCategoryProductByCategoryId(int cid) { for (CategoryProducts cp in _categoryProducts) { if (cp.id == cid) { return cp; } } return null; } void _resetProductListScroll() { if (_listScrollController1 != null && _listScrollController1.positions.isNotEmpty) { _listScrollController1.animateTo( 0, duration: new Duration( microseconds: 200, ), curve: Curves.linear, ); } } int _getCategoryIndexByRightScrollHeight(double height) { double cHeight = 0.0; if (height > 0) { for (int i = 0; i < _categoryProducts.length; ++i) { double categoryHeight = _categoryDescHeight + _categoryProducts[i].products.length * _productHeight; if (height >= cHeight && height < cHeight + categoryHeight) { return i; } cHeight += categoryHeight; } } if (height > cHeight) { return _categoryProducts.length - 1; } return -1; } void _setCategoryByProductScrolledIndex(int index) { if (index == _categoryIndex) { return; } double height = 0.0; for (int i = 0; i < index; ++i) { height += _categoryHeight; } if (height > _listScrollController2.position.maxScrollExtent) { height = _listScrollController2.position.maxScrollExtent; } _listScrollController2.animateTo(height, duration: new Duration( milliseconds: 200, ), curve: Curves.linear); if (mounted) { setState(() { _categoryIndex = index; }); } } int numCategoriesHasProducts() { int num = 0; for (CategoryProducts cp in _categoryProducts) { if (cp.products.length > 0) { num += 1; } } return num; } Widget _buildProducts() { return new SizedBox.expand( child: new NotificationListener( onNotification: (ScrollUpdateNotification notification) { if (!displayProductByCategoryClick) { if (_categoryIndexChange) { return; } int index = _getCategoryIndexByRightScrollHeight( notification.metrics.pixels); _setCategoryByProductScrolledIndex(index); } return; }, child: new ListView.builder( physics: ClampingScrollPhysics(), controller: _listScrollController1, // itemCount: displayProductByCategoryClick ? 1 : (_categoryProducts == null ? 0 : _categoryProducts.length), itemCount: _categoryProducts == null ? 0 : numCategoriesHasProducts(), itemBuilder: (BuildContext context, int i) { CategoryProducts cp; cp = _categoryProducts[i]; int index = -1; if (displayProductByCategoryClick) { if (categoryId > 0) { for (var j = 0; j < _categoryProducts.length; j++) { if (_categoryProducts[j].id == categoryId) { cp = _categoryProducts[j]; index = j; break; } } if (cp == null) { index = 0; cp = _categoryProducts[0]; } } WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _setCategoryByProductScrolledIndex(index); }); } if (cp.products.length == 0) { return SizedBox.shrink(); } Column col = new Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [], ); col.children.add(new Container( height: _categoryDescHeight, padding: new EdgeInsets.symmetric(horizontal: 10.0), color: new Color(0xFFF5F5F5), child: new Row( children: [ new Expanded( child: new Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, textBaseline: TextBaseline.alphabetic, children: [ new Padding( padding: new EdgeInsets.only(right: 5.0), child: new Text( cp.name, style: new TextStyle( fontSize: 16.0, fontWeight: (cp.id == Constants.FEATURED_PRODUCT_ID || cp.id == Constants.HOT_SALE_ID) ? FontWeight.bold : FontWeight.normal, color: (cp.id == Constants.HOT_SALE_ID) ? Colors.redAccent : ((cp.id == Constants.FEATURED_PRODUCT_ID) ? Colors.lightGreen : Colors.black87)), overflow: TextOverflow.ellipsis, ), ), new Visibility( visible: cp.description.isNotEmpty, child: new Text( cp.description, overflow: TextOverflow.ellipsis, style: new TextStyle( fontSize: 10.0, color: new Color(0xFF999999)), ), replacement: const SizedBox.shrink()), ], )) ], ))); for (var p in cp.products) { var pStack = new Stack( children: [], ); pStack.children.add(Container( // width: MediaQuery.of(context).size.width - 100.0, child: ProductItem( p, _business, horizontal: true, imageWidth: 80.0, ), )); col.children.add(pStack); } if (displayProductByCategoryClick) { if (displayProductByCategoryClickIndicator.isNotEmpty) { col.children.add( Container( padding: EdgeInsets.all(12.0), child: Center( child: Text( displayProductByCategoryClickIndicator, style: TextStyle( color: Colors.black54, ), ), ), ), ); } else { if (cp.products.length < Constants.ORDERS_PER_PAGE) { col.children.add( Container( padding: EdgeInsets.all(12.0), child: Center( child: Text( S.of(context).end_of_the_list, style: TextStyle( color: Colors.black54, ), ), ), ), ); } else { col.children.add( Container( padding: EdgeInsets.all(12.0), child: Center( child: Text( S.of(context).pull_up_to_load_more, style: TextStyle( color: Colors.black54, ), ), ), ), ); } } } return col; }), ), ); } @override void initState() { super.initState(); _shopCoordinator = ShopScrollCoordinator(); _tabController = TabController(vsync: this, length: 2); refresh = false; _listScrollController1 = _shopCoordinator.newChildScrollController(); _listScrollController2 = _shopCoordinator.newChildScrollController(); _listScrollController3 = _shopCoordinator.newChildScrollController(); _listScrollController1.addListener(() { if (_listScrollController1.position.atEdge) { if (_listScrollController1.position.pixels == 0) { print('product list at top'); } else { print('product list at bottom'); if (displayProductByCategoryClick && _productCurrentPage != 0) { loadMoreProducts(); } } } }); eventBus.on().listen((event) { if (mounted) { setState(() { refresh = true; }); } }); loadProducts(); } void loadProducts() { if (categoryId < 0) { return; } if (_categoryProducts != null) { _isLoading = true; Utils.showLoadingDialog(context, message: S.of(context).loading); } _productCurrentPage = 1; Utils.loadProducts( widget.businessId, categoryId, _productCurrentPage, false, (value) { if (_isLoading) { _isLoading = false; Navigator.of(context).pop(); } if (mounted) { setState(() { displayProductByCategoryClick = value['display_product_by_category_click']; displayProductByCategoryClickIndicator = ''; _business = Business.fromJson(value['business']); _categoryProducts = (value['category_products'] as List) .map((i) => CategoryProducts.fromJson(i)) .toList(); // _featuredProducts = (value['featured_products'] as List) // .map((i) => Product.fromJson(i)) // .toList(); _featuredProducts = []; // _hotSaleProducts = (value['hot_sale_products'] as List) // .map((i) => Product.fromJson(i)) // .toList(); _hotSaleProducts = []; _prompts = value['prompt'] as List; if (_hotSaleProducts.length > 0) { CategoryProducts hs = CategoryProducts( Constants.HOT_SALE_ID, widget.businessId, S.of(context).hot_sale, '', '', _hotSaleProducts); _categoryProducts.insert(0, hs); } if (_featuredProducts.length > 0) { CategoryProducts fe = CategoryProducts( Constants.FEATURED_PRODUCT_ID, widget.businessId, S.of(context).featured_product, '', '', _featuredProducts); _categoryProducts.insert(0, fe); } checkActionAndClose(context); if (displayProductByCategoryClick) { _resetProductListScroll(); } }); } }, (error) { print('error: $error'); if (_isLoading) { _isLoading = false; Navigator.of(context).maybePop(); } Utils.showMessageDialog(context, error); } ); } void loadMoreProducts() { if (_productCurrentPage == 0 || categoryId <= 0) { return; } _productCurrentPage += 1; Utils.loadProducts(widget.businessId, categoryId, _productCurrentPage, true, (value) { if (_isLoading) { _isLoading = false; Navigator.of(context).maybePop(); } if (mounted) { setState(() { List moreCategoryProducts = (value as List).map((i) => CategoryProducts.fromJson(i)).toList(); if (moreCategoryProducts.isEmpty) { _productCurrentPage = 0; displayProductByCategoryClickIndicator = S.of(context).end_of_the_list; } else { if (moreCategoryProducts[0].products.length < Constants.ORDERS_PER_PAGE) { _productCurrentPage = 0; displayProductByCategoryClickIndicator = S.of(context).end_of_the_list; } else { displayProductByCategoryClickIndicator = S.of(context).pull_up_to_load_more; } CategoryProducts currentCp = getCategoryProductByCategoryId(categoryId); if (currentCp != null) { currentCp.products.addAll(moreCategoryProducts[0].products); } else { _productCurrentPage = 0; displayProductByCategoryClickIndicator = S.of(context).end_of_the_list; } } }); } }, (error) { print('error: $error'); if (_isLoading) { _isLoading = false; Navigator.of(context).maybePop(); } Utils.showMessageDialog(context, error); } ); } CartLineItem _newCartLineItem( {int id, double price, Product product, String name, String description, double quantity}) { CartLineItem lineItem = CartLineItem(); lineItem.unitPrice = price; lineItem.product = product; lineItem.name = product.name; lineItem.description = description; lineItem.quantity = quantity; return lineItem; } @override void dispose() { print('Shop disposed!!!'); // onProductWillAddToCartSubscription?.cancel(); // onProductWillRemoveFromCartSubscription?.cancel(); _tabController?.dispose(); _pageScrollController?.dispose(); _commentRefreshController?.dispose(); _listScrollController1?.dispose(); _listScrollController2?.dispose(); _listScrollController3?.dispose(); _animationPointManager?.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); } void _showProductDetail(Product p) { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductDetailPage( product: p, business: _business, )), ); } void checkActionAndClose(BuildContext mainContext) { // if (widget.action == Constants.ACTION_COUPON && store.state.referrerContactId != null && store.state.referrerContactId > 0) { // showDialog( // context: context, // barrierDismissible: false, // builder: (BuildContext context) { // return AlertDialog( // title: Text(S.of(context).coupon), // content: Text( // S.of(context).a_friend_send_you_a_coupon // ), // actions: [ // FlatButton( // child: Text(S.of(context).no_thanks), // onPressed: () { // Navigator.of(context).pop(); // checkClose(mainContext, _business); // }, // ), // RaisedButton( // child: Text( // S.of(context).yes, // style: TextStyle( // color: Colors.white, // ), // ), // color: Theme.of(context).primaryColor, // onPressed: () { // Utils.showLoadingDialog(mainContext, message: S.of(context).processing); // HttpUtil.httpGet('v1/get-referrer-coupon', // businessId: _business.id, // queryParameters: { // 'referrer_contact_id': store.state.referrerContactId, // } // ).then((data) { // Navigator.of(context).pop(); // Coupon coupon = Coupon.fromJson(data); // showDialog( // context: mainContext, // barrierDismissible: false, // builder: (BuildContext context) { // return AlertDialog( // title: Text( // S.of(context).congratulation, // ), // content: Text( // S.of(context).got_a_coupon_token(coupon.valueAmount) // ), // actions: [ // FlatButton( // child: Text( // S.of(context).close, // ), // onPressed: () { // Navigator.of(context).pop(); // Navigator.of(context).pop(); // }, // ), // ], // ); // } // ); // }).catchError((error) { // Navigator.of(context).pop(); // Navigator.of(context).pop(); // if (error is DioError) { // if (error.response.data['code'] == 1013) { // showDialog( // context: mainContext, // barrierDismissible: false, // builder: (BuildContext context) { // return AlertDialog( // title: Text(S.of(context).error), // content: Text(error.response.data['message']), // actions: [ // FlatButton( // child: Text(S.of(context).cancel), // onPressed: () { // Navigator.of(context).pop(); // }, // ), // RaisedButton( // color: Theme.of(context).primaryColor, // child: Text( // S.of(context).login, // style: TextStyle( // color: Colors.white, // ), // ), // onPressed: () { // Navigator.of(context).pop(); // store.dispatch(UpdateRedirectRoute('/shop/${widget.businessId}/${widget.deviceId}/${widget.tableNo}/${widget.action}')); // Routes.router.navigateTo(context, '/login'); // }, // ), // ], // ); // } // ); // } else { // Utils.showMessageDialog(mainContext, error); // } // } else { // Utils.showMessageDialog(mainContext, error); // } // }); // }, // ) // ], // ); // } // ); // } else { // checkClose(context, _business); // } } void checkClose(BuildContext context, Business business) { if (checkCloseFlag) { return; } checkCloseFlag = true; if (business.forceClose) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(S.of(context).warning), content: Text( S.of(context).store_closed, ), actions: [ TextButton( child: Text(S.of(context).ok), onPressed: () { Navigator.of(context).maybePop(); showPrompt(0); }, ) ], ); }); } else { DateTime now = DateTime.now(); if (!(now.hour >= business.todayOpen && now.hour <= business.todayClose)) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( // title: Text( // S.of(context).warning, // ), content: Container( height: 170.0, child: Center( child: Column( children: [ Container( child: Icon( Icons.add_shopping_cart, size: 100.0, color: Colors.black12, ), ), Container( child: Text( S.of(context).closed, style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, ), ), ), Container( child: Text( S.of(context).book_now_delivery_later_token( business.todayOpen), style: TextStyle( color: Colors.black38, ), ), ), ], ), ), ), actions: [ TextButton( child: Text(S.of(context).ok), onPressed: () { Navigator.of(context).maybePop(); showPrompt(0); }, ) ], ); }); } else { showPrompt(0); } } } void showPrompt(int idx) { if (_prompts.length > idx) { Map _prompt = _prompts[idx] as Map; showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(_prompt['title']), content: Text(_prompt['message']), actions: [ TextButton( child: Text(S.of(context).ok), onPressed: () { Navigator.of(context).maybePop(); showPrompt(idx + 1); }, ), ], ); }); } } @override bool get wantKeepAlive => true; } class _SliverAppBarDelegate2 extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate2({ @required this.minHeight, @required this.maxHeight, @required this.child, }); final double minHeight; final double maxHeight; final Widget child; @override double get minExtent => this.minHeight; @override double get maxExtent => max(maxHeight, minHeight); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand(child: child); } @override bool shouldRebuild(_SliverAppBarDelegate2 oldDelegate) { return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child; } }