// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously import 'package:cross_file/cross_file.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:go_router/go_router.dart'; import 'package:hoshan/core/gen/assets.gen.dart'; import 'package:hoshan/core/services/api/dio_service.dart'; import 'package:hoshan/core/utils/date_time.dart'; import 'package:hoshan/core/utils/strings.dart'; import 'package:hoshan/data/model/empty_states_enum.dart'; import 'package:hoshan/data/model/forum_model.dart'; import 'package:hoshan/data/model/sort_by_model.dart'; import 'package:hoshan/data/model/tools_categories_model.dart'; import 'package:hoshan/data/repository/forum_repository.dart'; import 'package:hoshan/ui/screens/main/forum/cubit/category_cubit.dart'; import 'package:hoshan/ui/screens/main/forum/cubit/comments_cubit.dart'; import 'package:hoshan/ui/screens/main/forum/cubit/replies_cubit.dart'; import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; import 'package:hoshan/ui/theme/colors.dart'; import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; import 'package:hoshan/ui/theme/text.dart'; import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; import 'package:hoshan/ui/widgets/components/image/network_image.dart'; import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; class ForumScreen extends StatefulWidget { const ForumScreen({super.key}); @override State createState() => _ForumScreenState(); } class _ForumScreenState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; final ScrollController scrollController = ScrollController(); void loadCommentsFromCategory(int id) { if (id == CommentsCubit.categoriesId) return; if (context.read().state is CommentsInitial) return; context.read().loadComments(cId: id, retry: true); } void loadCommentsFromSortBy(SortByModel sortBy) { context.read().loadComments( cId: CommentsCubit.categoriesId, retry: true, orderBy: sortBy); } Future sendComment({required String message, XFile? file}) async { try { final comment = await ForumRepository.sendForum( text: message, categoryId: CommentsCubit.categoriesId, image: file, ); if (mounted) { final userInfo = UserInfoCubit.userInfoModel; final user = User( id: userInfo.id, image: userInfo.image, name: userInfo.name, username: userInfo.username); context .read() .addComment(comment: comment.copyWith(user: user)); context.pop(); } } on DioException catch (e) { if (kDebugMode) { print('Dio Error is: $e'); } } } Future sendReply( {required BuildContext context, required String message, required int parentId, required String repliedUserId, XFile? file}) async {} @override void initState() { super.initState(); scrollController.addListener( () { if (scrollController.position.pixels == scrollController.position.maxScrollExtent && context.read().state is CommentsSuccess) { context .read() .loadComments(cId: CommentsCubit.categoriesId); } }, ); } @override Widget build(BuildContext context) { super.build(context); return Scaffold( floatingActionButton: FloatingActionButton.small( heroTag: 'add-comment-fb', onPressed: () async { await BottomSheetHandler(context).showAddComment( onSend: (message, file) async => sendComment(message: message, file: file), ); }, shape: const CircleBorder(), backgroundColor: AppColors.primaryColor.defaultShade, child: const Icon( CupertinoIcons.add, color: Colors.white, ), ), body: RefreshIndicator( backgroundColor: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.primary, onRefresh: () async { // context.read().getAllCategorie(); context .read() .loadComments(cId: CommentsCubit.categoriesId, retry: true); scrollController.jumpTo(0); }, child: SingleChildScrollView( controller: scrollController, physics: context.watch().state is CommentsInitial ? const NeverScrollableScrollPhysics() : const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), child: Column( children: [ BlocBuilder( builder: (context, state) { if (state is CategoryLoading) { return categoriesPlaceholder(); } if (state is CategorySuccess) { final categories = state.categories; List oddCategories = []; List evenCategories = []; for (int i = 0; i < categories.length; i++) { if (i % 2 == 0) { evenCategories.add(categories[i]); } else { oddCategories.add(categories[i]); } } return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Directionality( textDirection: TextDirection.rtl, child: Stack( children: [ SizedBox( width: MediaQuery.sizeOf(context).width, child: SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.fromLTRB(120, 0, 16, 0), physics: const BouncingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (evenCategories.isNotEmpty) SizedBox( height: 36, child: Row( children: [ ...List.generate( evenCategories.length, (index) { final cat = evenCategories[index]; return categoryContainer(cat); }, ) ], ), ), const SizedBox( height: 8, ), if (oddCategories.isNotEmpty) SizedBox( height: 36, child: Row( children: [ const SizedBox( width: 16, ), ...List.generate( oddCategories.length, (index) { final cat = oddCategories[index]; return categoryContainer(cat); }, ) ], ), ), ], ), ), ), if (context.watch().state is CommentsSuccess || context.watch().state is CommentsFail) Positioned( left: 0, bottom: 0, top: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( boxShadow: [ BoxShadow( blurRadius: 16, color: Theme.of(context) .scaffoldBackgroundColor, spreadRadius: 10) ], color: Theme.of(context) .scaffoldBackgroundColor, gradient: RadialGradient(colors: [ Theme.of(context) .scaffoldBackgroundColor, Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.7), Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.6), ])), child: GestureDetector( onTap: () => BottomSheetHandler(context).showSortBy( initailValue: CommentsCubit.sortByModel, items: [ SortByModel( text: 'جدیدترین', value: 'date'), SortByModel( text: 'پربحث‌ترین‌ها', value: 'replies'), ], onSelected: (item) => loadCommentsFromSortBy(item), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: Icon( CupertinoIcons.sort_down, color: Theme.of(context) .colorScheme .onSurface, )), ), ), ) ], ), ), ); } return const SizedBox.shrink(); }, ), const SizedBox( height: 12, ), BlocBuilder( builder: (context, state) { if (state is CommentsInitial) { return ListView.builder( itemCount: 20, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return commentContainerPlaceholder(); }, ); } if (state.comments.isEmpty) { return Center( child: EmptyStates.getEmptyState( status: EmptyStatesEnum.messages)); } return Column( children: [ ListView.builder( itemCount: state.comments.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final comment = state.comments[index]; return BlocProvider( create: (context) => RepliesCubit(), child: Builder(builder: (context) { return Column( children: [ commentContainer(context, comment), Padding( padding: const EdgeInsets.only(right: 24), child: BlocBuilder( builder: (context, state) { return Column( children: [ if (state.replies.isNotEmpty) ListView.builder( physics: const NeverScrollableScrollPhysics(), itemCount: state.replies.length, shrinkWrap: true, itemBuilder: (context, index) { final reply = state.replies[index]; return commentContainer( context, reply, parrentComment: comment, ); }, ), BlocBuilder( builder: (context, state) { return Column( children: [ if (state.lastPage != null && state.page > state.lastPage!) Padding( padding: const EdgeInsets .symmetric( vertical: 12.0), child: GestureDetector( onTap: () { if (comment.replies != null && comment.replies! > 0) { if (state.lastPage != null && state.page > state .lastPage!) { final lastPosition = scrollController .position; context .read< RepliesCubit>() .clear(); scrollController.jumpTo(scrollController .position .pixels - lastPosition .pixels); } else { context .read< RepliesCubit>() .loadReplies( comment: comment); } } }, child: Row( children: [ const Expanded( flex: 6, child: Divider()), if (comment.replies != null && comment.replies! > 0) Padding( padding: const EdgeInsets .symmetric( horizontal: 8.0), child: Text( 'پنهان کردن پاسخ‌ها', style: AppTextStyles.body4.copyWith( color: AppColors .gray[context .read() .isDark() ? 600 : 900]), ), ), const Expanded( child: Divider()), ], ), ), ), ], ); }, ), ], ); }, ), ) ], ); }), ); }, ), if (state is CommentsLoading) Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: LinearProgressIndicator( color: AppColors.primaryColor.defaultShade, borderRadius: BorderRadius.circular(8), ), ), const SizedBox( height: 36, ) ], ); }, ) ], ), ), ), ); } GestureDetector categoryContainer(Categories cat) { return GestureDetector( onTap: () => loadCommentsFromCategory(cat.id!), child: Container( padding: const EdgeInsets.all(8), margin: const EdgeInsets.symmetric(horizontal: 4), constraints: const BoxConstraints(minWidth: 100), alignment: Alignment.center, decoration: BoxDecoration( color: CommentsCubit.categoriesId == cat.id ? AppColors.primaryColor[ context.read().isDark() ? 500 : 50] : AppColors .gray[context.read().isDark() ? 900 : 400], borderRadius: BorderRadius.circular(360)), child: Text( cat.name ?? '', style: AppTextStyles.body4.copyWith( color: context.read().isDark() ? Colors.white : CommentsCubit.categoriesId == cat.id ? AppColors.primaryColor.defaultShade : AppColors.black[300]), ), ), ); } Widget commentContainer(BuildContext context, Comment comment, {final Comment? parrentComment}) { final daysAgo = DateTimeUtils.getDaysBetweenNowAnd(comment.createdAt!); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: parrentComment != null ? Theme.of(context).colorScheme.surface : context.read().isDark() ? AppColors.black[900] : AppColors.primaryColor[50], borderRadius: BorderRadius.circular(16)), child: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ comment.createdAt != null ? Row( children: [ Assets.icon.outline.clock.svg( width: 20, height: 20, color: AppColors.gray[context .read() .isDark() ? 600 : 900]), const SizedBox( width: 8, ), Text( daysAgo == 0 ? 'امروز' : ('$daysAgo روز پیش'), style: AppTextStyles.body5.copyWith( color: Theme.of(context) .colorScheme .onSurface), textDirection: TextDirection.rtl, ) ], ) : const SizedBox.shrink(), Row( children: [ Text( comment.user?.username ?? '', style: AppTextStyles.body4.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context) .colorScheme .onSurface), ), const SizedBox( width: 8, ), ], ) ], ), const SizedBox( height: 8, ), SizedBox( width: MediaQuery.sizeOf(context).width, child: Text( comment.text ?? '', style: AppTextStyles.body3.copyWith( color: Theme.of(context).colorScheme.onSurface), textDirection: comment.text != null && comment.text!.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, ), ), const SizedBox( height: 8, ), if (comment.image != null) Column( children: [ Container( margin: const EdgeInsets.symmetric(vertical: 12), constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf(context).height * 0.2), alignment: Alignment.centerRight, child: AspectRatio( aspectRatio: 16 / 9, child: ImageNetwork( url: DioService.baseURL + comment.image!, radius: 16, showHero: true, ), ), ), const SizedBox( height: 8, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ GestureDetector( onTap: () { BottomSheetHandler(context) .showReportOptions(); }, child: commentBtn( icon: Assets.icon.outline.flag2, text: 'گزارش')), const SizedBox( width: 12, ), // BlocProvider( // create: (context) => // CommentLikeCubit()..getLike(comment.userFeedback), // child: BlocConsumer( // listener: (context, state) { // int likes = comment.likes!; // int disLikes = comment.dislikes!; // if (state.like != null) { // likes = likes + state.like!; // } // if (state.disLike != null) { // disLikes = disLikes + state.disLike!; // } // context.read().changeComment( // newComment: comment.copyWith( // likes: likes, // dislikes: disLikes, // userFeedback: state is CommentLiked // ? 1 // : state is CommentDisLiked // ? 0 // : -1)); // }, // builder: (context, state) { // return Row( // children: [ // DefaultPlaceHolder( // enabled: state is CommentLikeLoading, // child: GestureDetector( // onTap: () => context // .read() // .setLiked( // id: comment.id!, // status: // state is CommentLiked ? -1 : 1), // child: commentBtn( // color: state is CommentLiked // ? AppColors.green.defaultShade // : null, // icon: state is CommentLiked // ? Assets.icon.bold.like // : Assets.icon.outline.like, // text: comment.likes?.toString() ?? '0'), // ), // ), // const SizedBox( // width: 12, // ), // DefaultPlaceHolder( // enabled: state is CommentLikeLoading, // child: GestureDetector( // onTap: () => context // .read() // .setLiked( // id: comment.id!, // status: state is CommentDisLiked // ? -1 // : 0), // child: commentBtn( // color: state is CommentDisLiked // ? AppColors.red.defaultShade // : null, // icon: state is CommentDisLiked // ? Assets.icon.bold.dislike // : Assets.icon.outline.dislike, // text: // comment.dislikes?.toString() ?? '0'), // ), // ), // ], // ); // }, // ), // ), // const SizedBox( // width: 12, // ), if (comment.replies != null && parrentComment == null && comment.replies != 0) commentBtn( icon: Assets.icon.outline.messageText, text: comment.replies!.toString()), ], ), GestureDetector( onTap: () async { await BottomSheetHandler(context).showAddComment( comment: comment, onSend: (message, file) async { try { final commentRes = await ForumRepository.sendForum( text: message, categoryId: CommentsCubit.categoriesId, image: file, parentId: parrentComment?.id ?? comment.id, repliedUserId: parrentComment?.user!.id ?? comment.user!.id); if (mounted) { final userInfo = UserInfoCubit.userInfoModel; final user = User( id: userInfo.id, image: userInfo.image, name: userInfo.name, username: userInfo.username); context.read().addReply( parent: comment, comment: commentRes.copyWith(user: user)); context.read().addReplies( commentId: parrentComment?.id! ?? comment.id!); context.pop(); } } on DioException catch (e) { if (kDebugMode) { print('Dio Error is: $e'); } } }, ); }, child: Text('پاسخ دادن', style: AppTextStyles.body4.copyWith( color: Theme.of(context).colorScheme.primary, )), ) ], ), ], ), ), const SizedBox( width: 12, ), ImageNetwork( url: comment.user != null && comment.user!.image != null ? DioService.baseURL + comment.user!.image! : 'https://placehold.co/600x400', width: 40, height: 40, radius: 360, ) ], ), ), if (comment.replies != null && comment.replies! > 0) BlocBuilder( builder: (context, state) { if (state.lastPage != null && state.page > state.lastPage!) { return const SizedBox(); } return Column( children: [ state is RepliesLoading ? Padding( padding: const EdgeInsets.only(top: 24.0), child: SpinKitThreeBounce( color: AppColors.primaryColor.defaultShade, size: 32, ), ) : Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: GestureDetector( onTap: () { if (comment.replies != null && comment.replies! > 0) { if (state.lastPage != null && state.page > state.lastPage!) { final lastPosition = scrollController.position; context.read().clear(); scrollController.jumpTo( scrollController.position.pixels - lastPosition.pixels); } else { context .read() .loadReplies(comment: comment); } } }, child: Row( children: [ const Expanded(flex: 6, child: Divider()), Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0), child: Text( 'مشاهده پاسخ‌ها', style: AppTextStyles.body4.copyWith( color: AppColors.gray[context .read() .isDark() ? 600 : 900]), ), ), const Expanded(child: Divider()), ], ), ), ), ], ); }, ), ], ), ); } Padding categoriesPlaceholder() { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Directionality( textDirection: TextDirection.rtl, child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), child: Column( children: [ SizedBox( height: 36, child: Row( children: [ ...List.generate( 20, (index) { return DefaultPlaceHolder( child: Container( padding: const EdgeInsets.all(8), margin: const EdgeInsets.symmetric(horizontal: 4), constraints: const BoxConstraints(minWidth: 100), alignment: Alignment.center, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(360)), child: Text('$index'), ), ); }, ) ], ), ), const SizedBox( height: 8, ), SizedBox( height: 36, child: Row( children: [ const SizedBox( width: 24, ), ...List.generate( 20, (index) { return DefaultPlaceHolder( child: Container( padding: const EdgeInsets.all(8), margin: const EdgeInsets.symmetric(horizontal: 4), constraints: const BoxConstraints(minWidth: 100), alignment: Alignment.center, decoration: BoxDecoration( color: AppColors.gray[400], borderRadius: BorderRadius.circular(360)), child: Text('$index'), ), ); }, ) ], ), ), ], ), ), ), ); } Container commentContainerPlaceholder() { return Container( padding: const EdgeInsets.all(24), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ DefaultPlaceHolder( child: Assets.icon.outline.clock.svg( width: 20, height: 20, color: AppColors.gray[900]), ), const SizedBox( width: 8, ), DefaultPlaceHolder( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Colors.white, ), child: Text( 'time ago', style: AppTextStyles.body5, ), ), ) ], ), Row( children: [ DefaultPlaceHolder( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Colors.white, ), child: Text( 'this is a username', style: AppTextStyles.body4 .copyWith(fontWeight: FontWeight.bold), ), ), ), const SizedBox( width: 8, ), ], ) ], ), const SizedBox( height: 8, ), DefaultPlaceHolder( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Colors.white, ), child: Text( 'بات‌های تولید عکس می‌تونه تصاویری با سبک‌های مختلف مثل نقاشی، سه‌بعدی یا رئال تولید کنه؟🎨🎨🎨', style: AppTextStyles.body3, textDirection: TextDirection.rtl, ), ), ), const SizedBox( height: 8, ), Row( children: [ Row( children: [ DefaultPlaceHolder( child: commentBtn( icon: Assets.icon.outline.flag2, text: 'گزارش'), ), const SizedBox( width: 12, ), DefaultPlaceHolder( child: commentBtn( icon: Assets.icon.outline.like, text: '---'), ), const SizedBox( width: 12, ), DefaultPlaceHolder( child: commentBtn( icon: Assets.icon.outline.dislike, text: '---'), ), const SizedBox( width: 12, ), DefaultPlaceHolder( child: commentBtn( icon: Assets.icon.outline.messageText, text: '---'), ), ], ), ], ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ DefaultPlaceHolder( child: Text('پاسخ دادن', style: AppTextStyles.body4.copyWith( color: AppColors.primaryColor.defaultShade, )), ) ], ) ], ), ), const SizedBox( width: 12, ), const DefaultPlaceHolder( child: ImageNetwork( url: 'https://placehold.co/600x400', width: 40, height: 40, radius: 360, ), ) ], ), ], ), ); } Row commentBtn( {required final SvgGenImage icon, final String? text, final Color? color}) { return Row( children: [ icon.svg( width: 18, height: 18, color: color ?? AppColors .gray[context.read().isDark() ? 600 : 900]), if (text != null) Row( children: [ const SizedBox( width: 4, ), Text( text, style: AppTextStyles.body4.copyWith( color: AppColors.gray[ context.read().isDark() ? 600 : 900]), ), ], ) ], ); } }