From 61aea521611b986c33dcc259e7ca338e5e45fe6c Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Tue, 18 Jan 2022 18:16:25 +0330 Subject: [PATCH] component updates --- .../authentication/screens/username.dart | 1 + .../widgets/authentication_app_bar.dart | 3 +- lib/widgets/bookmark_button.dart | 42 ++++++ lib/widgets/didvan/app_bar.dart | 3 +- lib/widgets/didvan/page_view.dart | 14 +- lib/widgets/didvan/text_field.dart | 23 ++- lib/widgets/floating_navigation_bar.dart | 114 +++++++++------ lib/widgets/search_field.dart | 8 +- lib/widgets/skeleton_image.dart | 135 ++++++++++++++++++ lib/widgets/skeletun_image.dart | 99 ------------- 10 files changed, 290 insertions(+), 152 deletions(-) create mode 100644 lib/widgets/bookmark_button.dart create mode 100644 lib/widgets/skeleton_image.dart delete mode 100644 lib/widgets/skeletun_image.dart diff --git a/lib/pages/authentication/screens/username.dart b/lib/pages/authentication/screens/username.dart index fb0bf7d..395db9d 100644 --- a/lib/pages/authentication/screens/username.dart +++ b/lib/pages/authentication/screens/username.dart @@ -29,6 +29,7 @@ class _UsernameInputState extends State { title: 'نام کاربری یا شماره موبایل', hintText: 'نام کاربری یا شماره موبایل', textAlign: TextAlign.center, + acceptSpace: false, onSubmitted: (value) => state.confirmUsername(), validator: (value) { if (value.contains(' ')) { diff --git a/lib/pages/authentication/widgets/authentication_app_bar.dart b/lib/pages/authentication/widgets/authentication_app_bar.dart index 03489c2..e9cb490 100644 --- a/lib/pages/authentication/widgets/authentication_app_bar.dart +++ b/lib/pages/authentication/widgets/authentication_app_bar.dart @@ -1,4 +1,5 @@ import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/pages/authentication/authentication_state.dart'; import 'package:didvan/widgets/didvan/icon_button.dart'; import 'package:didvan/widgets/didvan/text.dart'; @@ -15,7 +16,7 @@ class AuthenticationAppBar extends StatelessWidget { return Row( children: [ DidvanIconButton( - icon: Icons.arrow_back, + icon: DidvanIcons.back_regular, onPressed: () { if (state.currentPageIndex == 0) { Navigator.of(context).pop(); diff --git a/lib/widgets/bookmark_button.dart b/lib/widgets/bookmark_button.dart new file mode 100644 index 0000000..6fae1b9 --- /dev/null +++ b/lib/widgets/bookmark_button.dart @@ -0,0 +1,42 @@ +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/widgets/didvan/icon_button.dart'; +import 'package:flutter/material.dart'; + +class BookmarkButton extends StatefulWidget { + final bool value; + final VoidCallback onMark; + final VoidCallback onUnmark; + const BookmarkButton( + {Key? key, + required this.value, + required this.onMark, + required this.onUnmark}) + : super(key: key); + + @override + State createState() => _BookmarkButtonState(); +} + +class _BookmarkButtonState extends State { + bool _value = false; + + @override + void initState() { + _value = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DidvanIconButton( + gestureSize: 24, + icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular, + onPressed: () { + setState(() { + _value = !_value; + }); + _value ? widget.onMark() : widget.onUnmark(); + }, + ); + } +} diff --git a/lib/widgets/didvan/app_bar.dart b/lib/widgets/didvan/app_bar.dart index d4ca0df..be5fc38 100644 --- a/lib/widgets/didvan/app_bar.dart +++ b/lib/widgets/didvan/app_bar.dart @@ -1,4 +1,5 @@ import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; @@ -16,7 +17,7 @@ class DidvanAppBar extends StatelessWidget { IconButton( onPressed: () => Navigator.of(context).pop(), color: Theme.of(context).colorScheme.title, - icon: const Icon(Icons.arrow_back), + icon: const Icon(DidvanIcons.back_regular), ), const SizedBox(width: 16), Expanded( diff --git a/lib/widgets/didvan/page_view.dart b/lib/widgets/didvan/page_view.dart index a1abbdc..c455540 100644 --- a/lib/widgets/didvan/page_view.dart +++ b/lib/widgets/didvan/page_view.dart @@ -5,8 +5,15 @@ import 'package:flutter/material.dart'; class DidvanPageView extends StatelessWidget { final List pages; final ScrollController? scrollController; - const DidvanPageView({Key? key, required this.pages, this.scrollController}) - : super(key: key); + final int initialPage; + final void Function(int index) onPageChanged; + const DidvanPageView({ + Key? key, + required this.pages, + this.scrollController, + required this.onPageChanged, + this.initialPage = 2, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -14,6 +21,7 @@ class DidvanPageView extends StatelessWidget { return CarouselSlider.builder( itemCount: pages.length, options: CarouselOptions( + onPageChanged: (index, reason) => onPageChanged(index), height: double.infinity, initialPage: 2, viewportFraction: 0.94, @@ -22,7 +30,7 @@ class DidvanPageView extends StatelessWidget { itemBuilder: (context, index, realIndex) => SizedBox( height: MediaQuery.of(context).size.height, child: SingleChildScrollView( - controller: scrollController, + controller: index == 2 ? scrollController : null, physics: const BouncingScrollPhysics(), padding: EdgeInsets.only( left: 4, diff --git a/lib/widgets/didvan/text_field.dart b/lib/widgets/didvan/text_field.dart index 9186744..608c40b 100644 --- a/lib/widgets/didvan/text_field.dart +++ b/lib/widgets/didvan/text_field.dart @@ -4,6 +4,7 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/widgets/animated_visibility.dart'; import 'package:didvan/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; class DidvanTextField extends StatefulWidget { @@ -16,6 +17,7 @@ class DidvanTextField extends StatefulWidget { final String? hintText; final dynamic initialValue; final bool obsecureText; + final bool acceptSpace; final Function(String value)? validator; final TextInputType? textInputType; const DidvanTextField({ @@ -31,6 +33,7 @@ class DidvanTextField extends StatefulWidget { this.obsecureText = false, this.autoFocus = false, this.onSubmitted, + this.acceptSpace = true, }) : super(key: key); @override @@ -79,6 +82,10 @@ class _DidvanTextFieldState extends State { border: Border.all(color: _borderColor()), ), child: TextFormField( + inputFormatters: [ + if (!widget.acceptSpace) + FilteringTextInputFormatter.allow(RegExp("[0-9a-zA-Z]")), + ], autofocus: widget.autoFocus, obscureText: _hideContent, textAlign: widget.textAlign, @@ -89,7 +96,10 @@ class _DidvanTextFieldState extends State { onChanged: _onChanged, validator: _validator, obscuringCharacter: '*', - style: Theme.of(context).textTheme.bodyText1, + style: Theme.of(context) + .textTheme + .bodyText2! + .copyWith(fontFamily: 'Dana'), decoration: InputDecoration( suffixIcon: _suffixBuilder(), enabled: widget.enabled, @@ -181,9 +191,7 @@ class _DidvanTextFieldState extends State { _error = null; }); value = value.toEnglishDigit(); - if (widget.onChanged != null) { - widget.onChanged!(value); - } + widget.onChanged?.call(value); } String? _validator(String? value) { @@ -193,6 +201,7 @@ class _DidvanTextFieldState extends State { setState(() { _error = error; }); + return ''; } else { setState(() { _error = null; @@ -200,4 +209,10 @@ class _DidvanTextFieldState extends State { } } } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } } diff --git a/lib/widgets/floating_navigation_bar.dart b/lib/widgets/floating_navigation_bar.dart index e5b348d..4d8e7d4 100644 --- a/lib/widgets/floating_navigation_bar.dart +++ b/lib/widgets/floating_navigation_bar.dart @@ -3,6 +3,12 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/news_details_data.dart'; import 'package:didvan/models/radar_details_data.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/pages/home/profile/widgets/menu_item.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/widgets/bookmark_button.dart'; +import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/icon_button.dart'; import 'package:didvan/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; @@ -11,13 +17,15 @@ class FloatingNavigationBar extends StatefulWidget { final RadarDetailsData? radar; final NewsDetailsData? news; final ScrollController scrollController; - final VoidCallback bookmarkCallback; + final VoidCallback onMark; + final VoidCallback onUnmark; const FloatingNavigationBar({ Key? key, this.radar, this.news, required this.scrollController, - required this.bookmarkCallback, + required this.onMark, + required this.onUnmark, }) : super(key: key); @override @@ -25,8 +33,6 @@ class FloatingNavigationBar extends StatefulWidget { } class _FloatingNavigationBarState extends State { - bool _isMarked = false; - bool get _isRadar => widget.radar != null; bool _isScrolled = false; @@ -34,7 +40,6 @@ class _FloatingNavigationBarState extends State { @override void initState() { - _isMarked = _item.marked; widget.scrollController.addListener(() { final position = widget.scrollController.position.pixels; if (position > 300 && !_isScrolled) { @@ -57,9 +62,9 @@ class _FloatingNavigationBarState extends State { : Theme.of(context).colorScheme.focused; return Container( margin: const EdgeInsets.only(left: 32, right: 32, bottom: 20), + padding: const EdgeInsets.symmetric(horizontal: 12), width: double.infinity, height: 48, - padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.navigation, borderRadius: BorderRadius.circular(24), @@ -75,7 +80,7 @@ class _FloatingNavigationBarState extends State { ), child: Row( children: [ - IconButton( + DidvanIconButton( onPressed: () { if (_isScrolled) { widget.scrollController.animateTo( @@ -87,63 +92,86 @@ class _FloatingNavigationBarState extends State { } Navigator.of(context).pop(); }, - icon: Icon( - _isScrolled ? Icons.arrow_upward : Icons.arrow_back, - ), + icon: _isScrolled + ? DidvanIcons.arrow_up_regular + : DidvanIcons.back_regular, ), const Spacer(), if (_isRadar) - DidvanIconButton( - onPressed: () { - setState(() { - _isMarked = !_isMarked; - }); - widget.bookmarkCallback(); - }, - icon: _isMarked - ? DidvanIcons.bookmark_solid - : DidvanIcons.bookmark_regular, - ), - if (_isRadar) - IconButton( - onPressed: () {}, - icon: const Icon( - DidvanIcons.evaluation_regular, - ), + BookmarkButton( + value: _item.marked, + onMark: widget.onMark, + onUnmark: widget.onUnmark, ), + // if (_isRadar) + // IconButton( + // onPressed: () {}, + // icon: const Icon( + // DidvanIcons.evaluation_regular, + // ), + // ), SizedBox( width: 48, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - DidvanText( - '2', - color: foregroundColor, - ), + if (_item.comments != 0) + DidvanText( + _item.comments.toString(), + color: foregroundColor, + ), const SizedBox(width: 4), - const Icon( - DidvanIcons.chats_regular, + DidvanIconButton( + gestureSize: 32, + onPressed: () => Navigator.of(context).pushNamed( + Routes.comments, + arguments: {'id': _item.id, 'isRadar': _isRadar}, + ), + icon: DidvanIcons.chats_regular, ), ], ), ), + if (!_isRadar) const SizedBox(width: 12), if (!_isRadar) - IconButton( - onPressed: () {}, - icon: const Icon( - DidvanIcons.bookmark_regular, - ), + BookmarkButton( + value: _item.marked, + onMark: widget.onMark, + onUnmark: widget.onUnmark, ), if (_isRadar) - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.more_horiz, - ), + DidvanIconButton( + gestureSize: 32, + onPressed: _showMoreOptions, + icon: DidvanIcons.menu_regular, ), ], ), ), ); } + + void _showMoreOptions() { + ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + content: Column( + children: [ + MenuItem( + title: 'ارتباط با سردبیر', + onTap: () {}, + icon: DidvanIcons.profile_regular, + ), + const DidvanDivider(), + MenuItem( + title: 'گزارش اشکال', + onTap: () {}, + icon: DidvanIcons.description_regular, + ), + ], + ), + title: 'موارد بیشتر', + withoutButtonMode: true, + ), + ); + } } diff --git a/lib/widgets/search_field.dart b/lib/widgets/search_field.dart index 20c0436..9c8fd38 100644 --- a/lib/widgets/search_field.dart +++ b/lib/widgets/search_field.dart @@ -43,7 +43,7 @@ class _SearchFieldState extends State { decoration: BoxDecoration( color: _fillColor(), ), - child: TextField( + child: TextFormField( focusNode: _focusNode, style: Theme.of(context).textTheme.bodyText1, textAlignVertical: TextAlignVertical.center, @@ -122,4 +122,10 @@ class _SearchFieldState extends State { } return Theme.of(context).colorScheme.surface; } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } } diff --git a/lib/widgets/skeleton_image.dart b/lib/widgets/skeleton_image.dart new file mode 100644 index 0000000..eda8f80 --- /dev/null +++ b/lib/widgets/skeleton_image.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:didvan/services/storage/storage.dart'; +import 'package:didvan/widgets/shimmer_placeholder.dart'; +import 'package:http/http.dart' as http; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:skeleton_text/skeleton_text.dart'; + +class SkeletonImage extends StatefulWidget { + final String imageUrl; + final double width; + final double height; + final BorderRadius? borderRadius; + final double? aspectRatio; + const SkeletonImage({ + Key? key, + required this.imageUrl, + this.width = 300, + this.height = 140, + this.borderRadius = DesignConfig.lowBorderRadius, + this.aspectRatio, + }) : super(key: key); + + @override + State createState() => _SkeletonImageState(); +} + +class _SkeletonImageState extends State { + Uint8List? _bytes; + + @override + void initState() { + if (kIsWeb) _getImage(); + super.initState(); + } + + Future _getImage() async { + final url = RequestHelper.baseUrl + widget.imageUrl; + final storage = StorageService.webStorage; + String? imageCache = storage['image-cache']; + final Map data = imageCache == null ? {} : jsonDecode(imageCache); + if (data.containsKey(url)) { + _bytes = Uint8List.fromList( + List.from(data[url]), + ); + } else { + _bytes = (await http.get( + Uri.parse(url), + headers: {'Authorization': 'Bearer ${RequestService.token}'}, + )) + .bodyBytes; + addImageToStorage(); + } + if (mounted) { + setState(() {}); + } + } + + void addImageToStorage() { + final storage = StorageService.webStorage; + String? imageCache = storage['image-cache']; + final Map data = imageCache == null ? {} : Map.from(jsonDecode(imageCache)); + data.addAll({RequestHelper.baseUrl + widget.imageUrl: _bytes}); + StorageService.webStorage.addAll({'image-cache': jsonEncode(data)}); + } + + @override + Widget build(BuildContext context) { + return _aspectRatioGenerator( + child: Builder(builder: (context) { + if (kIsWeb) { + return Builder( + builder: (context) { + if (_bytes == null || _bytes!.isEmpty) { + return ShimmerPlaceholder( + borderRadius: widget.borderRadius, + ); + } + return ClipRRect( + borderRadius: widget.borderRadius, + child: Image.memory( + _bytes!, + width: widget.width, + height: widget.height, + fit: BoxFit.cover, + ), + ); + }, + ); + } + return CachedNetworkImage( + httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, + width: widget.width, + height: widget.height, + imageUrl: RequestHelper.baseUrl + widget.imageUrl, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: widget.borderRadius ?? DesignConfig.lowBorderRadius, + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + progressIndicatorBuilder: (context, url, progress) => + SkeletonAnimation( + shimmerColor: Theme.of(context).colorScheme.border, + borderRadius: widget.borderRadius ?? DesignConfig.lowBorderRadius, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.disabledBackground, + borderRadius: widget.borderRadius, + ), + ), + ), + ); + }), + ); + } + + Widget _aspectRatioGenerator({required Widget child}) => + widget.aspectRatio == null + ? SizedBox(child: child) + : AspectRatio( + aspectRatio: widget.aspectRatio!, + child: child, + ); +} diff --git a/lib/widgets/skeletun_image.dart b/lib/widgets/skeletun_image.dart deleted file mode 100644 index c1fe353..0000000 --- a/lib/widgets/skeletun_image.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:typed_data'; - -import 'package:didvan/widgets/shimmer_placeholder.dart'; -import 'package:http/http.dart' as http; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:didvan/config/design_config.dart'; -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/services/network/request.dart'; -import 'package:didvan/services/network/request_helper.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:skeleton_text/skeleton_text.dart'; - -class SkeletonImage extends StatefulWidget { - final String imageUrl; - final double width; - final double height; - final BorderRadius? borderRadius; - const SkeletonImage({ - Key? key, - required this.imageUrl, - required this.width, - required this.height, - this.borderRadius = DesignConfig.lowBorderRadius, - }) : super(key: key); - - @override - State createState() => _SkeletonImageState(); -} - -class _SkeletonImageState extends State { - Uint8List? _bytes; - - @override - void initState() { - if (kIsWeb) _getImage(); - super.initState(); - } - - Future _getImage() async { - _bytes = (await http.get( - Uri.parse(RequestHelper.baseUrl + widget.imageUrl), - headers: {'Authorization': 'Bearer ${RequestService.token}'}, - )) - .bodyBytes; - if (mounted) { - setState(() {}); - } - } - - @override - Widget build(BuildContext context) { - if (kIsWeb) { - if (_bytes == null || _bytes!.isEmpty) { - return ShimmerPlaceholder( - width: widget.width, - height: widget.height, - borderRadius: widget.borderRadius, - ); - } - return ClipRRect( - borderRadius: widget.borderRadius, - child: Image.memory( - _bytes!, - width: widget.width, - height: widget.height, - fit: BoxFit.cover, - ), - ); - } - return CachedNetworkImage( - httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}, - width: widget.width, - height: widget.height, - imageUrl: RequestHelper.baseUrl + widget.imageUrl, - imageBuilder: (context, imageProvider) => Container( - decoration: BoxDecoration( - borderRadius: widget.borderRadius ?? DesignConfig.lowBorderRadius, - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - ), - ), - ), - progressIndicatorBuilder: (context, url, progress) => SkeletonAnimation( - shimmerColor: Theme.of(context).colorScheme.border, - borderRadius: widget.borderRadius ?? DesignConfig.lowBorderRadius, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.disabledBackground, - borderRadius: widget.borderRadius, - ), - height: widget.height, - width: widget.width, - ), - ), - ); - } -}