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 { defaultConfig {
applicationId "com.example.sonnat" applicationId "com.example.sonnat"
minSdkVersion 16
minSdkVersion 19
targetSdkVersion 33 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName 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", "contact_to_us": "Contact us",
"send_message_to_us": "Send us a message", "send_message_to_us": "Send us a message",
"send_message": "Write your 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": "ویدئو", "video": "ویدئو",
"news": "اخبار", "news": "اخبار",
"specials": "ویژه\u200Cها", "specials": "ویژه\u200Cها",
"search_term": "...جستجوی عبارت",
"search_term": "جستجوی عبارت...",
"main_header_text": "پرتال جامع سنت", "main_header_text": "پرتال جامع سنت",
"second_header_text": " شرحی بر تاریـــخ تحلیل نشــده اســــلام", "second_header_text": " شرحی بر تاریـــخ تحلیل نشــده اســــلام",
"more_about_us": "با ما بیشتر آشنا شوید", "more_about_us": "با ما بیشتر آشنا شوید",
@ -13,5 +13,8 @@
"contact_to_us": "ارتباط با ما", "contact_to_us": "ارتباط با ما",
"send_message_to_us": "ارسال پیام به ما", "send_message_to_us": "ارسال پیام به ما",
"send_message": "پیام خود را بنویسید", "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( 'h1': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 2.3, Unit.rem), fontSize: FontSize(fontSizeFactor * 2.3, Unit.rem),
), ),
'h2': Style( 'h2': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 2.1, Unit.rem), fontSize: FontSize(fontSizeFactor * 2.1, Unit.rem),
), ),
'h3': Style( 'h3': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.9, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.9, Unit.rem),
), ),
'h4': Style( 'h4': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.7, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.7, Unit.rem),
), ),
'h5': Style( 'h5': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.6, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.6, Unit.rem),
), ),
'h6': Style( 'h6': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.4, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.4, Unit.rem),
), ),
'li': Style( 'li': Style(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
), ),
'a': Style( 'a': Style(
color: textColor, color: textColor,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
textAlign: TextAlign.justify,
fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
), ),
'ol': Style( 'ol': Style(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
textAlign: TextAlign.justify,
), ),
'html': Style( 'html': Style(
fontSize: FontSize(baseFontSize * fontSizeFactor), 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}) { 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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:shamsi_date/shamsi_date.dart'; import 'package:shamsi_date/shamsi_date.dart';
import 'package:sonnat/core/extensions/number_extension.dart'; import 'package:sonnat/core/extensions/number_extension.dart';
import 'package:sonnat/core/language/language_cubit.dart'; import 'package:sonnat/core/language/language_cubit.dart';
@ -105,4 +106,9 @@ class Utils {
vertical: (vertical ?? 0).w, 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart'; import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart'; import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/language/translator.dart'; import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.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/main/widget/main_item_widget.dart';
import 'package:sonnat/features/posts/cubit/posts_cubit.dart';
import 'package:sonnat/features/posts/screen/posts_screen.dart'; import 'package:sonnat/features/posts/screen/posts_screen.dart';
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
@ -37,159 +38,136 @@ class _MainScreenState extends State<MainScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xff26237A), 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}) { 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:sonnat/core/extensions/context_extension.dart'; import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/extensions/string_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/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/filter_item_widget.dart';
import 'package:sonnat/features/posts/widgets/post_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 { class PostsScreen extends StatefulWidget {
final String title; final String title;
final bool searchMode;
const PostsScreen({super.key, required this.title});
const PostsScreen({super.key, required this.title, this.searchMode = false});
@override @override
State<PostsScreen> createState() => _PostsScreenState(); State<PostsScreen> createState() => _PostsScreenState();
} }
class _PostsScreenState extends State<PostsScreen> { 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 = [ final List<FilterItem> _filterList = [
const FilterItem(selected: false, title: 'پر تکرارترین‌ها'), const FilterItem(selected: false, title: 'پر تکرارترین‌ها'),
const FilterItem(selected: true, title: 'مشابه‌ها'), const FilterItem(selected: true, title: 'مشابه‌ها'),
@ -29,82 +46,232 @@ class _PostsScreenState extends State<PostsScreen> {
const FilterItem(selected: false, title: 'محبوبترین‌ها'), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( 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:flutter/material.dart';
import 'package:sonnat/core/extensions/context_extension.dart'; import 'package:sonnat/core/extensions/context_extension.dart';
import 'package:sonnat/core/utils/app_constants.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 { class PostItemWidget extends StatelessWidget {
const PostItemWidget({super.key});
final Post post;
const PostItemWidget({super.key, required this.post});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,38 +30,41 @@ class PostItemWidget extends StatelessWidget {
children: [ children: [
Container( Container(
height: context.height * 174 / AppConstants.instance.appHeight, height: context.height * 174 / AppConstants.instance.appHeight,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: 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), SizedBox(height: context.height * 14 / AppConstants.instance.appHeight),
const Text(
'عدم بیعت صحابه با ابوبکر+سند',
style: TextStyle(
Text(
post.name,
style: const TextStyle(
color: Color(0xff222D4E), color: Color(0xff222D4E),
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
SizedBox(height: context.height * 8 / AppConstants.instance.appHeight), SizedBox(height: context.height * 8 / AppConstants.instance.appHeight),
const Text(
'سعد بن عباده انصار که از صحابه به شمار می آید هرگز با ابوبکر بیعت نکرد ، یعنی او را به خلافت قبول نداشت (1) چرا کسانی را که خلافت ابوبکر را قبول ندارند و پیرو سعد بن عباده می باشند ، هدایت یافته نمی دانید ',
style: TextStyle(
Text(
post.description,
style: const TextStyle(
color: Color(0xff8990A1), color: Color(0xff8990A1),
fontSize: 13, fontSize: 13,
), ),
textAlign: TextAlign.justify, textAlign: TextAlign.justify,
), ),
SizedBox(height: context.height * 30 / AppConstants.instance.appHeight), SizedBox(height: context.height * 30 / AppConstants.instance.appHeight),
const Align(
Align(
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: Text( child: Text(
'1404/12/25',
style: TextStyle(
Utils.instance.dateToString(post.date),
style: const TextStyle(
color: Color(0xff8D95AB), color: Color(0xff8D95AB),
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.bold, 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:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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/context_extension.dart';
import 'package:sonnat/core/extensions/string_extension.dart';
import 'package:sonnat/core/html/html_viewer.dart'; import 'package:sonnat/core/html/html_viewer.dart';
import 'package:sonnat/core/language/translator.dart'; import 'package:sonnat/core/language/translator.dart';
import 'package:sonnat/core/utils/app_constants.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/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 { class SinglePostScreen extends StatefulWidget {
final Post post; final Post post;
@ -17,135 +25,202 @@ class SinglePostScreen extends StatefulWidget {
} }
class _SinglePostScreenState extends State<SinglePostScreen> { class _SinglePostScreenState extends State<SinglePostScreen> {
late final SinglePostCubit _cubit;
@override
void initState() {
_cubit = BlocProvider.of<SinglePostCubit>(context);
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( 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,
);
},
placeholder: (context, url) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(color: Colors.white),
child: BlocBuilder<SinglePostCubit, BaseCubitType<SinglePostState>>(
builder: (context, state) {
switch (state.eventName!) {
case SinglePostState.empty:
break;
case SinglePostState.data:
break;
}
return SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: 304,
child: Stack(
fit: StackFit.expand,
children: [
Image.asset(
widget.post.image,
fit: BoxFit.cover,
),
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),
],
), ),
), ),
), ),
), ),
Text(
"${Translator.translate('author')} : ${widget.post.author ?? ""}",
style: const TextStyle(fontSize: 10),
Padding(
padding: EdgeInsetsDirectional.only(
start: context.width * 26 / AppConstants.instance.appWidth,
end: context.width * 37 / AppConstants.instance.appWidth,
bottom: 18,
),
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: SvgPicture.asset('ic_share'.svgPath),
),
const SizedBox(width: 16),
Padding(
padding: const EdgeInsets.all(8),
child: SvgPicture.asset('ic_bookmark'.svgPath),
),
const SizedBox(width: 16),
Padding(
padding: const EdgeInsets.all(8),
child: SvgPicture.asset('ic_like'.svgPath),
),
const Spacer(),
Text(
_cubit.commentList.length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 17,
),
),
Padding(
padding: const EdgeInsets.all(8),
child: SvgPicture.asset('ic_comment'.svgPath),
),
const SizedBox(width: 21),
const Text(
'29',
style: TextStyle(
color: Colors.white,
fontSize: 17,
),
),
Padding(
padding: const EdgeInsets.all(8),
child: SvgPicture.asset('ic_view'.svgPath),
),
],
),
),
),
const Padding(
padding: EdgeInsets.all(8),
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [],
),
),
), ),
], ],
), ),
), ),
),
const Padding(
padding: EdgeInsets.all(8),
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [],
Padding(
padding: EdgeInsetsDirectional.only(
start: context.width * 26 / AppConstants.instance.appWidth,
end: context.width * 37 / AppConstants.instance.appWidth,
), ),
),
),
],
),
Padding(
padding: EdgeInsetsDirectional.only(
start: context.width * 26 / AppConstants.instance.appWidth,
end: context.width * 37 / AppConstants.instance.appWidth,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'عدم بیعت صحابه با ابوبکر+سند',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.post.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
Utils.instance.dateToString(widget.post.date),
style: const TextStyle(fontSize: 11),
),
HTMLViewer(
htmlContent: widget.post.description,
fontSizeFactor: 1,
),
const SizedBox(height: 30),
Container(
width: context.width,
height: 1,
color: const Color(0xffD3D8E9),
),
const SizedBox(height: 30),
AddCommentWidget(sendComment: _cubit.addComment),
if (_cubit.commentList.isEmpty) const SizedBox(height: 30),
const SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return PostCommentWidget(comment: _cubit.commentList[index]);
},
itemCount: _cubit.commentList.length,
),
if (_cubit.commentList.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 15, bottom: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
Translator.translate('show_all_comments'),
style: const TextStyle(
fontSize: 10,
color: Color(0xff404966),
),
),
const SizedBox(width: 4),
SvgPicture.asset(
'ic_arrow_down'.svgPath,
width: 8,
height: 5,
),
],
),
),
],
), ),
), ),
const Text(
'1404/12/25',
style: TextStyle(fontSize: 11),
),
HTMLViewer(
htmlContent: widget.post.content ?? '',
fontSizeFactor: 1,
),
], ],
), ),
),
],
);
},
), ),
), ),
); );

8
lib/features/single_post/view_models/comment.dart

@ -0,0 +1,8 @@
class Comment {
String author;
String text;
int date;
int id;
Comment({required this.date, required this.text, required this.author, required this.id});
}

145
lib/features/single_post/view_models/post.dart

@ -1,120 +1,35 @@
import 'package:sonnat/features/single_post/view_models/thumbnail.dart';
class Post { class Post {
String? _url;
Thumbnails? _thumbnail;
List<Categories>? _categories;
String? _author;
String? _title;
String? _content;
String? _summary;
bool? _asSpecial;
bool? _status;
String? _createdAt;
String? get url => _url;
Thumbnails? get thumbnail => _thumbnail;
List<Categories>? get categories => _categories;
String? get author => _author;
String? get title => _title;
String? get content => _content;
String? get summary => _summary;
bool? get asSpecial => _asSpecial;
bool? get status => _status;
String? get createdAt => _createdAt;
Post(
{String? url,
Thumbnails? thumbnail,
List<Categories>? categories,
String? author,
String? title,
String? content,
String? summary,
bool? asSpecial,
bool? status,
String? createdAt}) {
_url = url;
_thumbnail = thumbnail;
_categories = categories;
_author = author;
_title = title;
_content = content;
_summary = summary;
_asSpecial = asSpecial;
_status = status;
_createdAt = createdAt;
}
Post.fromJson(dynamic json) {
_url = json['url'];
_thumbnail = json['thumbnail'] != null ? Thumbnails.fromJson(json['thumbnail']) : null;
if (json['categories'] != null) {
_categories = [];
json['categories'].forEach((v) {
_categories?.add(Categories.fromJson(v));
});
}
_author = json['author'];
_title = json['title'];
_content = json['content'];
_summary = json['summary'];
_asSpecial = json['as_special'];
_status = json['status'];
_createdAt = json['created_at'];
}
Map<String, dynamic> toJson() {
var map = <String, dynamic>{};
map['url'] = _url;
if (_thumbnail != null) {
map['thumbnail'] = _thumbnail?.toJson();
}
if (_categories != null) {
map['categories'] = _categories?.map((v) => v.toJson()).toList();
}
map['author'] = _author;
map['title'] = _title;
map['content'] = _content;
map['summary'] = _summary;
map['as_special'] = _asSpecial;
map['status'] = _status;
map['created_at'] = _createdAt;
return map;
}
}
class Categories {
String? _name;
String? _url;
String? get name => _name;
String? get url => _url;
Categories({String? name, String? url}) {
_name = name;
_url = url;
}
Categories.fromJson(dynamic json) {
_name = json['name'];
_url = json['url'];
int id;
String name;
String description;
String image;
int date;
Post({
required this.description,
required this.name,
required this.image,
required this.id,
required this.date,
});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
name: json['name'],
image: json['image'],
description: json['description'],
id: json['id'],
date: json['date'],
);
} }
Map<String, dynamic> toJson() {
var map = <String, dynamic>{};
map['name'] = _name;
map['url'] = _url;
return map;
Post copyWith(int date) {
return Post(
description: description,
name: name,
image: image,
id: id,
date: date,
);
} }
} }

31
lib/features/single_post/view_models/thumbnail.dart

@ -1,31 +0,0 @@
class Thumbnails {
String? _sm;
String? _md;
String? _lg;
String? get sm => _sm;
String? get md => _md;
String? get lg => _lg;
Thumbnails({String? sm, String? md, String? lg}) {
_sm = sm;
_md = md;
_lg = lg;
}
Thumbnails.fromJson(dynamic json) {
_sm = json['sm'];
_md = json['md'];
_lg = json['lg'];
}
Map<String, dynamic> toJson() {
var map = <String, dynamic>{};
map['sm'] = _sm;
map['md'] = _md;
map['lg'] = _lg;
return map;
}
}

71
lib/features/single_post/widget/add_comment_widget.dart

@ -0,0 +1,71 @@
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 AddCommentWidget extends StatefulWidget {
final Function(String comment) sendComment;
const AddCommentWidget({super.key, required this.sendComment});
@override
State<AddCommentWidget> createState() => _AddCommentWidgetState();
}
class _AddCommentWidgetState extends State<AddCommentWidget> {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return SizedBox(
height: 49 * context.height / AppConstants.instance.appHeight,
child: TextFormField(
autofocus: false,
maxLength: 300,
controller: _controller,
maxLines: 10,
minLines: 4,
style: const TextStyle(
color: Color(0xff8D95AB),
fontSize: 10,
),
decoration: InputDecoration(
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
vertical: 15,
horizontal: 18,
),
hintText: Translator.translate('write_comment'),
filled: true,
counterText: '',
hintStyle: const TextStyle(
color: Color(0xff8D95AB),
fontSize: 10,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(19),
borderSide: const BorderSide(color: Color(0xff636E88)),
),
suffixIcon: GestureDetector(
onTap: () {
if (_controller.text.trim().isNotEmpty) {
widget.sendComment.call(_controller.text.trim());
_controller.text = '';
}
},
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 18, bottom: 8, top: 8),
child: SvgPicture.asset('ic_send'.svgPath),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(19),
borderSide: const BorderSide(color: Color(0xff636E88)),
),
),
),
);
}
}

59
lib/features/single_post/widget/post_comment_widget.dart

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:sonnat/core/utils/app_utils.dart';
import 'package:sonnat/features/single_post/view_models/comment.dart';
class PostCommentWidget extends StatelessWidget {
final Comment comment;
const PostCommentWidget({super.key, required this.comment});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color(0xff8D95AB).withOpacity(0.35),
borderRadius: BorderRadius.circular(19),
),
margin: const EdgeInsets.only(bottom: 8, top: 8),
padding: const EdgeInsetsDirectional.only(
top: 14,
bottom: 18,
start: 18,
end: 23,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
comment.author,
style: const TextStyle(
color: Color(0xff404966),
fontSize: 8,
),
),
const SizedBox(width: 4),
Text(
Utils.instance.dateToString(comment.date),
style: const TextStyle(
color: Color(0xff404966),
fontSize: 8,
),
),
],
),
const SizedBox(height: 13),
Text(
comment.text,
style: const TextStyle(
color: Color(0xff636E88),
fontSize: 10,
fontFamily: 'Vazir',
),
),
],
),
);
}
}

44
pubspec.lock

@ -5,10 +5,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: android_intent_plus name: android_intent_plus
sha256: f79fbb8ccb64b5584d19caa9c3d15613bf21cfbd829a6ca7f089fb5dfd43f8aa
sha256: "2c87d8330ba5deef5fe20e77f4d178190b3b24531dce08368030ab4be40a9d4e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0"
version: "4.0.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -37,10 +37,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: bloc name: bloc
sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80"
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.1"
version: "8.1.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -241,10 +241,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f
sha256: "6ff8c902c8056af9736de2689f63f81c42e2d642b9f4c79dbf8790ae48b63012"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.5"
version: "2.0.6"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -311,6 +311,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "3.3.0"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -549,14 +557,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
shimmer:
dependency: "direct main"
description:
name: shimmer
sha256: "1f1009b5845a1f88f1c5630212279540486f97409e9fc3f63883e71070d107bf"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -654,10 +654,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "7aac14be5f4731b923cc697ae2d42043945076cd0dbb8806baecc92c1dc88891"
sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.33"
version: "6.0.34"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@ -718,26 +718,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics name: vector_graphics
sha256: "59a230f8bf37dd8b077335d1d64d895bccef0fb14f50730e3d79e8990bf3ed2b"
sha256: b96f10cbdfcbd03a65758633a43e7d04574438f059b1043104b5d61b23d38a4f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.5+1"
version: "1.1.6"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics_codec name: vector_graphics_codec
sha256: "40781fe91c6d10a617c0289f7ec16cdb2d85a7f3654af2778c6d0adbf3bf45a3"
sha256: "57a8e6e24662a3bdfe3b3d61257db91768700c0b8f844e235877b56480f31c69"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.5+1"
version: "1.1.6"
vector_graphics_compiler: vector_graphics_compiler:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics_compiler name: vector_graphics_compiler
sha256: "6ca1298b70edcc3486fdb14032f1a186a593f1b5f6b5e82fb10febddcb1c61bb"
sha256: "7430f5d834d0db4560d7b19863362cd892f1e52b43838553a3c5cdfc9ab28e5b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.5+1"
version: "1.1.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

2
pubspec.yaml

@ -18,13 +18,13 @@ dependencies:
android_intent_plus: ^4.0.0 android_intent_plus: ^4.0.0
csslib: ^0.17.2 csslib: ^0.17.2
collection: ^1.17.0 collection: ^1.17.0
intl: ^0.18.1
list_counter: ^1.0.2 list_counter: ^1.0.2
shamsi_date: ^1.0.1 shamsi_date: ^1.0.1
url_launcher: ^6.1.11 url_launcher: ^6.1.11
cached_network_image: ^3.1.0 cached_network_image: ^3.1.0
google_fonts: ^4.0.4 google_fonts: ^4.0.4
chewie_audio: ^1.5.0 chewie_audio: ^1.5.0
shimmer: ^2.0.0
local_db_core: local_db_core:
path: data/data_core/local_db/local_db_core path: data/data_core/local_db/local_db_core
repositories: repositories:

Loading…
Cancel
Save