Houshan-Basa/lib/ui/screens/gmedia/chats/audio_chat_page.dart

973 lines
50 KiB
Dart

// 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<AudioChatPage> createState() => _AudioChatPageState();
}
class _AudioChatPageState extends State<AudioChatPage> {
final FocusNode _textFieldFocus = FocusNode();
final CarouselSliderController _carouselController =
CarouselSliderController();
final ValueNotifier<int> _currentIndex = ValueNotifier(3);
final TextEditingController _query = TextEditingController();
final ValueNotifier<bool> isGhost = ValueNotifier(false);
final ValueNotifier<int> maxSize = ValueNotifier(1);
late int? chatId = widget.chatId;
late Bots bot = widget.bot;
List<List<Messages>> groupMessages(List<Messages> messages) {
return messages.fold<List<List<Messages>>>([], (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<MessagesBloc>(
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<MediaGResponseCubit>().state
is MediaGResponseLoading) {
return;
}
FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.audio,
);
if (result != null) {
if (mounted) {
context
.read<MediaGResponseCubit>()
.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<MediaGResponseCubit>().state
is MediaGResponseLoading) {
return;
}
context
.read<MediaGResponseCubit>()
.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<ThemeModeCubit>()
.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<ThemeModeCubit>().isDark() ? 0.4 : 0.8),
fit: BoxFit.cover,
),
Positioned.fill(child: BlocBuilder<MessagesBloc, MessagesState>(
builder: (context, mState) {
if (mState is MessagesFail) {
return const SizedBox();
}
if (mState is MessagesLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
final m = mState.messages;
List<List<Messages>> messages = groupMessages(m);
return BlocConsumer<MediaGResponseCubit, MediaGResponseState>(
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<MessagesBloc>().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<MessagesBloc>().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<ChatsHistoryBloc>().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<MessagesBloc>().add(
DeleteMessageWithId(
messageId: user!.id!));
context
.read<MediaGResponseCubit>()
.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<WidgetState>
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<ThemeModeCubit>().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),
)
],
),
);
}
}