11/06/1403 -- Rhmn -- fix web and change ai message bar and dall e bot. appVersion: 3.3.4

This commit is contained in:
OkaykOrhmn 2024-10-02 15:01:07 +03:30
parent dfac461eee
commit 0512e22727
15 changed files with 1435 additions and 324 deletions

View File

@ -4,6 +4,7 @@ import 'package:didvan/models/ai/chats_model.dart';
class AiChatArgs {
final BotsModel bot;
final ChatsModel? chat;
final Prompts? prompts;
AiChatArgs({required this.bot, this.chat});
AiChatArgs({required this.bot, this.chat, this.prompts});
}

View File

@ -32,6 +32,7 @@ class FilesModel {
: name != null
? p.extension(name)
: '';
main = File(path);
}
@ -42,7 +43,9 @@ class FilesModel {
}
bool isImage() {
return image ?? lookupMimeType(path)?.startsWith('image/') ?? false;
return image ??
lookupMimeType(path)?.startsWith('image/') ??
false || path.contains(".png");
}
bool isNetwork() {

View File

@ -1,4 +1,3 @@
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:flutter/foundation.dart';
@ -66,7 +65,6 @@ class VoiceService {
static Future<void> voiceHelper(
{required String src,
final Uint8List? bytes,
final Duration? duration,
final Function()? startTimer,
final Function()? stopTimer}) async {
@ -96,7 +94,7 @@ class VoiceService {
// AudioSource.uri(Uri.parse(blobUrl)),
// );
// } else {
await audioPlayer.setUrl(source);
await audioPlayer.setUrl(source.replaceAll('%3A', ''));
// }
} else if (src.startsWith('blob:') || src == '') {
await audioPlayer.setUrl(src);

View File

@ -364,12 +364,14 @@ class ActionSheetUtils {
padding: const EdgeInsets.symmetric(
vertical: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.colorScheme
.border,
width: 1))),
border: index == state.bots.length - 1
? null
: Border(
bottom: BorderSide(
color: Theme.of(context)
.colorScheme
.border,
width: 1))),
child: Row(
children: [
ClipOval(

View File

@ -164,80 +164,96 @@ class _AiState extends State<Ai> {
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 32),
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
child: InkWell(
onTap: () =>
Navigator.of(context).pushNamed(Routes.aiChat,
arguments: AiChatArgs(
bot: bot,
)),
child: Row(
child: Column(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
boxShadow: DesignConfig.defaultShadow,
color: Theme.of(context).colorScheme.surface,
border: Border.all(
Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
boxShadow: DesignConfig.defaultShadow,
color:
Theme.of(context).colorScheme.border),
borderRadius: DesignConfig.highBorderRadius),
child: Row(
children: [
const SizedBox(
width: 8,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Form(
child: Row(
children: [
const MessageBarBtn(
enable: true,
icon:
DidvanIcons.mic_regular),
const SizedBox(
width: 8,
Theme.of(context).colorScheme.surface,
border: Border.all(
color: Theme.of(context)
.colorScheme
.border),
borderRadius:
DesignConfig.highBorderRadius),
child: Row(
children: [
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 8.0,
),
Expanded(
child: TextFormField(
textInputAction:
TextInputAction.newline,
style: Theme.of(context)
.textTheme
.bodyMedium,
minLines: 1,
enabled: false,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'بنویسید...',
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
color: Theme.of(
context)
.colorScheme
.disabledText),
child: Form(
child: Row(
children: [
const MessageBarBtn(
enable: true,
icon: DidvanIcons
.mic_regular),
const SizedBox(
width: 8,
),
),
),
const SizedBox(
width: 8,
),
const MessageBarBtn(
enable: false,
icon: Icons
.attach_file_rounded),
],
))))
],
Expanded(
child: TextFormField(
textInputAction:
TextInputAction
.newline,
style: Theme.of(context)
.textTheme
.bodyMedium,
minLines: 1,
enabled: false,
decoration:
InputDecoration(
border:
InputBorder.none,
hintText: 'بنویسید...',
hintStyle: Theme.of(
context)
.textTheme
.bodySmall!
.copyWith(
color: Theme.of(
context)
.colorScheme
.disabledText),
),
),
),
const SizedBox(
width: 8,
),
const MessageBarBtn(
enable: false,
icon: Icons
.attach_file_rounded),
],
))))
],
),
),
),
),
],
),
const Padding(
padding: EdgeInsets.fromLTRB(8, 8, 8, 4),
child: DidvanText(
'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.',
fontSize: 12,
),
)
],
)),
),

View File

@ -1,21 +1,27 @@
// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/main.dart';
import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/ai/chats_model.dart';
import 'package:didvan/models/ai/files_model.dart';
import 'package:didvan/models/ai/messages_model.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/ai/ai_chat_state.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/ai/widgets/ai_message_bar.dart';
import 'package:didvan/views/ai/widgets/ai_message_bar_ios.dart';
import 'package:didvan/views/ai/widgets/audio_wave.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
@ -62,6 +68,17 @@ class _AiChatPageState extends State<AiChatPage> {
() => focusNode.requestFocus(),
);
}
if (widget.args.prompts != null) {
state.messages.add(MessageModel(
dateTime: DateTime.now()
.subtract(const Duration(minutes: 210))
.toIso8601String(),
prompts: [widget.args.prompts!]));
state.message.clear();
state.update();
await state.postMessage(widget.args.bot);
}
});
super.initState();
}
@ -296,10 +313,13 @@ class _AiChatPageState extends State<AiChatPage> {
bottomSheet: Column(
mainAxisSize: MainAxisSize.min,
children: [
AiMessageBar(
bot: widget.args.bot,
// focusNode: focusNode,
),
Platform.isIOS
? AiMessageBarIOS(
bot: widget.args.bot,
)
: AiMessageBar(
bot: widget.args.bot,
),
],
)),
),
@ -429,8 +449,7 @@ class _AiChatPageState extends State<AiChatPage> {
)
: Column(
children: [
if (message.role.toString().contains('user') &&
file != null)
if (file != null)
(file.isAudio()
// && (!kIsWeb && !Platform.isIOS)
)
@ -472,6 +491,78 @@ class _AiChatPageState extends State<AiChatPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (message.role.toString().contains('user'))
PopupMenuButton(
offset: const Offset(0, 46),
onSelected: (value) async {
navigatorKey.currentState!.pushNamed(
Routes.aiChat,
arguments: AiChatArgs(
bot: value,
prompts: message));
},
itemBuilder: (BuildContext context) {
final historyAiChatState = context
.read<HistoryAiChatState>();
return <PopupMenuEntry>[
...List.generate(
historyAiChatState.bots.length,
(index) => PopupMenuItem(
value: historyAiChatState
.bots[index],
height: 72,
child: Row(
children: [
ClipOval(
child: CachedNetworkImage(
imageUrl:
historyAiChatState
.bots[index]
.image
.toString(),
width: 42,
height: 42,
),
),
const SizedBox(width: 12),
DidvanText(
'${historyAiChatState.bots[index].name}X',
),
],
),
),
)
];
},
child: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.symmetric(
horizontal: 8),
constraints: const BoxConstraints(
maxWidth: 120),
decoration: BoxDecoration(
borderRadius:
DesignConfig.lowBorderRadius,
border: Border.all(
color: Theme.of(context)
.colorScheme
.title)),
child: Row(
children: [
Expanded(
child: DidvanText(
'${widget.args.bot.name}',
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
),
const Icon(
DidvanIcons.angle_down_light),
],
),
)),
if (message.role
.toString()
.contains('user') &&

View File

@ -177,9 +177,13 @@ class AiChatState extends CoreProvier {
final r = res.listen((value) async {
var str = utf8.decode(value);
if (!kIsWeb) {
if (bot.id == 12) {
responseMessgae += str.split('{{{').first;
}
if (str.contains('{{{')) {
dataMessgae += str;
dataMessgae += "{{{${str.split('{{{').last}";
update();
return;
}
@ -198,6 +202,10 @@ class AiChatState extends CoreProvier {
} catch (e) {
e.printError();
}
if (bot.id == 12) {
responseMessgae = "${responseMessgae.split('.png').first}.png";
return;
}
}
messageOnstream.value = Stream.value(responseMessgae);
@ -233,8 +241,11 @@ class AiChatState extends CoreProvier {
e.printError();
return;
}
messages.last.prompts.last = messages.last.prompts.last
.copyWith(finished: true, text: responseMessgae, id: aiMessageId);
messages.last.prompts.last = messages.last.prompts.last.copyWith(
finished: true,
text: bot.id == 12 ? null : responseMessgae,
file: bot.id == 12 ? responseMessgae : null,
id: aiMessageId);
if (messages.last.prompts.length > 2) {
messages.last.prompts[messages.last.prompts.length - 2] = messages
.last.prompts[messages.last.prompts.length - 2]

View File

@ -1,7 +1,7 @@
// ignore_for_file: library_private_types_in_public_api, avoid_web_libraries_in_flutter
import 'dart:async';
// import 'dart:html' as html;
import 'package:universal_html/html.dart' as html;
import 'dart:io';
import 'package:audio_session/audio_session.dart';
import 'package:didvan/config/design_config.dart';
@ -60,6 +60,7 @@ class _AiMessageBarState extends State<AiMessageBar> {
Timer? _timer;
final theSource = AudioSource.microphone;
final ValueNotifier<Duration> _countTimer = ValueNotifier(Duration.zero);
late HistoryAiChatState historyState = context.read<HistoryAiChatState>();
@override
void initState() {
@ -174,9 +175,6 @@ class _AiMessageBarState extends State<AiMessageBar> {
_mPlayer!
.startPlayer(
fromURI: _mPath,
// fromDataBuffer: Uint8List.fromList(
// recordedData.expand((element) => element).toList()),
//codec: kIsWeb ? Codec.opusWebM : Codec.aacADTS,
whenFinished: () {
setState(() {});
})
@ -226,26 +224,18 @@ class _AiMessageBarState extends State<AiMessageBar> {
@override
Widget build(BuildContext context) {
return Consumer<AiChatState>(builder: (context, state, child) {
final historyState = context.read<HistoryAiChatState>();
return Container(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 12)
.copyWith(
top: openAttach ||
(state.file != null && !state.file!.isRecorded)
? 0
: 24),
padding: const EdgeInsets.fromLTRB(12, 24, 12, 0).copyWith(
top: (state.file != null && !state.file!.isRecorded) ? 0 : 24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
top: openAttach
? BorderSide(color: Theme.of(context).colorScheme.border)
: BorderSide.none)),
child: Stack(
color: Theme.of(context).colorScheme.background,
),
child: Column(
children: [
Column(
if (state.file != null && !(state.file!.isRecorded))
fileContainer(),
Stack(
children: [
attachmentLayout(state, historyState, context),
Container(
decoration: BoxDecoration(
boxShadow: DesignConfig.defaultShadow,
@ -283,24 +273,35 @@ class _AiMessageBarState extends State<AiMessageBar> {
],
),
),
if (state.onResponsing)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.focused
.withOpacity(0.5)),
child: Center(
child: SpinKitThreeBounce(
color: Theme.of(context).colorScheme.primary,
size: 32,
),
),
),
)
],
),
if (state.onResponsing)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.focused
.withOpacity(0.5)),
child: Center(
child: SpinKitThreeBounce(
color: Theme.of(context).colorScheme.primary,
size: 32,
MediaQuery.of(context).viewInsets.bottom == 0
? const Padding(
padding: EdgeInsets.fromLTRB(8, 8, 8, 4),
child: DidvanText(
'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.',
fontSize: 12,
),
),
),
)
)
: const SizedBox(
height: 12,
)
],
),
);
@ -411,26 +412,28 @@ class _AiMessageBarState extends State<AiMessageBar> {
},
)),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
child: MessageBarBtn(
enable: false,
icon: openAttach || state.file != null
? DidvanIcons.close_regular
: Icons.attach_file_outlined,
click: () {
if (_mPlayer!.isPlaying) {
stopPlayer();
}
if (state.file != null) {
state.file = null;
} else {
openAttach = !openAttach;
}
state.update();
},
),
)
attachmentLayout(state, context),
if (widget.bot.attachmentType!.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
child: MessageBarBtn(
enable: false,
icon: openAttach || state.file != null
? DidvanIcons.close_regular
: Icons.attach_file_outlined,
click: () {
if (_mPlayer!.isPlaying) {
stopPlayer();
}
if (state.file != null) {
state.file = null;
} else {
openAttach = !openAttach;
}
state.update();
},
),
)
],
);
}
@ -510,186 +513,328 @@ class _AiMessageBarState extends State<AiMessageBar> {
);
}
AnimatedVisibility attachmentLayout(AiChatState state,
HistoryAiChatState historyState, BuildContext context) {
// AnimatedVisibility attachmentLayout(AiChatState state,
// HistoryAiChatState historyState, BuildContext context) {
// return AnimatedVisibility(
// isVisible: openAttach,
// duration: DesignConfig.lowAnimationDuration,
// child: Container(
// padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 12),
// child: state.file != null && !(state.file!.isRecorded)
// ? fileContainer()
// : Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// if (historyState.bot!.attachmentType!.contains('pdf'))
// attachBtn(
// title: "PDF",
// icon: CupertinoIcons.doc_fill,
// color: Colors.redAccent,
// click: () async {
// MediaService.onLoadingPickFile(context);
// FilePickerResult? result =
// await MediaService.pickPdfFile();
// if (result != null) {
// if (kIsWeb) {
// Uint8List? bytes = result.files.first
// .bytes; // Access the bytes property
// String? name = result.files.first.name;
// // Store bytes and file name directly in your state or model
// state.file = FilesModel(
// '', // No need for a file path on web
// name: name,
// bytes: bytes,
// audio: false,
// image: false,
// );
// } else {
// state.file = FilesModel(result.files.single.path!,
// audio: false, image: false);
// }
// }
// Future.delayed(
// Duration.zero,
// () => ActionSheetUtils(context).pop(),
// );
// },
// ),
// if (historyState.bot!.attachmentType!.contains('image'))
// attachBtn(
// title: "تصویر",
// icon: CupertinoIcons.photo,
// color: Colors.deepOrangeAccent,
// click: () async {
// MediaService.onLoadingPickFile(context);
// final pickedFile = await MediaService.pickImage(
// source: ImageSource.gallery);
// File? file;
// if (pickedFile != null && !kIsWeb) {
// file = await ImageCropper().cropImage(
// sourcePath: pickedFile.path,
// androidUiSettings: const AndroidUiSettings(
// toolbarTitle: 'برش تصویر'),
// iosUiSettings: const IOSUiSettings(
// title: 'برش تصویر',
// doneButtonTitle: 'تایید',
// cancelButtonTitle: 'بازگشت',
// ),
// compressQuality: 30,
// );
// if (file == null) {
// await Future.delayed(
// Duration.zero,
// () => ActionSheetUtils(context).pop(),
// );
// return;
// }
// }
// if (pickedFile == null) {
// await Future.delayed(
// Duration.zero,
// () => ActionSheetUtils(context).pop(),
// );
// return;
// }
// state.file = kIsWeb
// ? FilesModel(pickedFile.path,
// name: pickedFile.name,
// image: true,
// audio: false)
// : FilesModel(file!.path,
// image: true, audio: false);
// await Future.delayed(
// Duration.zero,
// () => ActionSheetUtils(context).pop(),
// );
// },
// ),
// // if (!kIsWeb && !Platform.isIOS)
// if (historyState.bot!.attachmentType!.contains('audio'))
// attachBtn(
// title: "صوت",
// icon: CupertinoIcons.music_note_2,
// color: Colors.indigoAccent,
// click: () async {
// MediaService.onLoadingPickFile(context);
// FilePickerResult? result =
// await MediaService.pickAudioFile();
// if (result != null) {
// if (kIsWeb) {
// Uint8List? bytes = result.files.first
// .bytes; // Access the bytes property
// String? name = result.files.first.name;
// // final blob = html.Blob([bytes]);
// // final blobUrl =
// // html.Url.createObjectUrlFromBlob(blob);
// state.file = FilesModel(
// "", // No need for a file path on web
// name: name,
// bytes: bytes,
// audio: true,
// image: false,
// );
// } else {
// state.file = FilesModel(result.files.single.path!,
// audio: true, image: false);
// }
// }
// await Future.delayed(
// Duration.zero,
// () => ActionSheetUtils(context).pop(),
// );
// },
// )
// ],
// ),
// ));
// }
AnimatedVisibility attachmentLayout(AiChatState state, BuildContext context) {
return AnimatedVisibility(
isVisible: openAttach,
fadeMode: FadeMode.horizontal,
duration: DesignConfig.lowAnimationDuration,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 12),
child: state.file != null && !(state.file!.isRecorded)
? fileContainer()
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (historyState.bot!.attachmentType!.contains('pdf'))
attachBtn(
title: "PDF",
icon: CupertinoIcons.doc_fill,
color: Colors.redAccent,
click: () async {
MediaService.onLoadingPickFile(context);
FilePickerResult? result =
await MediaService.pickPdfFile();
if (result != null) {
if (kIsWeb) {
Uint8List? bytes = result.files.first
.bytes; // Access the bytes property
String? name = result.files.first.name;
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (historyState.bot!.attachmentType!.contains('pdf'))
MessageBarBtn(
enable: true,
icon: CupertinoIcons.doc_fill,
click: () async {
MediaService.onLoadingPickFile(context);
FilePickerResult? result = await MediaService.pickPdfFile();
if (result != null) {
if (kIsWeb) {
Uint8List? bytes =
result.files.first.bytes; // Access the bytes property
String? name = result.files.first.name;
// Store bytes and file name directly in your state or model
state.file = FilesModel(
'', // No need for a file path on web
name: name,
bytes: bytes,
audio: false,
image: false,
);
} else {
state.file = FilesModel(result.files.single.path!,
audio: false, image: false);
}
}
Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
},
),
if (historyState.bot!.attachmentType!.contains('image'))
attachBtn(
title: "تصویر",
icon: CupertinoIcons.photo,
color: Colors.deepOrangeAccent,
click: () async {
MediaService.onLoadingPickFile(context);
// Store bytes and file name directly in your state or model
state.file = FilesModel(
'', // No need for a file path on web
name: name,
bytes: bytes,
audio: false,
image: false,
);
} else {
state.file = FilesModel(result.files.single.path!,
audio: false, image: false);
}
}
Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
openAttach = !openAttach;
final pickedFile = await MediaService.pickImage(
source: ImageSource.gallery);
File? file;
if (pickedFile != null && !kIsWeb) {
file = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
androidUiSettings: const AndroidUiSettings(
toolbarTitle: 'برش تصویر'),
iosUiSettings: const IOSUiSettings(
title: 'برش تصویر',
doneButtonTitle: 'تایید',
cancelButtonTitle: 'بازگشت',
),
compressQuality: 30,
);
state.update();
},
),
if (historyState.bot!.attachmentType!.contains('image'))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: MessageBarBtn(
enable: true,
icon: CupertinoIcons.photo,
click: () async {
MediaService.onLoadingPickFile(context);
if (file == null) {
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
final pickedFile = await MediaService.pickImage(
source: ImageSource.gallery);
File? file;
if (pickedFile != null && !kIsWeb) {
file = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
androidUiSettings:
const AndroidUiSettings(toolbarTitle: 'برش تصویر'),
iosUiSettings: const IOSUiSettings(
title: 'برش تصویر',
doneButtonTitle: 'تایید',
cancelButtonTitle: 'بازگشت',
),
compressQuality: 30,
);
return;
}
}
if (pickedFile == null) {
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
if (file == null) {
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
return;
}
state.file = kIsWeb
? FilesModel(pickedFile.path,
name: pickedFile.name,
image: true,
audio: false)
: FilesModel(file!.path,
image: true, audio: false);
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
},
),
// if (!kIsWeb && !Platform.isIOS)
if (historyState.bot!.attachmentType!.contains('audio'))
attachBtn(
title: "صوت",
icon: CupertinoIcons.music_note_2,
color: Colors.indigoAccent,
click: () async {
MediaService.onLoadingPickFile(context);
return;
}
}
if (pickedFile == null) {
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
FilePickerResult? result =
await MediaService.pickAudioFile();
if (result != null) {
if (kIsWeb) {
Uint8List? bytes = result.files.first
.bytes; // Access the bytes property
String? name = result.files.first.name;
return;
}
state.file = kIsWeb
? FilesModel(pickedFile.path,
name: pickedFile.name, image: true, audio: false)
: FilesModel(file!.path, image: true, audio: false);
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
openAttach = !openAttach;
// final blob = html.Blob([bytes]);
// final blobUrl =
// html.Url.createObjectUrlFromBlob(blob);
state.file = FilesModel(
"", // No need for a file path on web
name: name,
bytes: bytes,
audio: true,
image: false,
);
} else {
state.file = FilesModel(result.files.single.path!,
audio: true, image: false);
}
}
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
},
)
],
state.update();
},
),
),
// if (!kIsWeb && !Platform.isIOS)
if (historyState.bot!.attachmentType!.contains('audio'))
MessageBarBtn(
enable: true,
icon: CupertinoIcons.music_note_2,
click: () async {
MediaService.onLoadingPickFile(context);
FilePickerResult? result = await MediaService.pickAudioFile();
if (result != null) {
if (kIsWeb) {
Uint8List? bytes =
result.files.first.bytes; // Access the bytes property
String? name = result.files.first.name;
final blob = html.Blob([bytes]);
final blobUrl = html.Url.createObjectUrlFromBlob(blob);
state.file = FilesModel(
blobUrl, // No need for a file path on web
name: name,
bytes: bytes,
audio: true,
image: false,
);
} else {
state.file = FilesModel(result.files.single.path!,
audio: true, image: false);
}
}
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
openAttach = !openAttach;
state.update();
},
)
],
));
}
InkWell attachBtn(
{required final String title,
required final IconData icon,
final Color? color,
final Function()? click}) {
final state = context.read<AiChatState>();
return InkWell(
onTap: () async {
await click?.call();
// InkWell attachBtn(
// {required final String title,
// required final IconData icon,
// final Color? color,
// final Function()? click}) {
// final state = context.read<AiChatState>();
// return InkWell(
// onTap: () async {
// await click?.call();
state.update();
},
child: Column(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
padding: const EdgeInsets.all(0.8),
margin: const EdgeInsets.symmetric(horizontal: 12),
child: Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.white,
)),
const SizedBox(
height: 4,
),
DidvanText(
title,
fontSize: 12,
)
],
),
);
}
// state.update();
// },
// child: Column(
// children: [
// Container(
// width: 40,
// height: 40,
// decoration: BoxDecoration(shape: BoxShape.circle, color: color),
// padding: const EdgeInsets.all(0.8),
// margin: const EdgeInsets.symmetric(horizontal: 12),
// child: Icon(
// icon,
// size: 20,
// color: Theme.of(context).colorScheme.white,
// )),
// const SizedBox(
// height: 4,
// ),
// DidvanText(
// title,
// fontSize: 12,
// )
// ],
// ),
// );
// }
Widget audioContainer() {
final state = context.watch<AiChatState>();
@ -711,6 +856,7 @@ class _AiMessageBarState extends State<AiMessageBar> {
borderRadius: DesignConfig.mediumBorderRadius,
color: Theme.of(context).colorScheme.border,
),
margin: const EdgeInsets.fromLTRB(4, 4, 4, 8),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
child: Row(
children: [

View File

@ -0,0 +1,791 @@
import 'dart:async';
import 'dart:io';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/models/ai/chats_model.dart';
import 'package:didvan/models/ai/files_model.dart';
import 'package:didvan/models/ai/messages_model.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/media/voice.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/ai/ai_chat_state.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/ai/widgets/audio_wave.dart';
import 'package:didvan/views/ai/widgets/message_bar_btn.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/marquee_text.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:record/record.dart';
import 'package:path/path.dart' as p;
class AiMessageBarIOS extends StatefulWidget {
final BotsModel bot;
const AiMessageBarIOS({
super.key,
required this.bot,
});
@override
State<AiMessageBarIOS> createState() => _AiMessageBarIOSState();
}
class _AiMessageBarIOSState extends State<AiMessageBarIOS> {
final ValueNotifier<String> messageText = ValueNotifier('');
bool openAttach = false;
String? path;
late HistoryAiChatState historyState = context.read<HistoryAiChatState>();
@override
void initState() {
super.initState();
}
final record = AudioRecorder();
@override
void dispose() {
super.dispose();
record.dispose();
try {
_timer.cancel();
} catch (e) {
e.printError();
}
}
late Timer _timer;
final ValueNotifier<Duration> _countTimer = ValueNotifier(Duration.zero);
void startTimer() {
const oneSec = Duration(seconds: 1);
_timer = Timer.periodic(
oneSec,
(Timer timer) {
_countTimer.value = Duration(seconds: _countTimer.value.inSeconds + 1);
},
);
}
@override
Widget build(BuildContext context) {
return Consumer<AiChatState>(
builder: (context, state, child) {
return Container(
padding: const EdgeInsets.fromLTRB(12, 24, 12, 0).copyWith(
top: (state.file != null && !state.file!.isRecorded) ? 0 : 24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
),
child: Column(
children: [
if (state.file != null && !(state.file!.isRecorded))
fileContainer(),
Stack(
children: [
Container(
// height: 50,
decoration: BoxDecoration(
boxShadow: DesignConfig.defaultShadow,
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: Theme.of(context).colorScheme.border),
borderRadius: DesignConfig.highBorderRadius),
child: Row(
children: [
const SizedBox(
width: 8,
),
Expanded(
child: StreamBuilder<RecordState>(
stream: record.onStateChanged(),
builder: (context, snapshot) {
return ValueListenableBuilder(
valueListenable: messageText,
builder: (context, value, child) {
return Row(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(
bottom: 8.0),
child: (
// !kIsWeb &&
snapshot.hasData &&
snapshot.data! !=
RecordState.stop)
? MessageBarBtn(
enable: true,
icon: DidvanIcons
.stop_circle_solid,
click: () async {
path =
await record.stop();
Duration? duration =
await VoiceService
.getDuration(
src: path ??
'');
state.file = FilesModel(
path.toString(),
name:
'${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a',
isRecorded: true,
audio: true,
image: false,
duration: duration);
_timer.cancel();
state.update();
},
)
:
// (!kIsWeb &&
// !Platform
// .isIOS) &&
widget.bot.attachmentType!
.contains(
'audio') &&
value.isEmpty &&
state.file == null &&
widget.bot.attachment !=
0
? MessageBarBtn(
enable: true,
icon: DidvanIcons
.mic_regular,
click: () async {
if (await record
.hasPermission()) {
try {
String path =
'${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a';
//
if (!kIsWeb) {
Directory?
downloadDir =
await getApplicationDocumentsDirectory();
path = p.join(
downloadDir
.path,
'${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a');
}
setState(
() {
openAttach =
false;
},
);
record.start(
const RecordConfig(),
path: path);
startTimer();
} catch (e) {
if (kDebugMode) {
print(
'Error starting recording: $e');
}
}
}
},
)
: MessageBarBtn(
enable: (state
.file !=
null &&
state.file!
.isRecorded) ||
(widget.bot
.attachment ==
1) ||
value.isNotEmpty,
icon: DidvanIcons
.send_light,
click: () async {
if ((state.file ==
null ||
!state.file!
.isRecorded) &&
(widget.bot
.attachment !=
1) &&
value.isEmpty) {
return;
}
if (state.messages
.isNotEmpty &&
DateTime.parse(state
.messages
.last
.dateTime)
.toPersianDateStr()
.contains(DateTime.parse(DateTime
.now()
.subtract(
const Duration(minutes: 210))
.toIso8601String())
.toPersianDateStr())) {
state.messages.last
.prompts
.add(Prompts(
error: false,
text: state
.message.text,
file: state
.file?.path,
fileName: state
.file
?.basename,
fileLocal:
state.file,
finished: true,
role: 'user',
createdAt: DateTime
.now()
.subtract(
const Duration(
minutes:
210))
.toIso8601String(),
));
} else {
state.messages.add(MessageModel(
dateTime: DateTime
.now()
.subtract(const Duration(
minutes:
210))
.toIso8601String(),
prompts: [
Prompts(
error:
false,
text: state
.message
.text,
finished:
true,
file: state
.file
?.path,
fileName: state
.file
?.basename,
fileLocal:
state
.file,
role:
'user',
createdAt: DateTime
.now()
.subtract(const Duration(
minutes:
210))
.toIso8601String(),
)
]));
}
state.message.clear();
messageText.value =
state
.message.text;
await state
.postMessage(
widget.bot);
},
),
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: snapshot.hasData &&
(snapshot.data! ==
RecordState
.record ||
snapshot.data! ==
RecordState.pause)
? Padding(
padding:
const EdgeInsets.all(
8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Directionality(
textDirection:
TextDirection
.ltr,
child: Row(
mainAxisAlignment:
MainAxisAlignment
.center,
children: List.generate(
4,
(index) => snapshot.data! == RecordState.pause
? Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: List.generate(
8,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 12),
child: Container(
width: 3,
height: 8,
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primary.withOpacity(0.4)),
),
)),
)
: SpinKitWave(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.4),
size:
32,
itemCount:
10,
))),
),
ValueListenableBuilder<
Duration>(
valueListenable:
_countTimer,
builder: (context,
value,
child) =>
DidvanText(DateTimeUtils
.normalizeTimeDuration(
value)),
),
],
),
)
: state.file != null &&
state.file!.isRecorded
? audioContainer()
: Form(
child: TextFormField(
textInputAction:
TextInputAction
.newline,
style:
Theme.of(context)
.textTheme
.bodyMedium,
minLines: 1,
maxLines:
6, // Set this
// expands: true, //
// keyboardType: TextInputType.text,
keyboardType:
TextInputType
.multiline,
controller:
state.message,
enabled: !(state
.file !=
null &&
widget.bot
.attachment ==
1),
decoration:
InputDecoration(
border: InputBorder
.none,
hintText:
'بنویسید...',
hintStyle: Theme.of(
context)
.textTheme
.bodySmall!
.copyWith(
color: Theme.of(
context)
.colorScheme
.disabledText),
suffixIcon: state
.isEdite
? InkWell(
onTap: () {
state.isEdite =
false;
state
.update();
},
child: const Icon(
DidvanIcons
.close_circle_solid),
)
: const SizedBox(),
),
onChanged: (value) {
messageText.value =
value;
state.update();
},
)),
),
),
if (snapshot.hasData)
Padding(
padding: const EdgeInsets.only(
bottom: 8.0),
child: snapshot.data! ==
RecordState.record
? MessageBarBtn(
enable: false,
icon: DidvanIcons
.pause_solid,
click: () async {
await record.pause();
_timer.cancel();
},
)
: snapshot.data! ==
RecordState.pause
? MessageBarBtn(
enable: false,
icon: DidvanIcons
.play_solid,
click: () async {
await record
.resume();
startTimer();
},
)
: const SizedBox(),
),
const SizedBox(
width: 8,
),
Padding(
padding: const EdgeInsets.only(
bottom: 8.0),
child: attachmentLayout(
state, context),
),
if (!snapshot.hasData ||
(snapshot.hasData &&
snapshot.data ==
RecordState.stop))
Padding(
padding:
const EdgeInsets.fromLTRB(
12, 0, 12, 8),
child: state.file != null ||
openAttach
? MessageBarBtn(
click: () {
if (openAttach) {
setState(() {
openAttach =
!openAttach;
});
return;
}
state.file = null;
_countTimer.value =
Duration.zero;
state.update();
},
enable: false,
icon: DidvanIcons
.close_solid)
: widget.bot.attachmentType!
.isNotEmpty
? MessageBarBtn(
click: () {
setState(() {
openAttach =
!openAttach;
});
},
enable: false,
icon: Icons
.attach_file_rounded,
)
: const SizedBox(),
),
],
);
});
}),
),
],
),
),
if (state.onResponsing)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.focused
.withOpacity(0.5)),
child: Center(
child: SpinKitThreeBounce(
color: Theme.of(context).colorScheme.primary,
size: 32,
),
),
),
)
],
),
MediaQuery.of(context).viewInsets.bottom == 0
? const Padding(
padding: EdgeInsets.fromLTRB(8, 8, 8, 4),
child: DidvanText(
'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.',
fontSize: 12,
),
)
: const SizedBox(
height: 12,
),
],
),
);
},
);
}
Widget audioContainer() {
final state = context.watch<AiChatState>();
return SizedBox(
width: MediaQuery.sizeOf(context).width,
child: AudioWave(
file: state.file!.path,
loadingPaddingSize: 8.0,
totalDuration: _countTimer.value,
),
);
}
AnimatedVisibility attachmentLayout(AiChatState state, BuildContext context) {
return AnimatedVisibility(
isVisible: openAttach,
fadeMode: FadeMode.horizontal,
duration: DesignConfig.lowAnimationDuration,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (historyState.bot!.attachmentType!.contains('pdf'))
MessageBarBtn(
enable: true,
icon: CupertinoIcons.doc_fill,
click: () async {
MediaService.onLoadingPickFile(context);
FilePickerResult? result = await MediaService.pickPdfFile();
if (result != null) {
if (kIsWeb) {
Uint8List? bytes =
result.files.first.bytes; // Access the bytes property
String? name = result.files.first.name;
// Store bytes and file name directly in your state or model
state.file = FilesModel(
'', // No need for a file path on web
name: name,
bytes: bytes,
audio: false,
image: false,
);
} else {
state.file = FilesModel(result.files.single.path!,
audio: false, image: false);
}
}
Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
openAttach = !openAttach;
state.update();
},
),
if (historyState.bot!.attachmentType!.contains('image'))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: MessageBarBtn(
enable: true,
icon: CupertinoIcons.photo,
click: () async {
MediaService.onLoadingPickFile(context);
final pickedFile = await MediaService.pickImage(
source: ImageSource.gallery);
File? file;
if (pickedFile != null && !kIsWeb) {
file = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
androidUiSettings:
const AndroidUiSettings(toolbarTitle: 'برش تصویر'),
iosUiSettings: const IOSUiSettings(
title: 'برش تصویر',
doneButtonTitle: 'تایید',
cancelButtonTitle: 'بازگشت',
),
compressQuality: 30,
);
if (file == null) {
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
return;
}
}
if (pickedFile == null) {
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
return;
}
state.file = kIsWeb
? FilesModel(pickedFile.path,
name: pickedFile.name, image: true, audio: false)
: FilesModel(file!.path, image: true, audio: false);
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
openAttach = !openAttach;
state.update();
},
),
),
// if (!kIsWeb && !Platform.isIOS)
if (historyState.bot!.attachmentType!.contains('audio'))
MessageBarBtn(
enable: true,
icon: CupertinoIcons.music_note_2,
click: () async {
MediaService.onLoadingPickFile(context);
FilePickerResult? result = await MediaService.pickAudioFile();
if (result != null) {
if (kIsWeb) {
Uint8List? bytes =
result.files.first.bytes; // Access the bytes property
String? name = result.files.first.name;
state.file = FilesModel(
'', // No need for a file path on web
name: name,
bytes: bytes,
audio: true,
image: false,
);
} else {
state.file = FilesModel(result.files.single.path!,
audio: true, image: false);
}
}
await Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).pop(),
);
openAttach = !openAttach;
state.update();
},
)
],
));
}
Widget fileContainer() {
final state = context.watch<AiChatState>();
return Container(
decoration: BoxDecoration(
borderRadius: DesignConfig.mediumBorderRadius,
color: Theme.of(context).colorScheme.border,
),
margin: const EdgeInsets.fromLTRB(4, 4, 4, 8),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
child: Row(
children: [
state.file != null && state.file!.isImage()
? SizedBox(
width: 32,
height: 42,
child: ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: state.file!.isNetwork()
? Image.network(
state.file!.path,
fit: BoxFit.cover,
)
: Image.file(
state.file!.main,
fit: BoxFit.cover,
)))
: const Icon(Icons.file_copy),
const SizedBox(
width: 12,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 24,
child: MarqueeText(
text: state.file != null ? state.file!.basename : '',
style: const TextStyle(fontSize: 14),
stop: const Duration(seconds: 3),
),
),
if (state.file != null && !kIsWeb)
FutureBuilder(
future: state.file!.main.length(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
return DidvanText(
'File Size ${(snapshot.data! / 1000).round()} KB',
fontSize: 12,
);
})
],
),
)
],
),
);
}
}

View File

@ -15,16 +15,14 @@ import 'package:just_audio/just_audio.dart';
class AudioWave extends StatefulWidget {
final String file;
final Uint8List? bytes;
final double loadingPaddingSize;
final Duration? totalDuration;
const AudioWave(
{Key? key,
required this.file,
this.loadingPaddingSize = 0,
this.totalDuration,
this.bytes})
: super(key: key);
const AudioWave({
Key? key,
required this.file,
this.loadingPaddingSize = 0,
this.totalDuration,
}) : super(key: key);
@override
_AudioWaveState createState() => _AudioWaveState();
@ -110,8 +108,7 @@ class _AudioWaveState extends State<AudioWave> {
? DidvanIcons.pause_solid
: DidvanIcons.play_solid,
click: () async {
await VoiceService.voiceHelper(
src: widget.file, bytes: widget.bytes);
await VoiceService.voiceHelper(src: widget.file);
},
);
}),

View File

@ -281,17 +281,15 @@ class DirectState extends CoreProvier {
}
} else {
final Uint8List uploadFile = kIsWeb
? (await http.get(Uri.parse(path!))).bodyBytes
? (await http.get(Uri.parse(path!.replaceAll('%3A', ':')))).bodyBytes
: await File(path!).readAsBytes();
path = null;
await service.multipartBytes(
file: uploadFile,
method: 'POST',
fieldName: 'audio',
fileName: 'voice-message',
mediaExtension: 'm4a',
mediaExtension: 'mp3',
mediaFormat: 'audio',
);
@ -304,8 +302,8 @@ class DirectState extends CoreProvier {
for (var i = 0; i < messages.length; i++) {
_addToDailyGrouped(messages[i]);
}
// update();
}
path = null;
}
notifyListeners();
}

View File

@ -83,9 +83,9 @@ class Message extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.text != null) DidvanText(message.text!),
if (message.audio != null || message.audioFile != null)
if (message.audio != null)
AudioWave(
file: message.audio ?? message.audioFile!.path,
file: message.audio!,
totalDuration: message.duration != null
? Duration(seconds: message.duration!)
: null,

View File

@ -32,8 +32,9 @@ class SkeletonImage extends StatelessWidget {
child: ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero,
child: CachedNetworkImage(
errorWidget: (context, url, error) =>
const Text("مشکلی پیش آمده است"),
errorWidget: (context, url, error) {
return const Text("مشکلی پیش آمده است");
},
errorListener: (value) {},
fit: BoxFit.cover,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
@ -41,8 +42,8 @@ class SkeletonImage extends StatelessWidget {
width: width,
height: height,
imageUrl: imageUrl.startsWith('http')
? imageUrl
: RequestHelper.baseUrl + imageUrl,
? imageUrl.replaceAll('\n', '')
: RequestHelper.baseUrl + imageUrl.replaceAll('\n', ''),
placeholder: (context, _) => ShimmerPlaceholder(
width: pWidth,
height: pHeight,

View File

@ -1037,6 +1037,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
record:
dependency: "direct main"
description:
name: record
sha256: "4a5cf4d083d1ee49e0878823c4397d073f8eb0a775f31215d388e2bc47a9e867"
url: "https://pub.dev"
source: hosted
version: "5.1.2"
record_android:
dependency: transitive
description:
name: record_android
sha256: d7af0b3119725a0f561817c72b5f5eca4d7a76d441deef519ae04e4824c0734c
url: "https://pub.dev"
source: hosted
version: "1.2.6"
record_darwin:
dependency: transitive
description:
name: record_darwin
sha256: fe90d302acb1f3cee1ade5df9c150ca5cee33b48d8cdf1cf433bf577d7f00134
url: "https://pub.dev"
source: hosted
version: "1.1.2"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "11f8b03ea8a0e279b0e306571dbe0db0202c0b8e866495c9fa1ad2281d5e4c15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "0ef370d1e6553ad33c39dd03103b374e7861f3518b0533e64c94d73f988a5ffa"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: e653555aa3fda168aded7c34e11bd82baf0c6ac84e7624553def3c77ffefd36f
url: "https://pub.dev"
source: hosted
version: "1.0.3"
rive:
dependency: "direct main"
description:

View File

@ -51,7 +51,7 @@ dependencies:
carousel_slider: ^4.0.0
flutter_vibrate: ^1.3.0
universal_html: ^2.0.8
# record: ^5.1.2
record: ^5.1.2
persian_datetime_picker: ^2.6.0
persian_number_utility: ^1.1.1