// ignore_for_file: use_build_context_synchronously, avoid_print import 'dart:math'; import 'package:before_after/before_after.dart'; import 'package:carousel_slider/carousel_slider.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/gen/my_flutter_app_icons.dart'; import 'package:hoshan/core/routes/route_generator.dart'; import 'package:hoshan/core/services/file_manager/download_file_services.dart'; import 'package:hoshan/data/model/ai/chats_history_model.dart'; import 'package:hoshan/data/model/ai/messages_model.dart'; import 'package:hoshan/data/model/ai/send_message_model.dart'; import 'package:hoshan/data/model/chat_args.dart'; import 'package:hoshan/data/storage/shared_preferences_helper.dart'; import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; import 'package:hoshan/ui/screens/library/library_screen.dart'; import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; import 'package:hoshan/ui/screens/gmedia/send_image_modal.dart'; import 'package:hoshan/ui/theme/colors.dart'; import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; import 'package:hoshan/ui/theme/responsive.dart'; import 'package:hoshan/ui/theme/text.dart'; import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; import 'package:hoshan/ui/widgets/components/image/network_image.dart'; import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; import 'package:share_plus/share_plus.dart'; class PhotoChatPage extends StatefulWidget { final ChatArgs chatArgs; const PhotoChatPage({super.key, required this.chatArgs}); @override State createState() => _PhotoChatPageState(); } class _PhotoChatPageState extends State { final CarouselSliderController _carouselController = CarouselSliderController(); final TextEditingController _query = TextEditingController(); final ValueNotifier _currentIndex = ValueNotifier(0); final ValueNotifier _comp = ValueNotifier(0.5); final ValueNotifier _compBot = ValueNotifier(0.5); final FocusNode _textFieldFocus = FocusNode(); bool inComp = false; late final ptp = widget.chatArgs.bot.attachmentType?.contains('image') ?? false; final ValueNotifier isGhost = ValueNotifier(false); late final bot = widget.chatArgs.bot; late int? chatId = widget.chatArgs.chatId; final ValueNotifier maxSize = ValueNotifier(1); List> groupMessages(List messages) { return messages.fold>>([], (acc, message) { if (acc.isEmpty || (acc.last.first.fromBot != message.fromBot && acc.last.length == 2) || (acc.last.first.fromBot == message.fromBot && message.fromBot!) || (acc.last.first.fromBot == message.fromBot && !message.fromBot!)) { acc.add([message]); } else { acc.last.add(message); } return acc; }); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().add(RestartChatsHistory()); context.read().add(const GetAllChats(type: 'image')); if (!GuidsStorage.isSeenImage()) { DialogHandler(context: context).onPhotoCreated(); GuidsStorage.setSeenImage(true); } }); } @override Widget build(BuildContext context) { return Scaffold( body: Responsive(context).maxWidthInDesktop( child: (contxet, mw) => Scaffold( appBar: AppBar(), drawer: Drawer( shape: const BeveledRectangleBorder(borderRadius: BorderRadius.zero), child: LibraryScreen( type: 'image', onTap: (chat) { context.push(Routes.photoToPhoto, extra: ChatArgs(bot: chat.bot!, chatId: chat.id)); }, )), bottomSheet: Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: ptp ? Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: LoadingButton( width: double.infinity, onPressed: () { Navigator.of(context).push(SendImageModal( onFileSelected: (image) { if (context .read() .state is MediaGResponseLoading) { return; } context.read().request( SendMessageModel( ghost: isGhost.value, messageId: DateTime.now() .toIso8601String(), file: image.xFile, botId: widget.chatArgs.bot.id, id: chatId)); }, )); }, backgroundColor: Theme.of(context).colorScheme.primary, child: Text( 'بارگذاری عکس', style: AppTextStyles.body4 .copyWith(color: Colors.white), )), ) : Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Directionality( textDirection: TextDirection.rtl, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ CircleIconBtn( icon: Assets.icon.bold.send, color: Theme.of(context).colorScheme.primary, iconColor: Colors.white, iconPadding: const EdgeInsets.all(6), size: 26, onTap: () { if (context .read() .state is MediaGResponseLoading) { return; } context.read().request( SendMessageModel( messageId: DateTime.now() .toIso8601String(), query: _query.text, botId: widget.chatArgs.bot.id, ghost: isGhost.value, id: chatId)); _query.clear(); }, ), const SizedBox( width: 8, ), Flexible( child: TextField( controller: _query, focusNode: _textFieldFocus, onTapOutside: (event) { _textFieldFocus.unfocus(); }, style: AppTextStyles.body4.copyWith( color: Theme.of(context) .colorScheme .onSurface), decoration: InputDecoration.collapsed( hintText: 'تصویری که می‌خوای رو توصیف کن...', hintStyle: AppTextStyles.body4.copyWith( color: AppColors.gray[context .read() .isDark() ? 600 : 900])), ), ), ], ), ), ), ), ], ), ), floatingActionButton: ValueListenableBuilder( valueListenable: _currentIndex, builder: (context, value, _) { return ValueListenableBuilder( valueListenable: maxSize, builder: (context, size, child) { return value < size - 1 ? Padding( padding: const EdgeInsets.only(bottom: 64.0), child: FloatingActionButton.small( shape: const CircleBorder(), onPressed: () { _carouselController.animateToPage(size - 1); }, child: Transform.rotate( angle: pi / 2, child: Assets.icon.outline.arrowRight.svg( color: Colors.white, ), ), )) : const SizedBox.shrink(); }, ); }), body: Stack( children: [ Assets.image.imageGBack.image( width: MediaQuery.sizeOf(context).width, height: MediaQuery.sizeOf(context).height, color: Theme.of(context).scaffoldBackgroundColor.withAlpha(240), colorBlendMode: BlendMode.multiply, fit: BoxFit.cover, opacity: AlwaysStoppedAnimation( context.read().isDark() ? 0.5 : 0.2)), Positioned.fill(child: BlocBuilder( builder: (context, mState) { if (mState is MessagesFail) { return const SizedBox(); } if (mState is MessagesLoading) { return const Center( child: CircularProgressIndicator(), ); } // if (state is MessagesSuccess) { final m = mState.messages; List> messages = groupMessages(m); return BlocConsumer( listener: (context, state) async { if (state is MediaGResponseSucess) { context.read().add(AddMessage( message: Messages( query: state.query, file: state.file, createdAt: DateTime.now().toIso8601String(), error: state.response.error, id: state.response.humanMessageId, role: 'user'))); if (!(state.response.error ?? true)) { context.read().add(AddMessage( message: Messages( content: [ Content( imageUrl: FileUrl( url: state.response.content)) ], createdAt: DateTime.now().toIso8601String(), error: state.response.error, id: state.response.aiMessageId, role: 'ai'))); } if (chatId == null && !isGhost.value) { context.read().add(AddChat( chats: Chats( bot: bot, title: state.response.chatTitle, createdAt: DateTime.now().toIso8601String(), id: state.response.chatId))); } chatId = state.response.chatId; } }, builder: (context, state) { maxSize.value = messages.length + 1; print('📊 Messages count: ${messages.length}'); print('📊 Bot name: ${bot.name}'); return ListView( physics: const BouncingScrollPhysics(), children: [ // Bot info section Column( children: [ const SizedBox( height: 16, ), ptp ? ValueListenableBuilder( valueListenable: _compBot, builder: (context, val, child) => SizedBox( width: 86 * 2, height: 100 * 2, child: BeforeAfter( trackWidth: 4, thumbWidth: 18, value: val, onValueChanged: (value) => _compBot.value = value, before: AspectRatio( aspectRatio: 3 / 4, child: ImageNetwork( width: double.infinity, height: double.infinity, radius: 12, showHero: true, url: bot.image2 ?? '')), after: AspectRatio( aspectRatio: 3 / 4, child: ImageNetwork( width: double.infinity, height: double.infinity, radius: 12, showHero: true, url: bot.image ?? '')), ), )) : ImageNetwork( width: 86, height: 100, url: bot.image ?? ''), const SizedBox( height: 8, ), Text( bot.name ?? '', style: AppTextStyles.headline6.copyWith( color: Theme.of(context) .colorScheme .onSurface), ), const SizedBox( height: 8, ), if (bot.description != null) Container( margin: const EdgeInsets.symmetric( horizontal: 16), padding: const EdgeInsets.all(8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of(context) .colorScheme .surface), child: Text( bot.description!, style: AppTextStyles.body4.copyWith( color: Theme.of(context) .colorScheme .onSurface), textDirection: TextDirection.rtl, textAlign: TextAlign.justify, ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: isGhost, builder: (context, g, _) { return Transform.scale( scale: 0.8, child: Switch.adaptive( value: g, thumbIcon: WidgetStateProperty .resolveWith( (Set states) { if (states.contains( WidgetState.selected)) { return Icon( CustomIcons.ghost, color: Theme.of(context) .colorScheme .onSurface); } return Icon(Icons.close, color: Theme.of(context) .colorScheme .onSurface); }), onChanged: (value) { isGhost.value = value; }, ), ); }), const SizedBox( width: 8, ), Text( 'حالت ناشناس', style: AppTextStyles.body4.copyWith( color: Theme.of(context) .colorScheme .onSurface, fontWeight: FontWeight.bold), ), const SizedBox( width: 8, ), Padding( padding: const EdgeInsets.only( bottom: 4.0), child: HintTooltip( hint: 'با فعال کردن این گزینه؛ چت‌های شما در قسمت تاریخچه، ذخیره نمی‌شوند و اطلاعاتتان ناشناس باقی می‌ماند.', iconColor: Theme.of(context) .colorScheme .onSurface, ), ) ], ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .secondary, borderRadius: BorderRadius.circular(12)), child: Row( children: [ Text( bot.cost == 0 || bot.cost == null ? 'رایگان' : bot.cost.toString(), style: AppTextStyles.body3 .copyWith(color: Colors.white), ), const SizedBox( width: 4, ), Assets.icon.outline.coin.svg( color: Colors.white, width: 18, height: 18) ], ), ), ], ), ), const SizedBox( height: 16, ), ], ), // Messages list ...messages.asMap().entries.map((entry) { final index = entry.key; final ms = entry.value; print('🔵 Building message index: $index'); final yourScrollController = ScrollController(); Messages? user; Messages? ai; if (ms.length == 2) { user = ms.first; ai = ms.last; } else if (ms.length == 1) { if (ms.single.fromBot ?? false) { ai = ms.single; } else { user = ms.single; } } return inComp ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: _comp, builder: (context, val, child) => BeforeAfter( trackWidth: 4, thumbWidth: 24, value: val, onValueChanged: (value) => _comp.value = value, before: Container( width: mw * 0.6, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors.black[900] : AppColors .primaryColor[50], borderRadius: BorderRadius.circular( 16)), child: AspectRatio( aspectRatio: 3 / 4, child: user!.file != null ? ClipRRect( borderRadius: BorderRadius .circular( 12), child: CustomeImage( src: user .file!.path, fit: BoxFit.cover, )) : ImageNetwork( width: double.infinity, height: double.infinity, radius: 12, showHero: true, url: user .content ?.first .imageUrl ?.url ?? ''), ), ), after: Container( width: mw * 0.6, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors.black[900] : AppColors .primaryColor[50], borderRadius: BorderRadius.circular( 16)), child: AspectRatio( aspectRatio: 3 / 4, child: ImageNetwork( width: double.infinity, height: double.infinity, radius: 12, showHero: true, url: ai! .content ?.first .imageUrl ?.url ?? '')), ), )), SizedBox( height: 16, ), CircleIconBtn( size: 32, color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, onTap: () { setState(() { inComp = !inComp; }); }, icon: Assets.icon.outline.bitcoinRefresh, ), ], ) : Column( children: [ const SizedBox( height: 16, ), if (user != null) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Container( width: mw * 0.6, padding: const EdgeInsets.all(4), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: user.error ?? false ? AppColors .red.defaultShade : Theme.of(context) .colorScheme .primary, borderRadius: BorderRadius.circular(16) .copyWith( bottomRight: Radius.zero)), child: ptp ? AspectRatio( aspectRatio: 3 / 4, child: user.file != null ? ClipRRect( borderRadius: BorderRadius .circular( 12), child: CustomeImage( src: user .file!.path, fit: BoxFit .cover, )) : ImageNetwork( width: double .infinity, height: double .infinity, radius: 12, showHero: true, url: user .content ?.first .imageUrl ?.url ?? ''), ) : Padding( padding: const EdgeInsets.all( 16.0), child: Directionality( textDirection: TextDirection.rtl, child: Scrollbar( thumbVisibility: true, trackVisibility: true, interactive: true, controller: yourScrollController, radius: const Radius .circular(16), child: SingleChildScrollView( controller: yourScrollController, physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets .only( left: 8.0), child: SelectableText( user.query ?? '', style: AppTextStyles .body4 .copyWith( color: Colors .white, ), // overflow: TextOverflow.ellipsis, // textAlign: TextAlign.justify, ), ), ), ), ), ), ), ], ), if (ai != null && user != null && state is! MediaGResponseLoading && ptp) CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, onTap: () { setState(() { inComp = !inComp; }); }, icon: Assets .icon.outline.bitcoinRefresh, ), if (user?.error ?? false) error( context, () { context.read().add( DeleteMessageWithId( messageId: user!.id!)); context .read() .request(SendMessageModel( id: chatId, query: ptp ? null : _query.text, file: ptp ? user.file : null, botId: widget.chatArgs.bot.id, ghost: isGhost.value, messageId: DateTime.now() .toIso8601String(), )); }, ), if (ai != null) Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( width: MediaQuery.sizeOf(context) .width * (ptp ? 0.6 : 0.7), padding: const EdgeInsets.all(4), margin: const EdgeInsets.all(16) .copyWith(bottom: 8), decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors.black[900] : AppColors .secondryColor[ 50], borderRadius: BorderRadius .circular(16) .copyWith( bottomLeft: Radius.zero)), child: AspectRatio( aspectRatio: 3 / 4, child: ImageNetwork( width: double.infinity, height: double.infinity, radius: 12, showHero: true, url: ai .content ?.first .imageUrl ?.url ?? ''), ), ), if (ptp) Expanded( child: Padding( padding: const EdgeInsets.only( bottom: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment .start, crossAxisAlignment: CrossAxisAlignment .end, children: [ CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets.icon .outline.download, onTap: () { try { DownloadFileService.getFile( url: ai! .content! .first .imageUrl! .url!) .then( (value) { SnackBarManager( context) .show( message: 'فایل با موفقیت در پوشه Downloads نشست.', status: SnackBarStatus.success); }); } catch (e) { if (kDebugMode) { print(e); } } }, ), const SizedBox( width: 8, ), CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets.icon .outline.share, onTap: () async { try { await Share.share(ai! .content! .first .imageUrl! .url .toString()); } catch (e) { if (kDebugMode) { print( 'Error in share Text: $e'); } } }, ), ], ), )), ], ), if (!ptp) Padding( padding: EdgeInsets.only( right: MediaQuery.sizeOf( context) .width * 0.26), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets.icon.outline .download, onTap: () { try { DownloadFileService.getFile( url: ai! .content! .first .imageUrl! .url!) .then((value) { SnackBarManager(context).show( message: 'فایل با موفقیت در پوشه Downloads نشست.', status: SnackBarStatus .success); }); } catch (e) { if (kDebugMode) { print(e); } } }, ), const SizedBox( width: 8, ), CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets .icon.outline.share, onTap: () async { try { await Share.share(ai! .content! .first .imageUrl! .url .toString()); } catch (e) { if (kDebugMode) { print( 'Error in share Text: $e'); } } }, ), ], ), ) ], ), const SizedBox( height: 90, ) ], ); }).toList(), const SizedBox(height: 100), ], ); }, ); // } }, )), ], ), ), ), ); } Container loading(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: context.read().isDark() ? AppColors.black[900] : AppColors.secondryColor[50]), child: Row( children: [ Flexible( child: SpinKitThreeBounce( size: 32, color: Theme.of(context).colorScheme.secondary, )), Text( 'این کار ممکن است کمی طول بکشد', style: AppTextStyles.body5 .copyWith(color: Theme.of(context).colorScheme.onSurface), ) ], ), ); } Widget error(BuildContext context, Function()? onRetry) { return Column( mainAxisSize: MainAxisSize.min, children: [ Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: AppColors.red.defaultShade), child: Center( child: Text( 'خطا لطفا مجددا تلاش کنید', style: AppTextStyles.body5 .copyWith(color: Colors.white, fontWeight: FontWeight.bold), ), ), ), const SizedBox( height: 8, ), // LoadingButton( // color: AppColors.red.defaultShade, // onPressed: () { // context.go(Routes.purchase); // }, // child: Text( // 'افزایش اعتبار', // style: AppTextStyles.body4.copyWith(color: Colors.white), // ), // ) ], ); } }