Browse Source

finish app

fix_bug
mohsen zamani 2 years ago
parent
commit
c38393a9cf
  1. 2
      android/app/build.gradle
  2. BIN
      assets/images/png/ic_about.png
  3. BIN
      assets/images/png/image_mock.png
  4. 3
      assets/images/svg/ic_arrow_down.svg
  5. 3
      assets/images/svg/ic_bookmark.svg
  6. 7
      assets/images/svg/ic_clear.svg
  7. 3
      assets/images/svg/ic_comment.svg
  8. 3
      assets/images/svg/ic_like.svg
  9. 3
      assets/images/svg/ic_send.svg
  10. 9
      assets/images/svg/ic_share.svg
  11. 6
      assets/images/svg/ic_view.svg
  12. 5
      assets/languages/en.json
  13. 7
      assets/languages/fa.json
  14. 1061
      assets/meta/data.json
  15. 14
      lib/core/html/html_viewer.dart
  16. 6
      lib/core/utils/app_utils.dart
  17. 35
      lib/core/widgets/loading_list_widget.dart
  18. 213
      lib/core/widgets/shimmer.dart
  19. 260
      lib/features/main/main_screen.dart
  20. 108
      lib/features/posts/cubit/posts_cubit.dart
  21. 293
      lib/features/posts/screen/posts_screen.dart
  22. 33
      lib/features/posts/widgets/post_item_widget.dart
  23. 106
      lib/features/posts/widgets/search_widget.dart
  24. 34
      lib/features/single_post/cubit/single_post_cubit.dart
  25. 301
      lib/features/single_post/screen/single_post_screen.dart
  26. 8
      lib/features/single_post/view_models/comment.dart
  27. 145
      lib/features/single_post/view_models/post.dart
  28. 31
      lib/features/single_post/view_models/thumbnail.dart
  29. 71
      lib/features/single_post/widget/add_comment_widget.dart
  30. 59
      lib/features/single_post/widget/post_comment_widget.dart
  31. 44
      pubspec.lock
  32. 2
      pubspec.yaml

2
android/app/build.gradle

@ -45,7 +45,7 @@ android {
defaultConfig {
applicationId "com.example.sonnat"
minSdkVersion 16
minSdkVersion 19
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

BIN
assets/images/png/ic_about.png

After

Width: 512  |  Height: 512  |  Size: 20 KiB

BIN
assets/images/png/image_mock.png

After

Width: 1204  |  Height: 696  |  Size: 1.6 MiB

3
assets/images/svg/ic_arrow_down.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="7.858" height="4.458" viewBox="0 0 7.858 4.458">
<path id="Icon_awesome-caret-down" data-name="Icon awesome-caret-down" d="M1.323,13.5h6.8a.528.528,0,0,1,.373.9L5.1,17.8a.53.53,0,0,1-.748,0L.95,14.4A.528.528,0,0,1,1.323,13.5Z" transform="translate(-0.794 -13.5)" fill="#404966"/>
</svg>

3
assets/images/svg/ic_bookmark.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.003" height="17.718" viewBox="0 0 14.003 17.718">
<path id="Icon_feather-bookmark" data-name="Icon feather-bookmark" d="M20.5,21.218,14,16.574,7.5,21.218V6.358A1.858,1.858,0,0,1,9.358,4.5h9.288A1.858,1.858,0,0,1,20.5,6.358Z" transform="translate(-7 -4)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</svg>

7
assets/images/svg/ic_clear.svg

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 34 34">
<g id="Group_1609" data-name="Group 1609" transform="translate(-87 -30)">
<circle id="Ellipse_370" data-name="Ellipse 370" cx="17" cy="17" r="17" transform="translate(87 30)" fill="#e7e7f5"/>
<line id="Line_242" data-name="Line 242" y2="15" transform="translate(98.697 41.697) rotate(-45)" fill="none" stroke="#8d95ab" stroke-linecap="round" stroke-width="2"/>
<line id="Line_243" data-name="Line 243" y2="15" transform="translate(109.303 41.697) rotate(45)" fill="none" stroke="#8d95ab" stroke-linecap="round" stroke-width="2"/>
</g>
</svg>

3
assets/images/svg/ic_comment.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19.668" height="19.679" viewBox="0 0 19.668 19.679">
<path id="Icon_awesome-comment-alt" data-name="Icon awesome-comment-alt" d="M16.334,0h-14A2.336,2.336,0,0,0,0,2.333v10.5a2.336,2.336,0,0,0,2.333,2.333h3.5V18.23a.438.438,0,0,0,.7.354l4.554-3.416h5.25a2.336,2.336,0,0,0,2.333-2.333V2.333A2.336,2.336,0,0,0,16.334,0Z" transform="translate(0.5 0.5)" fill="none" stroke="#fff" stroke-width="1"/>
</svg>

3
assets/images/svg/ic_like.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20.671" height="18.087" viewBox="0 0 20.671 18.087">
<path id="Icon_awesome-heart" data-name="Icon awesome-heart" d="M18.664,3.484a5.521,5.521,0,0,0-7.534.549l-.8.82-.8-.82a5.521,5.521,0,0,0-7.534-.549,5.8,5.8,0,0,0-.4,8.393l7.812,8.066a1.266,1.266,0,0,0,1.829,0l7.812-8.066a5.794,5.794,0,0,0-.4-8.393Z" transform="translate(0.001 -2.248)" fill="#ef4c4c"/>
</svg>

3
assets/images/svg/ic_send.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24.915" height="21.355" viewBox="0 0 24.915 21.355">
<path id="Icon_material-send" data-name="Icon material-send" d="M.012,21.355l24.9-10.678L.012,0,0,8.3l17.8,2.373L0,13.05Z" transform="translate(24.915 21.355) rotate(180)" fill="#636e88"/>
</svg>

9
assets/images/svg/ic_share.svg

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16.046" height="17.718" viewBox="0 0 16.046 17.718">
<g id="Icon_feather-share-2" data-name="Icon feather-share-2" transform="translate(0.5 0.5)">
<path id="Path_2718" data-name="Path 2718" d="M27.515,5.508A2.508,2.508,0,1,1,25.008,3,2.508,2.508,0,0,1,27.515,5.508Z" transform="translate(-12.469 -3)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Path_2719" data-name="Path 2719" d="M9.515,16.008A2.508,2.508,0,1,1,7.008,13.5,2.508,2.508,0,0,1,9.515,16.008Z" transform="translate(-4.5 -7.649)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Path_2720" data-name="Path 2720" d="M27.515,26.508A2.508,2.508,0,1,1,25.008,24,2.508,2.508,0,0,1,27.515,26.508Z" transform="translate(-12.469 -12.298)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Path_2721" data-name="Path 2721" d="M12.885,20.265l5.709,3.327" transform="translate(-8.212 -10.644)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Path_2722" data-name="Path 2722" d="M18.586,9.765l-5.7,3.327" transform="translate(-8.212 -5.995)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

6
assets/images/svg/ic_view.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24.021" height="17.743" viewBox="0 0 24.021 17.743">
<g id="Icon_feather-eye" data-name="Icon feather-eye" transform="translate(0.5 0.5)">
<path id="Path_2723" data-name="Path 2723" d="M1.5,14.371S5.686,6,13.01,6s11.51,8.371,11.51,8.371-4.186,8.371-11.51,8.371S1.5,14.371,1.5,14.371Z" transform="translate(-1.5 -6)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
<path id="Path_2724" data-name="Path 2724" d="M19.778,16.639A3.139,3.139,0,1,1,16.639,13.5,3.139,3.139,0,0,1,19.778,16.639Z" transform="translate(-5.129 -8.268)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
</g>
</svg>

5
assets/languages/en.json

@ -13,5 +13,8 @@
"contact_to_us": "Contact us",
"send_message_to_us": "Send us a message",
"send_message": "Write your message",
"send": "Send"
"write_comment": "Write your comment",
"send": "Send",
"show_all_comments": "View all comments",
"search": "Search"
}

7
assets/languages/fa.json

@ -5,7 +5,7 @@
"video": "ویدئو",
"news": "اخبار",
"specials": "ویژه\u200Cها",
"search_term": "...جستجوی عبارت",
"search_term": "جستجوی عبارت...",
"main_header_text": "پرتال جامع سنت",
"second_header_text": " شرحی بر تاریـــخ تحلیل نشــده اســــلام",
"more_about_us": "با ما بیشتر آشنا شوید",
@ -13,5 +13,8 @@
"contact_to_us": "ارتباط با ما",
"send_message_to_us": "ارسال پیام به ما",
"send_message": "پیام خود را بنویسید",
"send": "ارسال"
"send": "ارسال",
"write_comment": "دیدگاه خود را وارد کنید",
"show_all_comments": "مشاهده همه دیدگاه ها",
"search": "جستجو"
}

1061
assets/meta/data.json
File diff suppressed because it is too large
View File

14
lib/core/html/html_viewer.dart

@ -159,45 +159,54 @@ class HTMLViewer extends StatelessWidget {
'h1': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 2.3, Unit.rem),
),
'h2': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 2.1, Unit.rem),
),
'h3': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.9, Unit.rem),
),
'h4': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.7, Unit.rem),
),
'h5': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.6, Unit.rem),
),
'h6': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.4, Unit.rem),
),
'li': Style(
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
),
'a': Style(
color: textColor,
fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
),
'ol': Style(
fontWeight: FontWeight.normal,
fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
textAlign: TextAlign.justify,
),
'html': Style(
fontSize: FontSize(baseFontSize * fontSizeFactor),
@ -214,10 +223,7 @@ class HTMLViewer extends StatelessWidget {
},
);
return Padding(
padding: Utils.instance.singleMargin(left: 15, right: 15, bottom: 60.h),
child: html,
);
return html;
}
void _openImage({required String imageUrl, required BuildContext context}) {

6
lib/core/utils/app_utils.dart

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:shamsi_date/shamsi_date.dart';
import 'package:sonnat/core/extensions/number_extension.dart';
import 'package:sonnat/core/language/language_cubit.dart';
@ -105,4 +106,9 @@ class Utils {
vertical: (vertical ?? 0).w,
);
}
String dateToString(int dateInMilliseconds) {
Jalali jalali = Jalali.fromDateTime(DateTime.fromMillisecondsSinceEpoch(dateInMilliseconds));
return '${jalali.year}/${jalali.month}/${jalali.day}';
}
}

35
lib/core/widgets/loading_list_widget.dart

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/widgets/shimmer.dart';
class LoadingListWidget extends StatelessWidget {
const LoadingListWidget({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.black12,
highlightColor: Colors.white,
child: ListView.builder(
padding: EdgeInsets.zero,
itemBuilder: (_, __) => Container(
height: 200,
decoration: BoxDecoration(
color: const Color(0xffffffff),
borderRadius: BorderRadius.circular(16),
),
margin: EdgeInsets.only(
left: context.width * 26 / AppConstants.instance.appWidth,
right: context.width * 26 / AppConstants.instance.appWidth,
bottom: context.height * 8 / AppConstants.instance.appHeight,
top: context.height * 8 / AppConstants.instance.appHeight,
),
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: 10,
),
);
}
}

213
lib/core/widgets/shimmer.dart

@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
enum ShimmerDirection { ltr, rtl, ttb, btt }
@immutable
class Shimmer extends StatefulWidget {
final Widget child;
final Duration period;
final ShimmerDirection direction;
final Gradient gradient;
final int loop;
final bool enabled;
const Shimmer({
super.key,
required this.child,
required this.gradient,
this.direction = ShimmerDirection.rtl,
this.period = const Duration(milliseconds: 1500),
this.loop = 0,
this.enabled = true,
});
Shimmer.fromColors({
super.key,
required this.child,
required Color baseColor,
required Color highlightColor,
this.period = const Duration(milliseconds: 1500),
this.direction = ShimmerDirection.rtl,
this.loop = 0,
this.enabled = true,
}) : gradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.centerRight,
colors: <Color>[baseColor, baseColor, highlightColor, baseColor, baseColor],
stops: const <double>[0.0, 0.35, 0.5, 0.65, 1.0],
);
@override
State<Shimmer> createState() => _ShimmerState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null));
properties.add(EnumProperty<ShimmerDirection>('direction', direction));
properties.add(DiagnosticsProperty<Duration>('period', period, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
properties.add(DiagnosticsProperty<int>('loop', loop, defaultValue: 0));
}
}
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
late AnimationController _controller;
int _count = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.period)
..addStatusListener((status) {
if (status != AnimationStatus.completed) {
return;
}
_count++;
if (widget.loop <= 0) {
_controller.repeat();
} else if (_count < widget.loop) {
_controller.forward(from: 0.0);
}
});
if (widget.enabled) {
_controller.forward();
}
}
@override
void didUpdateWidget(Shimmer oldWidget) {
if (widget.enabled) {
_controller.forward();
} else {
_controller.stop();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: widget.child,
builder: (context, child) => _Shimmer(
direction: widget.direction,
gradient: widget.gradient,
percent: _controller.value,
child: child,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
@immutable
class _Shimmer extends SingleChildRenderObjectWidget {
final double percent;
final ShimmerDirection direction;
final Gradient gradient;
const _Shimmer({
super.child,
required this.percent,
required this.direction,
required this.gradient,
});
@override
_ShimmerFilter createRenderObject(BuildContext context) {
return _ShimmerFilter(percent, direction, gradient);
}
@override
void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) {
shimmer.percent = percent;
shimmer.gradient = gradient;
shimmer.direction = direction;
}
}
class _ShimmerFilter extends RenderProxyBox {
ShimmerDirection _direction;
Gradient _gradient;
double _percent;
_ShimmerFilter(this._percent, this._direction, this._gradient);
@override
ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;
@override
bool get alwaysNeedsCompositing => child != null;
set percent(double newValue) {
if (newValue == _percent) {
return;
}
_percent = newValue;
markNeedsPaint();
}
set gradient(Gradient newValue) {
if (newValue == _gradient) {
return;
}
_gradient = newValue;
markNeedsPaint();
}
set direction(ShimmerDirection newDirection) {
if (newDirection == _direction) {
return;
}
_direction = newDirection;
markNeedsLayout();
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
final double width = child!.size.width;
final double height = child!.size.height;
Rect rect;
double dx, dy;
if (_direction == ShimmerDirection.rtl) {
dx = _offset(width, -width, _percent);
dy = 0.0;
rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
} else if (_direction == ShimmerDirection.ttb) {
dx = 0.0;
dy = _offset(-height, height, _percent);
rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
} else if (_direction == ShimmerDirection.btt) {
dx = 0.0;
dy = _offset(height, -height, _percent);
rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
} else {
dx = _offset(-width, width, _percent);
dy = 0.0;
rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
}
layer ??= ShaderMaskLayer();
layer!
..shader = _gradient.createShader(rect)
..maskRect = offset & size
..blendMode = BlendMode.srcIn;
context.pushLayer(layer!, super.paint, offset);
} else {
layer = null;
}
}
double _offset(double start, double end, double percent) {
return start + (end - start) * percent;
}
}

260
lib/features/main/main_screen.dart

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/features/aabout_us/about_us_screen.dart';
import 'package:sonnat/features/main/widget/main_item_widget.dart';
import 'package:sonnat/features/posts/cubit/posts_cubit.dart';
import 'package:sonnat/features/posts/screen/posts_screen.dart';
class MainScreen extends StatefulWidget {
@ -37,159 +38,136 @@ class _MainScreenState extends State<MainScreen> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xff26237A),
body: Column(
children: [
Container(
margin: EdgeInsets.only(
top: context.height * 20 / AppConstants.instance.appHeight,
left: context.width * 86 / AppConstants.instance.appWidth,
right: context.width * 86 / AppConstants.instance.appWidth,
body: SafeArea(
child: Column(
children: [
Container(
margin: EdgeInsets.only(
left: context.width * 86 / AppConstants.instance.appWidth,
right: context.width * 86 / AppConstants.instance.appWidth,
),
child: Image.asset(
'ic_main_header'.pngPath,
width: context.width * 200 / AppConstants.instance.appWidth,
height: context.height * 200 / AppConstants.instance.appHeight,
),
),
child: Image.asset(
'ic_main_header'.pngPath,
width: context.width * 200 / AppConstants.instance.appWidth,
height: context.height * 200 / AppConstants.instance.appHeight,
Text(
Translator.translate('main_header_text'),
style: const TextStyle(
color: Color(0xffFFD800),
fontSize: 28,
),
),
),
Text(
Translator.translate('main_header_text'),
style: const TextStyle(
color: Color(0xffFFD800),
fontSize: 28,
Text(
Translator.translate('second_header_text'),
style: const TextStyle(
color: Color(0xffDEDEDE),
fontSize: 16,
),
),
),
Text(
Translator.translate('second_header_text'),
style: const TextStyle(
color: Color(0xffDEDEDE),
fontSize: 16,
),
),
Container(
decoration: BoxDecoration(
color: const Color(0xffF4F4F8),
borderRadius: BorderRadius.circular(22),
),
margin: EdgeInsets.only(
left: context.width * 35 / AppConstants.instance.appWidth,
right: context.width * 35 / AppConstants.instance.appWidth,
top: context.height * 40 / AppConstants.instance.appHeight,
bottom: context.height * 60 / AppConstants.instance.appHeight,
),
padding: EdgeInsets.symmetric(
vertical: context.height * 13 / AppConstants.instance.appHeight,
horizontal: context.width * 8 / AppConstants.instance.appWidth,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
Translator.translate('search_term'),
style: const TextStyle(
color: Color(0xffBCC1CD),
fontSize: 16,
),
GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => PostsCubit(),
child: const PostsScreen(title: '', searchMode: true),
);
},
));
},
child: Container(
decoration: BoxDecoration(
color: const Color(0xffF4F4F8),
borderRadius: BorderRadius.circular(22),
),
SizedBox(width: context.width * 14 / AppConstants.instance.appWidth),
SvgPicture.asset('ic_search'.svgPath),
],
),
),
SvgPicture.asset('ic_line'.svgPath),
SizedBox(height: context.height * 30 / AppConstants.instance.appHeight),
Padding(
padding: EdgeInsets.symmetric(horizontal: context.width * 35 / AppConstants.instance.appWidth),
child: Row(
children: [
GestureDetector(
child: MainItemWidget(icon: _icons[2], name: _names[2]),
onTap: () => _openItem(index: 2),
margin: EdgeInsets.only(
left: context.width * 35 / AppConstants.instance.appWidth,
right: context.width * 35 / AppConstants.instance.appWidth,
top: context.height * 40 / AppConstants.instance.appHeight,
bottom: context.height * 60 / AppConstants.instance.appHeight,
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[1], name: _names[1]),
onTap: () => _openItem(index: 1),
padding: EdgeInsets.symmetric(
vertical: context.height * 13 / AppConstants.instance.appHeight,
horizontal: context.width * 8 / AppConstants.instance.appWidth,
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[0], name: _names[0]),
onTap: () => _openItem(index: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgPicture.asset('ic_search'.svgPath),
SizedBox(width: context.width * 14 / AppConstants.instance.appWidth),
Text(
Translator.translate('search_term'),
style: const TextStyle(
color: Color(0xffBCC1CD),
fontSize: 16,
),
),
],
),
],
),
),
),
SizedBox(height: context.height * 10 / AppConstants.instance.appHeight),
Padding(
padding: EdgeInsets.symmetric(horizontal: context.width * 35 / AppConstants.instance.appWidth),
child: Row(
children: [
GestureDetector(
child: MainItemWidget(icon: _icons[5], name: _names[5]),
onTap: () => _openItem(index: 5),
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[4], name: _names[4]),
onTap: () => _openItem(index: 4),
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[3], name: _names[3]),
onTap: () => _openItem(index: 3),
),
],
SvgPicture.asset('ic_line'.svgPath),
SizedBox(height: context.height * 30 / AppConstants.instance.appHeight),
Padding(
padding: EdgeInsets.symmetric(horizontal: context.width * 35 / AppConstants.instance.appWidth),
child: Row(
children: [
GestureDetector(
child: MainItemWidget(icon: _icons[2], name: _names[2]),
onTap: () => _openItem(index: 2),
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[1], name: _names[1]),
onTap: () => _openItem(index: 1),
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[0], name: _names[0]),
onTap: () => _openItem(index: 0),
),
],
),
),
SizedBox(height: context.height * 10 / AppConstants.instance.appHeight),
Padding(
padding: EdgeInsets.symmetric(horizontal: context.width * 35 / AppConstants.instance.appWidth),
child: Row(
children: [
GestureDetector(
child: MainItemWidget(icon: _icons[5], name: _names[5]),
onTap: () => _openItem(index: 5),
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[4], name: _names[4]),
onTap: () => _openItem(index: 4),
),
SizedBox(width: context.width * 13 / AppConstants.instance.appWidth),
GestureDetector(
child: MainItemWidget(icon: _icons[3], name: _names[3]),
onTap: () => _openItem(index: 3),
),
],
),
),
),
SizedBox(height: context.height * 20 / AppConstants.instance.appHeight),
],
SizedBox(height: context.height * 20 / AppConstants.instance.appHeight),
],
),
),
);
}
void _openItem({required int index}) {
switch (index) {
case 0:
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return PostsScreen(title: _names[index]);
},
));
break;
case 1:
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return PostsScreen(title: _names[index]);
},
));
break;
case 2:
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return PostsScreen(title: _names[index]);
},
));
break;
case 3:
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return PostsScreen(title: _names[index]);
},
));
break;
case 4:
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return PostsScreen(title: _names[index]);
},
));
break;
case 5:
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return const AboutUsScreen();
},
));
break;
}
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => PostsCubit(),
child: PostsScreen(title: _names[index]),
);
},
));
}
}

108
lib/features/posts/cubit/posts_cubit.dart

@ -0,0 +1,108 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
class PostsCubit extends Cubit<BaseCubitType<PostsState>> {
List<Post> allData = [];
List<Post> postList = [];
List<Post> searchedList = [];
bool hasReachedMax = false;
String _query = '';
PostsCubit() : super(BaseCubitType(eventName: PostsState.empty));
void empty() => emit(BaseCubitType(eventName: PostsState.empty));
Future<void> getData() async {
if (hasReachedMax) {
return;
}
if (postList.isNotEmpty) {
emit(BaseCubitType(eventName: PostsState.loadingPagination));
} else {
emit(BaseCubitType(eventName: PostsState.loading));
}
String data = await rootBundle.loadString('assets/meta/data.json');
allData = List<Post>.from(
jsonDecode(data)['posts'].map(
(post) => Post.fromJson(post).copyWith(DateTime.now().millisecondsSinceEpoch),
),
);
if (allData.isNotEmpty) {
List<Post> posts = allData.getRange(postList.length, min(postList.length + 20, allData.length)).toList();
hasReachedMax = posts.length < 20;
postList += posts;
emit(BaseCubitType(eventName: PostsState.data));
} else {
emit(BaseCubitType(eventName: PostsState.data));
}
}
Future<void> search({required String query}) async {
if (query.isEmpty) {
unawaited(getData());
_query = '';
searchedList.clear();
emit(BaseCubitType(eventName: PostsState.data));
return;
}
if (_query != query) {
hasReachedMax = false;
searchedList.clear();
_query = query;
}
if (hasReachedMax) {
return;
}
if (searchedList.isNotEmpty) {
emit(BaseCubitType(eventName: PostsState.loadingPagination));
} else {
emit(BaseCubitType(eventName: PostsState.loading));
}
await Future.delayed(Duration(milliseconds: Random().nextInt(1000)));
String data = await rootBundle.loadString('assets/meta/data.json');
allData = List<Post>.from(
jsonDecode(data)['posts'].map(
(post) => Post.fromJson(post).copyWith(
DateTime.now().millisecondsSinceEpoch,
),
),
);
if (allData.isNotEmpty) {
List<Post> posts = allData.where((element) => element.name.contains(query.trim())).toList();
hasReachedMax = posts.length < 20;
searchedList += posts;
emit(BaseCubitType(eventName: PostsState.data));
} else {
emit(BaseCubitType(eventName: PostsState.data));
}
}
String get query => _query;
void clearSearch() {
unawaited(getData());
_query = '';
searchedList.clear();
emit(BaseCubitType(eventName: PostsState.data));
}
Future<void> changeFilter(int index) async {
emit(BaseCubitType(eventName: PostsState.changeFilterIndex, data: index));
await Future.delayed(Duration(milliseconds: Random().nextInt(1000)));
await getData();
}
}
enum PostsState {
empty,
data,
loading,
loadingPagination,
changeFilterIndex,
}

293
lib/features/posts/screen/posts_screen.dart

@ -1,21 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/core/widgets/loading_list_widget.dart';
import 'package:sonnat/features/posts/cubit/posts_cubit.dart';
import 'package:sonnat/features/posts/widgets/filter_item_widget.dart';
import 'package:sonnat/features/posts/widgets/post_item_widget.dart';
import 'package:sonnat/features/posts/widgets/search_widget.dart';
import 'package:sonnat/features/single_post/cubit/single_post_cubit.dart';
import 'package:sonnat/features/single_post/screen/single_post_screen.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
class PostsScreen extends StatefulWidget {
final String title;
final bool searchMode;
const PostsScreen({super.key, required this.title});
const PostsScreen({super.key, required this.title, this.searchMode = false});
@override
State<PostsScreen> createState() => _PostsScreenState();
}
class _PostsScreenState extends State<PostsScreen> {
late final PostsCubit _cubit;
bool _loading = true;
bool _getListFlag = false;
late bool _searchMode;
final ScrollController _controller = ScrollController();
int _selectedFilterIndex = 1;
final List<FilterItem> _filterList = [
const FilterItem(selected: false, title: 'پر تکرارترین‌ها'),
const FilterItem(selected: true, title: 'مشابه‌ها'),
@ -29,82 +46,232 @@ class _PostsScreenState extends State<PostsScreen> {
const FilterItem(selected: false, title: 'محبوبترین‌ها'),
];
@override
void initState() {
_searchMode = widget.searchMode;
_cubit = BlocProvider.of<PostsCubit>(context);
_cubit.getData();
_controller.addListener(_onScroll);
super.initState();
}
void _onScroll() {
final double maxScroll = _controller.position.maxScrollExtent;
final double currentScroll = _controller.position.pixels;
if (maxScroll - currentScroll <= 400) {
if (_getListFlag) {
_getListFlag = false;
_cubit.getData();
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
SizedBox(height: context.height * 26 / AppConstants.instance.appHeight),
Padding(
padding: EdgeInsets.symmetric(horizontal: context.width * 26 / AppConstants.instance.appWidth),
child: Row(
children: [
GestureDetector(
onTap: () {},
child: SvgPicture.asset(
'ic_more'.svgPath,
),
),
SizedBox(width: context.width * 8 / AppConstants.instance.appWidth),
GestureDetector(
onTap: () {},
child: SvgPicture.asset(
'ic_rounded_search'.svgPath,
child: BlocBuilder<PostsCubit, BaseCubitType<PostsState>>(
builder: (context, state) {
switch (state.eventName!) {
case PostsState.empty:
break;
case PostsState.data:
_loading = false;
_getListFlag = true;
break;
case PostsState.loading:
_loading = true;
break;
case PostsState.loadingPagination:
break;
case PostsState.changeFilterIndex:
_selectedFilterIndex = state.data;
_loading = true;
break;
}
return Column(
children: [
SizedBox(height: context.height * 26 / AppConstants.instance.appHeight),
Padding(
padding: EdgeInsets.symmetric(horizontal: context.width * 26 / AppConstants.instance.appWidth),
child: _searchMode
? Row(
children: [
Expanded(
child: SearchWidget(
search: (query) {
_cubit.search(query: query);
},
),
),
SizedBox(width: context.width * 12 / AppConstants.instance.appWidth),
GestureDetector(
onTap: () {
_cubit.clearSearch();
setState(() {
_searchMode = !_searchMode;
});
},
child: SvgPicture.asset(
'ic_back'.svgPath,
),
),
],
)
: Row(
children: [
GestureDetector(
onTap: () {},
child: SvgPicture.asset(
'ic_more'.svgPath,
),
),
SizedBox(width: context.width * 8 / AppConstants.instance.appWidth),
GestureDetector(
onTap: () {
setState(() {
_searchMode = !_searchMode;
});
},
child: SvgPicture.asset(
'ic_rounded_search'.svgPath,
),
),
const Spacer(),
Text(
widget.title,
style: const TextStyle(
color: Color(0xff404966),
fontSize: 22,
),
),
const Spacer(),
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: SvgPicture.asset(
'ic_back'.svgPath,
),
),
],
),
),
if (_searchMode)
Padding(
padding: EdgeInsets.only(
top: context.height * 17 / AppConstants.instance.appHeight,
left: context.width * 26 / AppConstants.instance.appWidth,
right: context.width * 26 / AppConstants.instance.appWidth,
),
),
const Spacer(),
Text(
widget.title,
style: const TextStyle(
color: Color(0xff404966),
fontSize: 22,
child: Row(
children: [
SvgPicture.asset(
'ic_search'.svgPath,
colorFilter: const ColorFilter.mode(
Color(0xff26237A),
BlendMode.srcIn,
),
),
const SizedBox(width: 8),
Text(
'${Translator.translate('search')}:',
style: const TextStyle(
color: Color(0xff26237A),
fontSize: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_cubit.query,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xff26237A),
fontSize: 16,
),
),
),
const Spacer(),
Text(
_loading
? 'در حال جستجو'
: _cubit.query.isEmpty
? ''
: _cubit.searchedList.isEmpty
? "موردی یافت نشد"
: "${_cubit.searchedList.length} مورد یافت شد",
style: const TextStyle(
color: Color(0xff636E88),
fontSize: 12,
),
)
],
),
),
const Spacer(),
GestureDetector(
onTap: () {
Navigator.pop(context);
SizedBox(height: context.height * 35 / AppConstants.instance.appHeight),
SizedBox(
height: context.height * 31 / AppConstants.instance.appHeight,
child: ListView.builder(
padding: EdgeInsetsDirectional.only(start: context.width * 26 / AppConstants.instance.appWidth),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_cubit.changeFilter(index);
},
child: FilterItemWidget(
title: _filterList[index].title,
selected: index == _selectedFilterIndex,
),
);
},
child: SvgPicture.asset(
'ic_back'.svgPath,
),
itemCount: _filterList.length,
scrollDirection: Axis.horizontal,
),
],
),
),
SizedBox(height: context.height * 35 / AppConstants.instance.appHeight),
SizedBox(
height: context.height * 31 / AppConstants.instance.appHeight,
child: ListView.builder(
padding: EdgeInsetsDirectional.only(start: context.width * 26 / AppConstants.instance.appWidth),
itemBuilder: (context, index) {
return FilterItemWidget(title: _filterList[index].title, selected: _filterList[index].selected);
},
itemCount: _filterList.length,
scrollDirection: Axis.horizontal,
),
),
SizedBox(height: context.height * 26 / AppConstants.instance.appHeight),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
onTap: _clickOnPost,
child: const PostItemWidget(),
);
},
itemCount: 10,
),
)
],
),
SizedBox(height: context.height * 26 / AppConstants.instance.appHeight),
if (_loading)
const Expanded(child: LoadingListWidget())
else
Expanded(
child: ListView.builder(
controller: _controller,
itemBuilder: (context, index) {
if (_searchMode) {
return GestureDetector(
onTap: () => _clickOnPost(_cubit.searchedList[index]),
child: PostItemWidget(post: _cubit.searchedList[index]),
);
}
return GestureDetector(
onTap: () => _clickOnPost(_cubit.postList[index]),
child: PostItemWidget(post: _cubit.postList[index]),
);
},
itemCount: _searchMode ? _cubit.searchedList.length : _cubit.postList.length,
),
)
],
);
},
),
),
);
}
void _clickOnPost() {
void _clickOnPost(Post post) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BlocProvider(
child: SinglePostScreen(post: post),
create: (context) => SinglePostCubit(),
);
},
),
);
}
}

33
lib/features/posts/widgets/post_item_widget.dart

@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/utils/app_utils.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
class PostItemWidget extends StatelessWidget {
const PostItemWidget({super.key});
final Post post;
const PostItemWidget({super.key, required this.post});
@override
Widget build(BuildContext context) {
@ -26,38 +30,41 @@ class PostItemWidget extends StatelessWidget {
children: [
Container(
height: context.height * 174 / AppConstants.instance.appHeight,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
color: Colors.red,
image: DecorationImage(
image: AssetImage(post.image),
fit: BoxFit.cover,
),
),
),
SizedBox(height: context.height * 14 / AppConstants.instance.appHeight),
const Text(
'عدم بیعت صحابه با ابوبکر+سند',
style: TextStyle(
Text(
post.name,
style: const TextStyle(
color: Color(0xff222D4E),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: context.height * 8 / AppConstants.instance.appHeight),
const Text(
'سعد بن عباده انصار که از صحابه به شمار می آید هرگز با ابوبکر بیعت نکرد ، یعنی او را به خلافت قبول نداشت (1) چرا کسانی را که خلافت ابوبکر را قبول ندارند و پیرو سعد بن عباده می باشند ، هدایت یافته نمی دانید ',
style: TextStyle(
Text(
post.description,
style: const TextStyle(
color: Color(0xff8990A1),
fontSize: 13,
),
textAlign: TextAlign.justify,
),
SizedBox(height: context.height * 30 / AppConstants.instance.appHeight),
const Align(
Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(
'1404/12/25',
style: TextStyle(
Utils.instance.dateToString(post.date),
style: const TextStyle(
color: Color(0xff8D95AB),
fontSize: 11,
fontWeight: FontWeight.bold,

106
lib/features/posts/widgets/search_widget.dart

@ -0,0 +1,106 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.dart';
class SearchWidget extends StatefulWidget {
final Function(String query) search;
const SearchWidget({super.key, required this.search});
@override
State<SearchWidget> createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 49 * context.height / AppConstants.instance.appHeight,
child: TextFormField(
autofocus: false,
maxLength: 300,
controller: _controller,
onChanged: (value) {
Debounce.debounce('search_query', const Duration(milliseconds: 1000), () {
widget.search(_controller.text.trim());
});
},
maxLines: 10,
minLines: 4,
style: const TextStyle(
color: Color(0xff8D95AB),
fontSize: 10,
),
decoration: InputDecoration(
fillColor: const Color(0xffF4F4F8),
contentPadding: const EdgeInsets.symmetric(
vertical: 15,
horizontal: 18,
),
hintText: Translator.translate('search'),
filled: true,
counterText: '',
hintStyle: const TextStyle(
color: Color(0xff8D95AB),
fontSize: 10,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(19),
borderSide: const BorderSide(color: Colors.transparent),
),
suffixIcon: GestureDetector(
onTap: () {
_controller.text = '';
widget.search.call(_controller.text.trim());
},
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 3, bottom: 4, top: 4),
child: SvgPicture.asset('ic_clear'.svgPath),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(19),
borderSide: const BorderSide(color: Colors.transparent),
),
),
),
);
}
}
class Debounce {
static final Map<String, Timer> _timers = {};
static void debounce(String tag, Duration duration, Function onExecute) {
if (duration == Duration.zero) {
_timers[tag]?.cancel();
_timers.remove(tag);
onExecute();
} else {
_timers[tag]?.cancel();
_timers[tag] = Timer(duration, () {
_timers[tag]?.cancel();
_timers.remove(tag);
onExecute();
});
}
}
static void cancel(String tag) {
_timers[tag]?.cancel();
_timers.remove(tag);
}
}

34
lib/features/single_post/cubit/single_post_cubit.dart

@ -0,0 +1,34 @@
import 'dart:math';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/features/single_post/view_models/comment.dart';
class SinglePostCubit extends Cubit<BaseCubitType<SinglePostState>> {
List<Comment> commentList = [];
SinglePostCubit() : super(BaseCubitType(eventName: SinglePostState.empty));
void empty() => emit(BaseCubitType(eventName: SinglePostState.empty));
void addComment(String comment) {
Comment newComment = Comment(
date: DateTime.now().millisecondsSinceEpoch,
text: comment,
author: 'محسن زمانی',
id: Random.secure().nextInt(10000),
);
commentList.insert(0, newComment);
emit(BaseCubitType(eventName: SinglePostState.data));
}
void deleteComment(int id) {
commentList.removeWhere((element) => element.id == id);
emit(BaseCubitType(eventName: SinglePostState.data));
}
}
enum SinglePostState {
empty,
data,
}

301
lib/features/single_post/screen/single_post_screen.dart

@ -1,11 +1,19 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/html/html_viewer.dart';
import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.dart';
import 'package:sonnat/core/utils/app_utils.dart';
import 'package:sonnat/core/utils/base_cubit_type.dart';
import 'package:sonnat/core/widgets/shimmer.dart';
import 'package:sonnat/features/single_post/cubit/single_post_cubit.dart';
import 'package:sonnat/features/single_post/view_models/post.dart';
import 'package:sonnat/features/single_post/widget/add_comment_widget.dart';
import 'package:sonnat/features/single_post/widget/post_comment_widget.dart';
class SinglePostScreen extends StatefulWidget {
final Post post;
@ -17,135 +25,202 @@ class SinglePostScreen extends StatefulWidget {
}
class _SinglePostScreenState extends State<SinglePostScreen> {
late final SinglePostCubit _cubit;
@override
void initState() {
_cubit = BlocProvider.of<SinglePostCubit>(context);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Stack(
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: widget.post.thumbnail?.lg ?? '',
fit: BoxFit.cover,
errorWidget: (context, url, error) {
return const Icon(
Icons.wifi_off,
size: 80,
color: Colors.black54,
);
},
placeholder: (context, url) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(color: Colors.white),
),
),
const Positioned(
top: 0,
left: 8,
right: 8,
child: Row(
children: <Widget>[
BackButton(color: Colors.white),
],
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 150,
width: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.black.withOpacity(0),
],
),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8),
child: ClipOval(
child: CachedNetworkImage(
imageUrl: /*post.thumbnail?.sm ??*/ '',
height: 30,
width: 30,
errorWidget: (context, url, error) {
return Image.asset(
'assets/images/png/avatar.jpg',
cacheWidth: 30 ~/ 1,
cacheHeight: 30 ~/ 1,
);