// ignore_for_file: use_build_context_synchronously import 'dart:math'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/bots_model.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/ui/screens/chat/bloc/messages_bloc.dart'; import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_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/audio/music_player.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/snackbar/snackbar_manager.dart'; import 'package:share_plus/share_plus.dart'; class AudioChatPage extends StatefulWidget { final String type; final Bots bot; final int? chatId; final double maxWidth; const AudioChatPage( {super.key, required this.type, required this.bot, this.chatId, required this.maxWidth}); @override State createState() => _AudioChatPageState(); } class _AudioChatPageState extends State { final FocusNode _textFieldFocus = FocusNode(); final CarouselSliderController _carouselController = CarouselSliderController(); final ValueNotifier _currentIndex = ValueNotifier(3); final TextEditingController _query = TextEditingController(); final ValueNotifier isGhost = ValueNotifier(false); final ValueNotifier maxSize = ValueNotifier(1); late int? chatId = widget.chatId; late Bots bot = widget.bot; 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 Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (context) { final messagesBloc = MessagesBloc()..add(ResetMessages()); if (chatId != null) { messagesBloc.add(GetallMessages(chatId: chatId!)); } return messagesBloc; }, ), ], child: Scaffold( 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(); }, ); }), bottomSheet: Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ widget.type == 'file' ? Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: LoadingButton( width: double.infinity, onPressed: () async { if (context.read().state is MediaGResponseLoading) { return; } FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.audio, ); if (result != null) { if (mounted) { context .read() .request(SendMessageModel( id: chatId, ghost: isGhost.value, messageId: DateTime.now().toIso8601String(), file: result.xFiles.single, botId: bot.id, )); } } }, backgroundColor: Theme.of(context).colorScheme.primary, child: Text( 'بارگذاری فایل صوتی', style: AppTextStyles.body4 .copyWith(color: Colors.white), )), ), ) : Expanded( child: 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(), id: chatId, ghost: isGhost.value, query: _query.text, botId: bot.id, )); _query.clear(); }, ), const SizedBox( width: 8, ), Expanded( child: TextField( controller: _query, focusNode: _textFieldFocus, onTapOutside: (event) { _textFieldFocus.unfocus(); }, minLines: widget.type == 'music' ? 4 : 1, maxLines: 4, keyboardType: TextInputType.multiline, style: AppTextStyles.body4.copyWith( color: Theme.of(context) .colorScheme .onSurface), decoration: InputDecoration.collapsed( hintText: 'متن ${widget.type == 'music' ? 'ترانه ' : ''}را وارد کنید...', hintStyle: AppTextStyles.body4.copyWith( color: AppColors.gray[context .read() .isDark() ? 600 : 900])), ), ), ], ), ), ), ), ], ), ), body: Stack( children: [ Assets.image.audioBack.image( width: widget.maxWidth, height: MediaQuery.sizeOf(context).height, // color: Theme.of(context).scaffoldBackgroundColor, // colorBlendMode: BlendMode.multiply, opacity: AlwaysStoppedAnimation( context.read().isDark() ? 0.4 : 0.8), fit: BoxFit.cover, ), Positioned.fill(child: BlocBuilder( builder: (context, mState) { if (mState is MessagesFail) { return const SizedBox(); } if (mState is MessagesLoading) { return const Center( child: CircularProgressIndicator(), ); } final m = mState.messages; List> messages = groupMessages(m); return BlocConsumer( listener: (context, state) async { if (state is MediaGResponseLoading) { await Future.delayed(const Duration(milliseconds: 600)); _carouselController.animateToPage(maxSize.value); } if (state is MediaGResponseFail) { SnackBarManager(context).show( message: 'خطا از طرف سرور لطفا لحظاتی دیگر دوباره تلاش کنید', status: SnackBarStatus.error); } 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( audioUrl: 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 + (state is MediaGResponseLoading ? 1 : 0); return CarouselSlider.builder( carouselController: _carouselController, itemCount: messages.length + 1 + (state is MediaGResponseLoading ? 1 : 0), options: CarouselOptions( initialPage: 3, viewportFraction: 1, enlargeFactor: 0.1, height: MediaQuery.sizeOf(context).height, autoPlay: false, scrollDirection: Axis.vertical, enableInfiniteScroll: false, onPageChanged: (index, reason) { _currentIndex.value = index; }), itemBuilder: (context, index, realIndex) { if (state is MediaGResponseLoading && index == messages.length + 1) { final yourScrollController = ScrollController(); return Column( children: [ Flexible( flex: 1, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ widget.type == 'file' ? Container( width: widget.maxWidth * 0.8, padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors.black[900] : Colors.white, borderRadius: BorderRadius .circular(16) .copyWith( bottomLeft: Radius.zero)), child: MusicPlayer( url: state.file!.path)) : Container( width: widget.maxWidth * 0.8, constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf( context) .height * 0.3), padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .primary, borderRadius: BorderRadius .circular(16) .copyWith( bottomRight: Radius.zero)), child: Directionality( textDirection: TextDirection.rtl, child: textLay( yourScrollController, state.query ?? ''), ), ), ], )), Flexible( child: Column( children: [ loading(context), ], )) ], ); } else if (index != 0) { final ms = messages[index - 1]; 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; } } final yourScrollController = ScrollController(); return Column( children: [ const SizedBox( height: 16, ), if (user != null) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ widget.type == 'file' ? Container( width: widget.maxWidth * 0.8, padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors.black[900] : Colors.white, borderRadius: BorderRadius.circular(16) .copyWith( bottomLeft: Radius.zero)), child: MusicPlayer( url: user.file!.path)) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: widget.maxWidth * 0.8, constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf( context) .height * 0.3), padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16) .copyWith(bottom: 8), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .primary, borderRadius: BorderRadius .circular(16) .copyWith( bottomRight: Radius.zero)), child: Directionality( textDirection: TextDirection.rtl, child: textLay( yourScrollController, user.query ?? ''), ), ), Padding( padding: const EdgeInsets.only( left: 18.0, bottom: 16), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets .icon.outline.copy, onTap: () async { try { await Clipboard.setData( ClipboardData( text: user! .query!)); Future.delayed( Duration.zero, () => SnackBarManager( context, id: 'Copy') .show( message: 'متن کپی شد 😃')); } catch (e) { if (kDebugMode) { print(e); } } }, ), ], ), ), ], ), ], ), if (user?.error ?? false) error( context, () { context.read().add( DeleteMessageWithId( messageId: user!.id!)); context .read() .request(SendMessageModel( id: chatId, query: widget.type != 'file' ? null : _query.text, file: widget.type == 'file' ? user.file : null, botId: bot.id, ghost: isGhost.value, messageId: DateTime.now() .toIso8601String(), )); }, ), if (ai != null) Row( mainAxisAlignment: MainAxisAlignment.start, children: [ widget.type == 'file' ? Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( width: widget.maxWidth * 0.8, constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf( context) .height * 0.3), padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16) .copyWith(bottom: 8), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .primary, borderRadius: BorderRadius .circular(16) .copyWith( bottomRight: Radius.zero)), child: Directionality( textDirection: TextDirection.rtl, child: textLay( yourScrollController, ai .content ?.first .audioUrl ?.url ?? ''), ), ), Padding( padding: const EdgeInsets.only( right: 18.0, bottom: 16), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ CircleIconBtn( color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets .icon.outline.copy, onTap: () async { try { await Clipboard.setData( ClipboardData( text: ai! .content! .first .audioUrl! .url!)); } catch (e) { if (kDebugMode) { print(e); } } }, ), ], ), ) ], ) : Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( width: widget.maxWidth * 0.8, padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16) .copyWith(bottom: 8), decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors.black[900] : Colors.white, borderRadius: BorderRadius .circular(16) .copyWith( bottomLeft: Radius.zero)), child: MusicPlayer( url: ai.content?.first .audioUrl?.url ?? ''), ), Padding( padding: const EdgeInsets.only( right: 18.0, bottom: 16), 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 .audioUrl! .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 .audioUrl! .url .toString()); } catch (e) { if (kDebugMode) { print( 'Error in share Text: $e'); } } }, ), ], ), ) ], ), ], ), ], ); } return Stack( children: [ Column( children: [ const SizedBox( height: 16, ), if (bot.description != null) Container( margin: const EdgeInsets.symmetric( horizontal: 16), padding: const EdgeInsets.all(8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), 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< Icon?>( (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: 32, ), ], ), // Positioned( // left: 16, // bottom: widget.type == 'music' ? 150 : 74, // child: CircleIconBtn( // onTap: () { // DialogHandler(context: context) // .onMusicCreate(); // }, // size: 32, // icon: Assets.icon.outline.idea)) ], ); }); }, ); }, )) ], ), ), ); } Scrollbar textLay(ScrollController yourScrollController, String text) { return 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( text, style: AppTextStyles.body4.copyWith( color: Colors.white, ), // overflow: TextOverflow.ellipsis, // textAlign: TextAlign.justify, ), ), ), ); } 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), // ), // ) ], ); } 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: [ Expanded( child: SpinKitThreeBounce( size: 32, color: Theme.of(context).colorScheme.secondary, )), Text( 'این کار ممکن است کمی طول بکشد', style: AppTextStyles.body5 .copyWith(color: Theme.of(context).colorScheme.onSurface), ) ], ), ); } }