diff --git a/lib/widgets/bookmark_button.dart b/lib/widgets/bookmark_button.dart index c7a72cd..b1b0716 100644 --- a/lib/widgets/bookmark_button.dart +++ b/lib/widgets/bookmark_button.dart @@ -4,15 +4,13 @@ import 'package:flutter/material.dart'; class BookmarkButton extends StatefulWidget { final bool value; - final VoidCallback onMark; - final VoidCallback onUnmark; + final void Function(bool value) onMarkChanged; final bool bigGestureSize; const BookmarkButton({ Key? key, required this.value, - required this.onMark, - required this.onUnmark, this.bigGestureSize = false, + required this.onMarkChanged, }) : super(key: key); @override @@ -43,7 +41,7 @@ class _BookmarkButtonState extends State { setState(() { _value = !_value; }); - _value ? widget.onMark() : widget.onUnmark(); + widget.onMarkChanged(_value); }, ); } diff --git a/lib/widgets/didvan/app_bar.dart b/lib/widgets/didvan/app_bar.dart index 569668e..764e588 100644 --- a/lib/widgets/didvan/app_bar.dart +++ b/lib/widgets/didvan/app_bar.dart @@ -36,6 +36,8 @@ class DidvanAppBar extends StatelessWidget { if (appBarData.subtitle != null) DidvanText( appBarData.subtitle!, + maxLines: 1, + overflow: TextOverflow.clip, style: Theme.of(context).textTheme.overline, color: Theme.of(context).colorScheme.caption, ), diff --git a/lib/widgets/didvan/page_view.dart b/lib/widgets/didvan/page_view.dart index f4cbfa5..ef66441 100644 --- a/lib/widgets/didvan/page_view.dart +++ b/lib/widgets/didvan/page_view.dart @@ -124,7 +124,18 @@ class _DidvanPageViewState extends State { Widget _contentBuilder(dynamic item, int index) { final content = item.contents[index]; if (content.text != null) { - return Html(data: item.contents[index].text!); + return Html( + data: content.text, + style: { + '*': Style( + direction: TextDirection.rtl, + lineHeight: LineHeight.percent(135), + // textAlign: TextAlign.justify, + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + ), + }, + ); } if (content.image != null) { return SkeletonImage( @@ -147,11 +158,13 @@ class _DidvanPageViewState extends State { for (var i = 0; i < item.categories.length; i++) DidvanText( item.categories[i].label + - ' - ' + - DateTimeUtils.momentGenerator(item.createdAt) + '${i != item.categories.length - 1 ? '،' : ''} ', style: Theme.of(context).textTheme.caption, ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(item.createdAt), + style: Theme.of(context).textTheme.caption, + ), ], ); } else { diff --git a/lib/widgets/didvan/switch.dart b/lib/widgets/didvan/switch.dart index ac353ed..6b6841d 100644 --- a/lib/widgets/didvan/switch.dart +++ b/lib/widgets/didvan/switch.dart @@ -1,4 +1,4 @@ -import 'package:didvan/pages/home/settings/widgets/menu_item.dart'; +import 'package:didvan/pages/home/widgets/menu_item.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/didvan/text_field.dart b/lib/widgets/didvan/text_field.dart index 608c40b..e8fbeb4 100644 --- a/lib/widgets/didvan/text_field.dart +++ b/lib/widgets/didvan/text_field.dart @@ -105,7 +105,7 @@ class _DidvanTextFieldState extends State { enabled: widget.enabled, border: InputBorder.none, hintText: widget.hintText, - errorStyle: const TextStyle(height: 0), + errorStyle: const TextStyle(height: 0.01), hintStyle: Theme.of(context) .textTheme .bodyText2! @@ -115,22 +115,23 @@ class _DidvanTextFieldState extends State { ), const SizedBox(height: 8), AnimatedVisibility( - isVisible: _error != null, - duration: DesignConfig.lowAnimationDuration, - child: Row( - children: [ - Icon( - DidvanIcons.lightbulb_exclamation_regular, - color: Theme.of(context).colorScheme.error, - size: 14, - ), - DidvanText( - _error ?? '', - style: Theme.of(context).textTheme.caption, - color: Theme.of(context).colorScheme.error, - ), - ], - )) + isVisible: _error != null, + duration: DesignConfig.lowAnimationDuration, + child: Row( + children: [ + Icon( + DidvanIcons.lightbulb_exclamation_regular, + color: Theme.of(context).colorScheme.error, + size: 14, + ), + DidvanText( + _error ?? '', + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.error, + ), + ], + ), + ) ], ); } diff --git a/lib/widgets/floating_navigation_bar.dart b/lib/widgets/floating_navigation_bar.dart index 9a9bf97..1d61ab9 100644 --- a/lib/widgets/floating_navigation_bar.dart +++ b/lib/widgets/floating_navigation_bar.dart @@ -1,10 +1,9 @@ import 'package:didvan/config/design_config.dart'; 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/category.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; -import 'package:didvan/pages/home/settings/widgets/menu_item.dart'; +import 'package:didvan/pages/home/widgets/menu_item.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/widgets/bookmark_button.dart'; @@ -16,18 +15,27 @@ import 'package:didvan/widgets/item_title.dart'; import 'package:flutter/material.dart'; class FloatingNavigationBar extends StatefulWidget { - final RadarDetailsData? radar; - final NewsDetailsData? news; final ScrollController scrollController; - final VoidCallback onMark; - final VoidCallback onUnmark; + final void Function(int count) onCommentsChanged; + final bool isRadar; + final bool marked; + final int comments; + final int id; + final String title; + final List? categories; + final void Function(bool value) onMarkChanged; + const FloatingNavigationBar({ Key? key, - this.radar, - this.news, required this.scrollController, - required this.onMark, - required this.onUnmark, + required this.onCommentsChanged, + required this.onMarkChanged, + required this.isRadar, + required this.marked, + required this.comments, + required this.id, + required this.title, + this.categories, }) : super(key: key); @override @@ -35,35 +43,13 @@ class FloatingNavigationBar extends StatefulWidget { } class _FloatingNavigationBarState extends State { - bool get _isRadar => widget.radar != null; bool _isScrolled = false; - - get _item => widget.radar ?? widget.news; - - @override - void didUpdateWidget(covariant FloatingNavigationBar oldWidget) { - if (widget.radar != null && oldWidget.radar!.id != widget.radar!.id || - widget.news != null && oldWidget.news!.id != widget.news!.id) { - _isScrolled = false; - } - super.didUpdateWidget(oldWidget); - } + int _comments = 0; @override void initState() { - widget.scrollController.addListener(() { - final position = widget.scrollController.position.pixels; - final offset = MediaQuery.of(context).size.width / 16 * 9 + 40; - if (position > offset && !_isScrolled) { - setState(() { - _isScrolled = true; - }); - } else if (position < offset && _isScrolled) { - setState(() { - _isScrolled = false; - }); - } - }); + _isScrolled = false; + _comments = widget.comments; super.initState(); } @@ -113,11 +99,10 @@ class _FloatingNavigationBarState extends State { ), ), const Spacer(), - if (_isRadar) + if (widget.isRadar) BookmarkButton( - value: _item.marked, - onMark: widget.onMark, - onUnmark: widget.onUnmark, + value: widget.marked, + onMarkChanged: widget.onMarkChanged, bigGestureSize: true, ), SizedBox( @@ -125,9 +110,9 @@ class _FloatingNavigationBarState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (_item.comments != 0) + if (_comments != 0) DidvanText( - _item.comments.toString(), + _comments.toString(), color: foregroundColor, ), DidvanIconButton( @@ -135,9 +120,10 @@ class _FloatingNavigationBarState extends State { onPressed: () => Navigator.of(context).pushNamed( Routes.comments, arguments: { - 'id': _item.id, - 'isRadar': _isRadar, - 'title': _item.title + 'id': widget.id, + 'isRadar': widget.isRadar, + 'title': widget.title, + 'onCommentsChanged': widget.onCommentsChanged, }, ), icon: DidvanIcons.chats_regular, @@ -145,15 +131,14 @@ class _FloatingNavigationBarState extends State { ], ), ), - if (!_isRadar) const SizedBox(width: 12), - if (!_isRadar) + if (!widget.isRadar) const SizedBox(width: 12), + if (!widget.isRadar) BookmarkButton( - value: _item.marked, - onMark: widget.onMark, - onUnmark: widget.onUnmark, + value: widget.marked, + onMarkChanged: widget.onMarkChanged, bigGestureSize: true, ), - if (_isRadar) + if (widget.isRadar) DidvanIconButton( gestureSize: 32, onPressed: _showMoreOptions, @@ -166,6 +151,7 @@ class _FloatingNavigationBarState extends State { } void _showMoreOptions() { + final categories = widget.categories!; ActionSheetUtils.showBottomSheet( data: ActionSheetData( content: Column( @@ -176,21 +162,21 @@ class _FloatingNavigationBarState extends State { icon: DidvanIcons.profile_regular, ), const SizedBox(height: 16), - for (var i = 0; i < _item.categories.length; i++) ...[ + for (var i = 0; i < categories.length; i++) ...[ Padding( padding: const EdgeInsets.only(right: 20), child: MenuItem( - titleWidget: DidvanChip(label: _item.categories[i].label), + titleWidget: DidvanChip(label: categories[i].label), onTap: () { Navigator.of(context).pop(); Navigator.of(context).pushNamed( Routes.direct, - arguments: _item.categories[i].id, + arguments: categories[i].id, ); }, ), ), - if (i != _item.categories.length - 1) + if (i != categories.length - 1) const Padding( padding: EdgeInsets.only(right: 20), child: DidvanDivider(verticalPadding: 8), diff --git a/lib/widgets/image_cropper.dart b/lib/widgets/image_cropper.dart new file mode 100644 index 0000000..fd67e93 --- /dev/null +++ b/lib/widgets/image_cropper.dart @@ -0,0 +1,201 @@ +import 'package:crop/crop.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:flutter/material.dart'; + +class ImageCropper extends StatefulWidget { + final Map data; + + const ImageCropper({Key? key, required this.data}) : super(key: key); + + @override + State createState() => _ImageCropperState(); +} + +class _ImageCropperState extends State { + double _rotation = 0; + BoxShape shape = BoxShape.rectangle; + + final _controller = CropController(); + + get _bytes => widget.data['bytes']; + get _onCropped => widget.data['onCropped']; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: const Text('برش عکس'), + centerTitle: true, + actions: [ + IconButton( + onPressed: _cropImage, + tooltip: 'Crop', + icon: const Icon(Icons.crop), + ) + ], + ), + body: Column( + children: [ + Expanded( + child: Container( + color: Colors.black, + padding: const EdgeInsets.all(8), + child: Crop( + onChanged: (decomposition) { + if (_rotation != decomposition.rotation) { + setState(() { + _rotation = ((decomposition.rotation + 180) % 360) - 180; + }); + } + }, + controller: _controller, + shape: shape, + child: Image.memory( + _bytes, + fit: BoxFit.cover, + ), + helper: shape == BoxShape.rectangle + ? Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + ), + ) + : null, + ), + ), + ), + Row( + children: [ + IconButton( + icon: const Icon(Icons.redo), + tooltip: 'برگرداندن', + onPressed: () { + _controller.rotation = 0; + _controller.scale = 1; + _controller.offset = Offset.zero; + setState(() { + _rotation = 0; + }); + }, + ), + Expanded( + child: SliderTheme( + data: theme.sliderTheme.copyWith( + trackShape: const RectangularSliderTrackShape(), + ), + child: Slider( + thumbColor: Theme.of(context).colorScheme.title, + divisions: 360, + value: _rotation, + min: -180, + max: 180, + label: '$_rotation°', + onChanged: (n) { + setState(() { + _rotation = n.roundToDouble(); + _controller.rotation = _rotation; + }); + }, + ), + ), + ), + // PopupMenuButton( + // icon: const Icon(Icons.crop_free), + // itemBuilder: (context) => [ + // const PopupMenuItem( + // child: Text("Box"), + // value: BoxShape.rectangle, + // ), + // const PopupMenuItem( + // child: Text("Oval"), + // value: BoxShape.circle, + // ), + // ], + // tooltip: 'Crop Shape', + // onSelected: (x) { + // setState(() { + // shape = x; + // }); + // }, + // ), + // PopupMenuButton( + // icon: const Icon(Icons.aspect_ratio), + // itemBuilder: (context) => [ + // const PopupMenuItem( + // child: Text("Original"), + // value: 1000 / 667.0, + // ), + // const PopupMenuDivider(), + // const PopupMenuItem( + // child: Text("16:9"), + // value: 16.0 / 9.0, + // ), + // const PopupMenuItem( + // child: Text("4:3"), + // value: 4.0 / 3.0, + // ), + // const PopupMenuItem( + // child: Text("1:1"), + // value: 1, + // ), + // const PopupMenuItem( + // child: Text("3:4"), + // value: 3.0 / 4.0, + // ), + // const PopupMenuItem( + // child: Text("9:16"), + // value: 9.0 / 16.0, + // ), + // ], + // tooltip: 'Aspect Ratio', + // onSelected: (x) { + // _controller.aspectRatio = x; + // setState(() {}); + // }, + // ), + ], + ), + ], + ), + ); + } + + void _cropImage() async { + final pixelRatio = MediaQuery.of(context).devicePixelRatio; + final cropped = await _controller.crop(pixelRatio: pixelRatio); + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: AppBar( + title: const Text('تایید برش'), + centerTitle: true, + actions: [ + Builder( + builder: (context) => IconButton( + icon: const Icon( + DidvanIcons.check_circle_solid, + size: 32, + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + _onCropped(); + }, + ), + ), + ], + ), + body: Center( + child: RawImage( + image: cropped, + ), + ), + ), + fullscreenDialog: true, + ), + ); + } +} diff --git a/lib/widgets/multitype_item.dart b/lib/widgets/multitype_item.dart new file mode 100644 index 0000000..4bdab92 --- /dev/null +++ b/lib/widgets/multitype_item.dart @@ -0,0 +1,75 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/item_overview.dart'; +import 'package:didvan/widgets/didvan/card.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:didvan/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class MultitypeItem extends StatelessWidget { + final ItemOverview item; + const MultitypeItem({Key? key, required this.item}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + SkeletonImage(imageUrl: item.image, height: 80, width: 80), + Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(10), + ), + ), + child: Icon( + item.type == 'radar' + ? DidvanIcons.radar_light + : DidvanIcons.news_light, + color: Theme.of(context).colorScheme.white, + size: 18, + ), + ), + ], + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + item.title, + style: Theme.of(context).textTheme.bodyText1, + ), + Row( + children: [ + const Icon( + DidvanIcons.calendar_day_light, + size: 18, + ), + const SizedBox(width: 4), + DidvanText( + DateTime.parse(item.createdAt).toPersianDateStr(), + style: Theme.of(context).textTheme.overline, + ), + // DidvanText('text'), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/skeleton_image.dart b/lib/widgets/skeleton_image.dart index c65477b..aecc541 100644 --- a/lib/widgets/skeleton_image.dart +++ b/lib/widgets/skeleton_image.dart @@ -1,7 +1,5 @@ -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'; @@ -31,8 +29,8 @@ class SkeletonImage extends StatefulWidget { } class _SkeletonImageState extends State { - Uint8List? _bytes; - + late Uint8List _bytes; + bool _isLoading = true; @override void initState() { if (kIsWeb) _getImage(); @@ -41,57 +39,76 @@ class _SkeletonImageState extends State { 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(); - } + + // 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(() {}); + setState(() { + _isLoading = false; + }); } } - 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)}); - } + // 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, - ), - ); - }, + if (_isLoading) { + return ShimmerPlaceholder( + borderRadius: widget.borderRadius, + width: widget.aspectRatio == null ? widget.width : null, + height: widget.aspectRatio == null ? widget.height : null, + ); + } + return ClipRRect( + borderRadius: widget.borderRadius, + child: Image.memory( + _bytes, + fit: BoxFit.cover, + width: widget.width, + height: widget.height, + ), ); + // 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}'}, @@ -108,7 +125,9 @@ class _SkeletonImageState extends State { ), ), progressIndicatorBuilder: (context, url, progress) => - const ShimmerPlaceholder(), + ShimmerPlaceholder( + borderRadius: widget.borderRadius, + ), ); }), ); diff --git a/lib/widgets/state_handlers/empty_list.dart b/lib/widgets/state_handlers/empty_list.dart new file mode 100644 index 0000000..da00ac7 --- /dev/null +++ b/lib/widgets/state_handlers/empty_list.dart @@ -0,0 +1,15 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/widgets/state_handlers/empty_state.dart'; +import 'package:flutter/material.dart'; + +class EmptyList extends StatelessWidget { + const EmptyList({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return EmptyState( + asset: Assets.emptyBookmark, + title: 'لیست خالی است', + ); + } +} diff --git a/lib/widgets/state_handlers/sliver_state_handler.dart b/lib/widgets/state_handlers/sliver_state_handler.dart index dde4ee4..e160f70 100644 --- a/lib/widgets/state_handlers/sliver_state_handler.dart +++ b/lib/widgets/state_handlers/sliver_state_handler.dart @@ -12,6 +12,7 @@ class SliverStateHandler extends SliverList { final Widget? emptyState; final Widget? placeholder; final EdgeInsets? itemPadding; + final bool centerEmptyState; SliverStateHandler({ Key? key, required this.state, @@ -22,21 +23,26 @@ class SliverStateHandler extends SliverList { this.placeholder, this.emptyState, this.enableEmptyState = false, + this.centerEmptyState = true, }) : super( key: key, delegate: SliverChildBuilderDelegate( (context, index) { if (state.appState == AppState.failed) { - return SizedBox( - height: MediaQuery.of(context).size.height - 240, - child: EmptyConnection( - onRetry: onRetry, + return Padding( + padding: EdgeInsets.only( + top: centerEmptyState ? 120 : 20, + bottom: 20, ), + child: EmptyConnection(onRetry: onRetry), ); } if (enableEmptyState && state.appState == AppState.idle) { - return SizedBox( - height: MediaQuery.of(context).size.height - 240, + return Padding( + padding: EdgeInsets.only( + top: centerEmptyState ? 120 : 20, + bottom: 20, + ), child: emptyState, ); }