mohsen zamani
2 years ago
32 changed files with 2368 additions and 507 deletions
-
2android/app/build.gradle
-
BINassets/images/png/ic_about.png
-
BINassets/images/png/image_mock.png
-
3assets/images/svg/ic_arrow_down.svg
-
3assets/images/svg/ic_bookmark.svg
-
7assets/images/svg/ic_clear.svg
-
3assets/images/svg/ic_comment.svg
-
3assets/images/svg/ic_like.svg
-
3assets/images/svg/ic_send.svg
-
9assets/images/svg/ic_share.svg
-
6assets/images/svg/ic_view.svg
-
5assets/languages/en.json
-
7assets/languages/fa.json
-
1061assets/meta/data.json
-
14lib/core/html/html_viewer.dart
-
6lib/core/utils/app_utils.dart
-
35lib/core/widgets/loading_list_widget.dart
-
213lib/core/widgets/shimmer.dart
-
260lib/features/main/main_screen.dart
-
108lib/features/posts/cubit/posts_cubit.dart
-
293lib/features/posts/screen/posts_screen.dart
-
33lib/features/posts/widgets/post_item_widget.dart
-
106lib/features/posts/widgets/search_widget.dart
-
34lib/features/single_post/cubit/single_post_cubit.dart
-
301lib/features/single_post/screen/single_post_screen.dart
-
8lib/features/single_post/view_models/comment.dart
-
145lib/features/single_post/view_models/post.dart
-
31lib/features/single_post/view_models/thumbnail.dart
-
71lib/features/single_post/widget/add_comment_widget.dart
-
59lib/features/single_post/widget/post_comment_widget.dart
-
44pubspec.lock
-
2pubspec.yaml
After Width: 512 | Height: 512 | Size: 20 KiB |
After Width: 1204 | Height: 696 | Size: 1.6 MiB |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
1061
assets/meta/data.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
@ -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}); |
||||
|
} |
@ -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, |
||||
|
); |
||||
} |
} |
||||
} |
} |
@ -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; |
|
||||
} |
|
||||
} |
|
@ -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)), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
@ -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', |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue