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

1037 lines
58 KiB
Dart

// 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<PhotoChatPage> createState() => _PhotoChatPageState();
}
class _PhotoChatPageState extends State<PhotoChatPage> {
final CarouselSliderController _carouselController =
CarouselSliderController();
final TextEditingController _query = TextEditingController();
final ValueNotifier<int> _currentIndex = ValueNotifier(0);
final ValueNotifier<double> _comp = ValueNotifier(0.5);
final ValueNotifier<double> _compBot = ValueNotifier(0.5);
final FocusNode _textFieldFocus = FocusNode();
bool inComp = false;
late final ptp =
widget.chatArgs.bot.attachmentType?.contains('image') ?? false;
final ValueNotifier<bool> isGhost = ValueNotifier(false);
late final bot = widget.chatArgs.bot;
late int? chatId = widget.chatArgs.chatId;
final ValueNotifier<int> maxSize = ValueNotifier(1);
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
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ChatsHistoryBloc>().add(RestartChatsHistory());
context.read<ChatsHistoryBloc>().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<MediaGResponseCubit>()
.state is MediaGResponseLoading) {
return;
}
context.read<MediaGResponseCubit>().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<MediaGResponseCubit>()
.state is MediaGResponseLoading) {
return;
}
context.read<MediaGResponseCubit>().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<ThemeModeCubit>()
.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<ThemeModeCubit>().isDark() ? 0.5 : 0.2)),
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(),
);
}
// if (state is MessagesSuccess) {
final m = mState.messages;
List<List<Messages>> messages = groupMessages(m);
return BlocConsumer<MediaGResponseCubit, MediaGResponseState>(
listener: (context, state) async {
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(
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<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;
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<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: 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<MessagesBloc>().add(
DeleteMessageWithId(
messageId: user!.id!));
context
.read<MediaGResponseCubit>()
.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<ThemeModeCubit>().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),
// ),
// )
],
);
}
}