diff --git a/lib/assets/images/features/ai-dark.svg b/lib/assets/images/features/ai-dark.svg
new file mode 100644
index 0000000..9147090
--- /dev/null
+++ b/lib/assets/images/features/ai-dark.svg
@@ -0,0 +1,8 @@
+
diff --git a/lib/assets/images/features/ai-light.svg b/lib/assets/images/features/ai-light.svg
new file mode 100644
index 0000000..e39406d
--- /dev/null
+++ b/lib/assets/images/features/ai-light.svg
@@ -0,0 +1,8 @@
+
diff --git a/lib/constants/assets.dart b/lib/constants/assets.dart
index 1142878..91e4793 100644
--- a/lib/constants/assets.dart
+++ b/lib/constants/assets.dart
@@ -78,7 +78,7 @@ class Assets {
static String get podcast => '$_baseFeaturesPath/podcast-$_themeSuffix.svg';
static String get risk => '$_baseFeaturesPath/risk-$_themeSuffix.svg';
static String get saha => '$_baseFeaturesPath/saha-$_themeSuffix.svg';
- static String get ai => '$_baseFeaturesPath/ai_solid.svg';
+ static String get ai => '$_baseFeaturesPath/ai-$_themeSuffix.svg';
static String get startup => '$_baseFeaturesPath/startup-$_themeSuffix.svg';
static String get stats => '$_baseFeaturesPath/stats-$_themeSuffix.svg';
static String get tech => '$_baseFeaturesPath/tech-$_themeSuffix.svg';
diff --git a/lib/models/ai/bots_model.dart b/lib/models/ai/bots_model.dart
index 58026d5..fa81daf 100644
--- a/lib/models/ai/bots_model.dart
+++ b/lib/models/ai/bots_model.dart
@@ -5,6 +5,7 @@ class BotsModel {
String? description;
List? attachmentType;
int? attachment;
+ bool? editable;
BotsModel({this.id, this.name, this.image});
@@ -20,6 +21,7 @@ class BotsModel {
});
}
attachment = json['attachment'];
+ editable = json['editable'];
}
Map toJson() {
@@ -32,6 +34,7 @@ class BotsModel {
data['attachmentType'] = attachmentType!.map((v) => v).toList();
}
data['attachment'] = attachment;
+ data['editable'] = editable;
return data;
}
}
diff --git a/lib/models/ai/chats_model.dart b/lib/models/ai/chats_model.dart
index 7c07d41..fb19b48 100644
--- a/lib/models/ai/chats_model.dart
+++ b/lib/models/ai/chats_model.dart
@@ -96,6 +96,7 @@ class Prompts {
String? createdAt;
bool? finished;
bool? error;
+ bool? audio;
Prompts({
this.id,
@@ -107,6 +108,7 @@ class Prompts {
this.createdAt,
this.finished,
this.error,
+ this.audio,
});
Prompts.fromJson(Map json) {
@@ -117,6 +119,7 @@ class Prompts {
fileName = json['fileName'];
role = json['role'];
createdAt = json['createdAt'];
+ audio = json['audio'];
}
Map toJson() {
@@ -128,6 +131,7 @@ class Prompts {
data['fileName'] = fileName;
data['role'] = role;
data['createdAt'] = createdAt;
+ data['audio'] = audio;
return data;
}
@@ -140,7 +144,8 @@ class Prompts {
String? role,
String? createdAt,
bool? finished,
- bool? error}) {
+ bool? error,
+ bool? audio}) {
return Prompts(
id: id ?? this.id,
chatId: chatId ?? this.chatId,
@@ -151,6 +156,7 @@ class Prompts {
createdAt: createdAt ?? this.createdAt,
finished: finished ?? this.finished,
error: error ?? this.error,
+ audio: audio ?? this.audio,
);
}
}
diff --git a/lib/models/ai/files_model.dart b/lib/models/ai/files_model.dart
index d5c54ea..61b0751 100644
--- a/lib/models/ai/files_model.dart
+++ b/lib/models/ai/files_model.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
class FilesModel {
@@ -7,10 +8,15 @@ class FilesModel {
late String basename;
late String extname;
late File main;
+ final bool isRecorded;
+ late bool isAudio;
+ late bool isImage;
- FilesModel(this.path) {
+ FilesModel(this.path, {this.isRecorded = false}) {
basename = p.basename(path);
extname = p.extension(path);
main = File(path);
+ isAudio = lookupMimeType(path)?.startsWith('audio/') ?? false;
+ isImage = lookupMimeType(path)?.startsWith('image/') ?? false;
}
}
diff --git a/lib/models/news_details_data.dart b/lib/models/news_details_data.dart
index b2b7b44..bc11421 100644
--- a/lib/models/news_details_data.dart
+++ b/lib/models/news_details_data.dart
@@ -40,7 +40,7 @@ class NewsDetailsData {
image: json['image'],
createdAt: json['createdAt'],
marked: json['marked'],
- liked: json['liked'],
+ liked: json['liked'] ?? false,
comments: json['comments'],
order: json['order'],
tags: List.from(json['tags'].map((tag) => Tag.fromJson(tag))),
diff --git a/lib/models/notification_message.dart b/lib/models/notification_message.dart
index 6a0f722..aafcbf7 100644
--- a/lib/models/notification_message.dart
+++ b/lib/models/notification_message.dart
@@ -53,15 +53,15 @@ class NotificationMessage {
Map toPayload() {
final Map data = {};
- data['notificationType'] = notificationType!;
- data['title'] = title!;
- data['body'] = body!;
- data['id'] = id!;
- data['type'] = type!;
- data['link'] = link!;
- data['image'] = image!;
- data['photo'] = photo!;
- data['userId'] = userId!;
+ data['notificationType'] = notificationType ?? '';
+ data['title'] = title ?? '';
+ data['body'] = body ?? '';
+ data['id'] = id ?? '';
+ data['type'] = type ?? '';
+ data['link'] = link ?? '';
+ data['image'] = image ?? '';
+ data['photo'] = photo ?? '';
+ data['userId'] = userId ?? '';
return data;
}
}
diff --git a/lib/models/view/app_bar_data.dart b/lib/models/view/app_bar_data.dart
index cda1066..2b3ab0a 100644
--- a/lib/models/view/app_bar_data.dart
+++ b/lib/models/view/app_bar_data.dart
@@ -12,7 +12,7 @@ class AppBarData {
AppBarData(
{this.title,
this.subtitle,
- this.hasBack = false,
+ this.hasBack = true,
this.trailing,
this.isSmall = false,
this.hasElevation = true,
diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart
index b23d322..6ceb0d9 100644
--- a/lib/routes/route_generator.dart
+++ b/lib/routes/route_generator.dart
@@ -56,6 +56,7 @@ import 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart'
if (dart.library.html) 'package:didvan/views/podcasts/studio_details/studio_details.web.dart';
import 'package:didvan/views/splash/splash.dart';
import 'package:didvan/routes/routes.dart';
+import 'package:didvan/views/web/web_view.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:home_widget/home_widget.dart';
@@ -310,6 +311,11 @@ class RouteGenerator {
return _createRoute(HistoryAiChatPage(
archived: settings.arguments as bool?,
));
+
+ case Routes.web:
+ return _createRoute(WebView(
+ src: settings.arguments as String,
+ ));
default:
return _errorRoute(settings.name ?? '');
}
diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart
index 57e58ba..b64043f 100644
--- a/lib/routes/routes.dart
+++ b/lib/routes/routes.dart
@@ -34,4 +34,5 @@ class Routes {
static const String notificationTime = '/notification-time';
static const String widgetSetting = '/widget-setting';
static const String newStatic = '/new-static';
+ static const String web = '/web';
}
diff --git a/lib/services/ai/ai_api_service.dart b/lib/services/ai/ai_api_service.dart
index 6f3f284..eff2925 100644
--- a/lib/services/ai/ai_api_service.dart
+++ b/lib/services/ai/ai_api_service.dart
@@ -32,7 +32,7 @@ class AiApiService {
request.fields['chatId'] = chatId.toString();
}
if (edite != null) {
- request.fields['edite'] = edite.toString().toLowerCase();
+ request.fields['edit'] = edite.toString().toLowerCase();
}
if (file != null) {
final length = await file.length();
@@ -50,6 +50,10 @@ class AiApiService {
mimeType)), // Use MediaType.parse to parse the MIME type
);
}
+
+ // print("req: ${request.files}");
+ // print("req: ${request.fields}");
+
return request;
}
diff --git a/lib/services/app_home_widget/home_widget_repository.dart b/lib/services/app_home_widget/home_widget_repository.dart
index 0ed6a77..00d64a2 100644
--- a/lib/services/app_home_widget/home_widget_repository.dart
+++ b/lib/services/app_home_widget/home_widget_repository.dart
@@ -1,8 +1,10 @@
import 'package:didvan/models/notification_message.dart';
+import 'package:didvan/services/app_initalizer.dart';
+import 'package:didvan/services/storage/storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:home_widget/home_widget.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+// import 'package:url_launcher/url_launcher_string.dart';
import '../../main.dart';
import '../../models/requests/infography.dart';
@@ -15,6 +17,7 @@ import '../network/request_helper.dart';
class HomeWidgetRepository {
static Future fetchWidget() async {
+ RequestService.token = await StorageService.getValue(key: 'token');
final service = RequestService(
RequestHelper.widgetNews(),
);
diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart
index dc04a4a..240f88f 100644
--- a/lib/services/app_initalizer.dart
+++ b/lib/services/app_initalizer.dart
@@ -14,10 +14,15 @@ import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
+enum LaunchMode { inAppWebView }
+
+void launchUrlString(String src, {dynamic mode}) {
+ navigatorKey.currentState!.pushNamed(Routes.web, arguments: src);
+}
+
class AppInitializer {
static String? fcmToken;
static String? clickAction;
-
static Future setupServices(BuildContext context) async {
if (!kIsWeb) {
StorageService.appDocsDir =
diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart
index c7d2d08..2f60b1b 100644
--- a/lib/services/media/media.dart
+++ b/lib/services/media/media.dart
@@ -62,7 +62,7 @@ class MediaService {
},
);
} else {
- audioPlayer.setAsset(
+ audioPlayer.setFilePath(
audioSource,
tag: isVoiceMessage
? null
diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart
index cd0f7a0..1412f5b 100644
--- a/lib/utils/action_sheet.dart
+++ b/lib/utils/action_sheet.dart
@@ -1,4 +1,6 @@
import 'dart:async';
+import 'dart:io';
+import 'dart:math';
import 'dart:ui';
import 'package:bot_toast/bot_toast.dart';
@@ -12,6 +14,7 @@ import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
+import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -385,6 +388,49 @@ class ActionSheetUtils {
)));
}
+ static void openInteractiveViewer(
+ BuildContext context, String image, bool isFile) {
+ showDialog(
+ context: context,
+ barrierDismissible: true,
+ builder: (context) => Dialog(
+ backgroundColor: Colors.transparent,
+ insetPadding: EdgeInsets.zero,
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: InteractiveViewer(
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Center(
+ child: isFile
+ ? ClipRRect(
+ borderRadius: DesignConfig.lowBorderRadius,
+ child: Image.file(File(image)))
+ : SkeletonImage(
+ width: min(MediaQuery.of(context).size.width,
+ MediaQuery.of(context).size.height),
+ imageUrl: image,
+ ),
+ ),
+ ),
+ ),
+ ),
+ Positioned(
+ right: 24,
+ top: 24,
+ child: Container(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Theme.of(context).colorScheme.surface),
+ child: const BackButton()),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
static void pop() {
DesignConfig.updateSystemUiOverlayStyle();
Navigator.of(context).pop();
diff --git a/lib/utils/media.dart b/lib/utils/media.dart
new file mode 100644
index 0000000..434ea95
--- /dev/null
+++ b/lib/utils/media.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/material.dart';
+
+/// Get screen media.
+final MediaQueryData media =
+ // ignore: deprecated_member_use
+ MediaQueryData.fromView(WidgetsBinding.instance.window);
+
+/// This extention help us to make widget responsive.
+extension NumberParsing on num {
+ double w() => this * media.size.width / 100;
+
+ double h() => this * media.size.height / 100;
+}
+
+///
+extension StringExtension on String {
+ String? get appendZeroPrefix {
+ return length <= 1 ? "0$this" : this;
+ }
+}
+
+/// This extention help us to make widget responsive.
+extension DurationExtension on Duration {
+ String get formattedTime {
+ int sec = inSeconds % 60;
+ int min = (inSeconds / 60).floor();
+ return "${min.toString().appendZeroPrefix}:${sec.toString().appendZeroPrefix}";
+ }
+}
diff --git a/lib/views/ai/ai.dart b/lib/views/ai/ai.dart
index 21dcf33..0a088f1 100644
--- a/lib/views/ai/ai.dart
+++ b/lib/views/ai/ai.dart
@@ -28,7 +28,10 @@ class _AiState extends State {
Future.delayed(
Duration.zero,
() {
- // state.getChats();
+ if (context.read().refresh) {
+ context.read().getChats();
+ context.read().refresh = false;
+ }
state.getBots();
},
);
diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart
index f49341b..e12d59d 100644
--- a/lib/views/ai/ai_chat_page.dart
+++ b/lib/views/ai/ai_chat_page.dart
@@ -11,23 +11,22 @@ import 'package:didvan/models/ai/files_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/services/storage/storage.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/voice_message_view.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';
+import 'package:didvan/views/widgets/marquee_text.dart';
+import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
-import 'package:mime/mime.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
-import 'package:voice_message_package/voice_message_package.dart';
class AiChatPage extends StatefulWidget {
final AiChatArgs args;
@@ -71,7 +70,10 @@ class _AiChatPageState extends State {
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
- context.read().getChats();
+ if (context.read().refresh) {
+ context.read().getChats();
+ context.read().refresh = false;
+ }
return true;
},
child: Consumer(
@@ -85,7 +87,10 @@ class _AiChatPageState extends State {
icon: DidvanIcons.angle_right_solid,
onPressed: () {
Navigator.of(context).pop();
- context.read().getChats();
+ if (context.read().refresh) {
+ context.read().getChats();
+ context.read().refresh = false;
+ }
},
),
],
@@ -99,6 +104,7 @@ class _AiChatPageState extends State {
final TextEditingController placeholder =
TextEditingController(
text: state.chat?.placeholder);
+ ActionSheetUtils.context = context;
ActionSheetUtils.openDialog(
data: ActionSheetData(
hasConfirmButtonClose: false,
@@ -168,6 +174,10 @@ class _AiChatPageState extends State {
fontFamily: DesignConfig
.fontFamily
.padRight(3)),
+ minLines: 4,
+ maxLines: 4,
+ keyboardType:
+ TextInputType.multiline,
decoration: InputDecoration(
filled: true,
fillColor: Theme.of(context)
@@ -249,7 +259,11 @@ class _AiChatPageState extends State {
itemCount: state.messages.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- padding: const EdgeInsets.only(bottom: 120),
+ padding: EdgeInsets.only(
+ bottom: state.file != null &&
+ !state.file!.isRecorded
+ ? 150
+ : 100),
itemBuilder: (context, mIndex) {
final prompts = state.messages[mIndex].prompts;
final time = state.messages[mIndex].dateTime;
@@ -263,6 +277,7 @@ class _AiChatPageState extends State {
const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final message = prompts[index];
+
return messageBubble(message, context,
state, index, mIndex);
},
@@ -274,13 +289,9 @@ class _AiChatPageState extends State {
bottomSheet: Column(
mainAxisSize: MainAxisSize.min,
children: [
- Padding(
- padding: const EdgeInsets.only(
- top: 8, bottom: 24.0, left: 12, right: 12),
- child: AiMessageBar(
- bot: widget.args.bot,
- focusNode: focusNode,
- ),
+ AiMessageBar(
+ bot: widget.args.bot,
+ focusNode: focusNode,
),
],
)),
@@ -308,20 +319,32 @@ class _AiChatPageState extends State {
);
}
- Padding messageBubble(Prompts message, BuildContext context,
- AiChatState state, int index, int mIndex) {
+ Widget messageBubble(Prompts message, BuildContext context, AiChatState state,
+ int index, int mIndex) {
FilesModel? file =
message.file == null ? null : FilesModel(message.file.toString());
MarkdownStyleSheet defaultMarkdownStyleSheet = MarkdownStyleSheet(
+ pPadding: const EdgeInsets.all(0.8),
+ h1Padding: const EdgeInsets.all(0.8),
+ h2Padding: const EdgeInsets.all(0.8),
+ h3Padding: const EdgeInsets.all(0.8),
+ h4Padding: const EdgeInsets.all(0.8),
+ h5Padding: const EdgeInsets.all(0.8),
+ h6Padding: const EdgeInsets.all(0.8),
+ tablePadding: const EdgeInsets.all(0.8),
+ blockquotePadding: const EdgeInsets.all(0.8),
+ listBulletPadding: const EdgeInsets.all(0.8),
+ tableCellsPadding: const EdgeInsets.all(0.8),
+ codeblockPadding: const EdgeInsets.all(8),
code: TextStyle(
backgroundColor: Theme.of(context).colorScheme.black,
color: Theme.of(context).colorScheme.white,
),
- codeblockPadding: const EdgeInsets.all(8),
codeblockDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.black));
+
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
child: Column(
@@ -336,8 +359,8 @@ class _AiChatPageState extends State {
: MainAxisAlignment.end,
children: [
Container(
- padding:
- const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.sizeOf(context).width / 1.5),
decoration: BoxDecoration(
borderRadius: DesignConfig.mediumBorderRadius.copyWith(
bottomLeft: !message.role.toString().contains('user')
@@ -359,8 +382,6 @@ class _AiChatPageState extends State {
),
),
child: Container(
- constraints: BoxConstraints(
- maxWidth: MediaQuery.sizeOf(context).width / 1.5),
child: message.finished != null && !message.finished!
? Column(
children: [
@@ -394,102 +415,29 @@ class _AiChatPageState extends State {
children: [
if (message.role.toString().contains('user') &&
file != null)
- lookupMimeType(file.path)?.startsWith('audio/') ??
- false
- ? Directionality(
- textDirection: TextDirection.ltr,
- child: FutureBuilder(
- future: StorageService.getValue(
- key: 'token'),
- builder: (context, snapshot) {
- return MyVoiceMessageView(
- size: 32,
- controller: VoiceController(
- audioSrc: file.path
- .startsWith('/uploads')
- ? 'https://api.didvan.app${file.path}?accessToken=${snapshot.data}'
- : file.path,
- onComplete: () {
- /// do something on complete
- },
- onPause: () {
- /// do something on pause
- },
- onPlaying: () {
- /// do something on playing
- },
- onError: (err) {
- /// do somethin on error
- },
- isFile: !file.path
- .startsWith('/uploads'),
- maxDuration:
- const Duration(seconds: 10),
- ),
- innerPadding: 0,
- cornerRadius: 20,
- circlesColor: Theme.of(context)
- .colorScheme
- .primary,
- activeSliderColor:
- Theme.of(context)
- .colorScheme
- .primary,
- );
- }),
+ file.isAudio
+ ? Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16, vertical: 8),
+ child: AudioWave(file: file.path),
)
- : Container(
- decoration: BoxDecoration(
- borderRadius:
- DesignConfig.mediumBorderRadius,
- color: Theme.of(context)
- .colorScheme
- .border,
- ),
- constraints:
- const BoxConstraints(minWidth: 200),
- padding: const EdgeInsets.fromLTRB(
- 12, 8, 12, 8),
- margin: const EdgeInsets.symmetric(
- horizontal: 12),
- child: Row(
- children: [
- const Icon(Icons.file_copy),
- const SizedBox(
- width: 12,
+ : file.isImage
+ ? Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: messageImage(file),
+ )
+ : Padding(
+ padding: const EdgeInsets.all(
+ 8.0,
),
- Expanded(
- child: Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- SizedBox(
- width: 200,
- child: DidvanText((message
- .fileName
- .toString())),
- ),
- if (state.file != null)
- 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,
- );
- })
- ],
- ),
- )
- ],
- ),
- ),
- if (message.text != null)
+ child: messageFile(
+ context, message, state),
+ ),
+ if (message.text != null &&
+ message.text!.isNotEmpty &&
+ ((message.audio == null ||
+ (message.audio != null &&
+ !message.audio!))))
Markdown(
data: message.text.toString(),
selectable: true,
@@ -497,24 +445,75 @@ class _AiChatPageState extends State {
physics: const NeverScrollableScrollPhysics(),
styleSheet: defaultMarkdownStyleSheet,
),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- if (message.role.toString().contains('user') &&
- index ==
- state.messages[mIndex].prompts.length -
- 2)
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ if (message.role
+ .toString()
+ .contains('user') &&
+ index ==
+ state.messages[mIndex].prompts
+ .length -
+ 2 &&
+ (widget.args.bot.editable != null &&
+ widget.args.bot.editable!))
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ onTap: () async {
+ state.isEdite = true;
+ state.message.text =
+ message.text.toString();
+ state.update();
+ },
+ child: Icon(
+ Icons.edit_outlined,
+ size: 18,
+ color: Theme.of(context)
+ .colorScheme
+ .focusedBorder,
+ ),
+ ),
+ ),
+ if (message.error != null && message.error!)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ onTap: () async {
+ state.messages.last.prompts
+ .remove(message);
+ state.messages.last.prompts.add(
+ message.copyWith(error: false));
+ state.update();
+ await state
+ .postMessage(widget.args.bot);
+ },
+ child: Icon(
+ DidvanIcons.refresh_solid,
+ size: 18,
+ color: Theme.of(context)
+ .colorScheme
+ .focusedBorder,
+ ),
+ ),
+ ),
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
- state.isEdite = true;
- state.message.text =
- message.text.toString();
- state.update();
+ await Clipboard.setData(ClipboardData(
+ text: state.messages[mIndex]
+ .prompts[index].text
+ .toString()));
+ ActionSheetUtils.showAlert(AlertData(
+ message: "متن با موفقیت کپی شد",
+ aLertType: ALertType.success));
},
child: Icon(
- Icons.edit_outlined,
+ DidvanIcons.copy_regular,
size: 18,
color: Theme.of(context)
.colorScheme
@@ -522,21 +521,20 @@ class _AiChatPageState extends State {
),
),
),
- if (message.error != null && message.error!)
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
- state.messages.last.prompts
- .remove(message);
- state.messages.last.prompts.add(
- message.copyWith(error: false));
- state.update();
- await state
- .postMessage(widget.args.bot);
+ if (message.id != null) {
+ state.deleteMessage(
+ message.id!, mIndex, index);
+ } else {
+ state.messages[mIndex].prompts
+ .removeAt(index);
+ }
},
child: Icon(
- DidvanIcons.refresh_solid,
+ DidvanIcons.trash_solid,
size: 18,
color: Theme.of(context)
.colorScheme
@@ -544,50 +542,10 @@ class _AiChatPageState extends State {
),
),
),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: InkWell(
- onTap: () async {
- await Clipboard.setData(ClipboardData(
- text: state.messages[mIndex]
- .prompts[index].text
- .toString()));
- ActionSheetUtils.showAlert(AlertData(
- message: "متن با موفقیت کپی شد",
- aLertType: ALertType.success));
- },
- child: Icon(
- DidvanIcons.copy_regular,
- size: 18,
- color: Theme.of(context)
- .colorScheme
- .focusedBorder,
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: InkWell(
- onTap: () async {
- if (message.id != null) {
- state.deleteMessage(
- message.id!, mIndex, index);
- } else {
- state.messages[mIndex].prompts
- .removeAt(index);
- }
- },
- child: Icon(
- DidvanIcons.trash_solid,
- size: 18,
- color: Theme.of(context)
- .colorScheme
- .focusedBorder,
- ),
- ),
- ),
- ],
- )
+ ],
+ ),
+ ),
+ const SizedBox(height: 8),
],
),
),
@@ -610,4 +568,67 @@ class _AiChatPageState extends State {
),
);
}
+
+ Container messageFile(
+ BuildContext context, Prompts message, AiChatState state) {
+ return Container(
+ decoration: BoxDecoration(
+ borderRadius: DesignConfig.mediumBorderRadius,
+ color: Theme.of(context).colorScheme.border,
+ ),
+ padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
+ margin: const EdgeInsets.fromLTRB(8, 8, 8, 0),
+ child: Row(
+ children: [
+ const Icon(Icons.file_copy),
+ const SizedBox(
+ width: 12,
+ ),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: MediaQuery.sizeOf(context).width,
+ child: MarqueeText(
+ text: message.fileName.toString(),
+ style: const TextStyle(fontSize: 14),
+ stop: const Duration(seconds: 3),
+ ),
+ ),
+ if (state.file != null)
+ 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,
+ );
+ })
+ ],
+ ),
+ )
+ ],
+ ),
+ );
+ }
+
+ Widget messageImage(FilesModel file) {
+ return GestureDetector(
+ onTap: () => ActionSheetUtils.openInteractiveViewer(
+ context, file.path, !file.path.startsWith('/uploads')),
+ child: file.path.startsWith('/uploads')
+ ? SkeletonImage(
+ pWidth: MediaQuery.sizeOf(context).width / 1,
+ pHeight: MediaQuery.sizeOf(context).height / 6,
+ imageUrl: file.path,
+ )
+ : ClipRRect(
+ borderRadius: DesignConfig.lowBorderRadius,
+ child: Image.file(file.main)),
+ );
+ }
}
diff --git a/lib/views/ai/ai_chat_state.dart b/lib/views/ai/ai_chat_state.dart
index 5d7a4a1..5716cc2 100644
--- a/lib/views/ai/ai_chat_state.dart
+++ b/lib/views/ai/ai_chat_state.dart
@@ -44,7 +44,8 @@ class AiChatState extends CoreProvier {
Future _onError(e) async {
onResponsing = false;
messages.last.prompts.removeLast();
- messages.last.prompts.removeLast();
+ messages.last.prompts.last =
+ messages.last.prompts.last.copyWith(error: true);
messageOnstream.value = const Stream.empty();
await ActionSheetUtils.showAlert(AlertData(
diff --git a/lib/views/ai/history_ai_chat_page.dart b/lib/views/ai/history_ai_chat_page.dart
index 0dfb015..17651ac 100644
--- a/lib/views/ai/history_ai_chat_page.dart
+++ b/lib/views/ai/history_ai_chat_page.dart
@@ -51,7 +51,10 @@ class _HistoryAiChatPageState extends State {
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
- context.read().getChats();
+ if (context.read().refresh) {
+ context.read().getChats();
+ context.read().refresh = false;
+ }
return true;
},
child: DidvanScaffold(
@@ -70,13 +73,17 @@ class _HistoryAiChatPageState extends State {
asset: Assets.emptyResult,
title: 'لیست خالی است',
),
- enableEmptyState: state.chats.isEmpty,
+ enableEmptyState: archived
+ ? state.archivedChats.isEmpty
+ : state.chats.isEmpty,
placeholder: const _HistoryPlaceholder(),
placeholderCount: 8,
// builder: (context, state, index) => _HistoryPlaceholder(),
builder: (context, state, index) {
- final chat = state.chats[index];
+ final chat = archived
+ ? state.archivedChats[index]
+ : state.chats[index];
TextEditingController title =
TextEditingController(text: state.chats[index].title);
return Dismissible(
@@ -349,8 +356,10 @@ class _HistoryAiChatPageState extends State {
),
);
},
- childCount: state.chats.length,
- onRetry: state.getChats);
+ childCount: archived
+ ? state.archivedChats.length
+ : state.chats.length,
+ onRetry: () => state.getChats(archived: archived));
},
)
],
@@ -359,7 +368,10 @@ class _HistoryAiChatPageState extends State {
hasBack: true,
hasElevation: true,
backClick: () {
- context.read().getChats();
+ if (context.read().refresh) {
+ context.read().getChats();
+ context.read().refresh = false;
+ }
},
trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
@@ -370,7 +382,7 @@ class _HistoryAiChatPageState extends State {
data: ActionSheetData(
onConfirmed: () async {
final state = context.read();
- await state.deleteAllChat();
+ archived ? null : await state.deleteAllChat();
},
content: Column(
children: [
@@ -385,7 +397,9 @@ class _HistoryAiChatPageState extends State {
width: 8,
),
DidvanText(
- 'پاک کردن همه گفتوگوها',
+ archived
+ ? 'خارج کردن همه آرشیو ها'
+ : 'پاک کردن همه گفتوگوها',
color: Theme.of(context).colorScheme.error,
fontSize: 20,
),
diff --git a/lib/views/ai/history_ai_chat_state.dart b/lib/views/ai/history_ai_chat_state.dart
index f7a3043..e69a0a9 100644
--- a/lib/views/ai/history_ai_chat_state.dart
+++ b/lib/views/ai/history_ai_chat_state.dart
@@ -12,12 +12,14 @@ import 'package:flutter/cupertino.dart';
class HistoryAiChatState extends CoreProvier {
final List chats = [];
+ final List archivedChats = [];
// final List chatsToDelete = [];
final List bots = [];
BotsModel? bot;
ValueNotifier loadingBots = ValueNotifier(false);
bool loadingchangeTitle = false;
bool loadingdeleteAll = false;
+ bool refresh = false;
Timer? timer;
String search = '';
@@ -31,10 +33,12 @@ class HistoryAiChatState extends CoreProvier {
);
await service.httpGet();
if (service.isSuccess) {
- chats.clear();
+ archived != null && archived ? archivedChats.clear() : chats.clear();
final messages = service.result['chats'];
for (var i = 0; i < messages.length; i++) {
- chats.add(ChatsModel.fromJson(messages[i]));
+ archived != null && archived
+ ? archivedChats.add(ChatsModel.fromJson(messages[i]))
+ : chats.add(ChatsModel.fromJson(messages[i]));
}
appState = AppState.idle;
update();
@@ -140,7 +144,8 @@ class HistoryAiChatState extends CoreProvier {
// update();
// }
- Future changeNameChat(int id, int index, String title) async {
+ Future changeNameChat(int id, int index, String title,
+ {final bool refresh = true}) async {
loadingchangeTitle = true;
update();
final service =
@@ -152,7 +157,7 @@ class HistoryAiChatState extends CoreProvier {
appState = AppState.idle;
loadingchangeTitle = false;
update();
-
+ this.refresh = refresh;
return;
}
appState = AppState.failed;
@@ -163,7 +168,8 @@ class HistoryAiChatState extends CoreProvier {
update();
}
- Future deleteChat(int id, int index) async {
+ Future deleteChat(int id, int index,
+ {final bool refresh = true}) async {
final service = RequestService(RequestHelper.deleteChat(id));
await service.delete();
if (service.isSuccess) {
@@ -173,6 +179,7 @@ class HistoryAiChatState extends CoreProvier {
loadingchangeTitle = false;
update();
+ this.refresh = refresh;
return;
}
@@ -184,7 +191,7 @@ class HistoryAiChatState extends CoreProvier {
update();
}
- Future deleteAllChat() async {
+ Future deleteAllChat({final bool refresh = true}) async {
loadingdeleteAll = true;
update();
final service = RequestService(
@@ -198,6 +205,7 @@ class HistoryAiChatState extends CoreProvier {
loadingdeleteAll = false;
update();
+ this.refresh = refresh;
return;
}
@@ -209,7 +217,8 @@ class HistoryAiChatState extends CoreProvier {
update();
}
- Future archivedChat(int id, int index) async {
+ Future archivedChat(int id, int index,
+ {final bool refresh = true}) async {
update();
final service = RequestService(
RequestHelper.archivedChat(id),
@@ -221,6 +230,7 @@ class HistoryAiChatState extends CoreProvier {
appState = AppState.idle;
update();
+ this.refresh = refresh;
return true;
}
diff --git a/lib/views/ai/widgets/ai_message_bar.dart b/lib/views/ai/widgets/ai_message_bar.dart
index ce69783..67a0d0c 100644
--- a/lib/views/ai/widgets/ai_message_bar.dart
+++ b/lib/views/ai/widgets/ai_message_bar.dart
@@ -9,31 +9,28 @@ 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/network/request.dart';
-import 'package:didvan/services/network/request_helper.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/ai/widgets/voice_message_view.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:mime/mime.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;
-import 'package:voice_message_package/voice_message_package.dart';
class AiMessageBar extends StatefulWidget {
final FocusNode? focusNode;
@@ -81,6 +78,7 @@ class AiMessageBar extends StatefulWidget {
class _AiMessageBarState extends State {
final ValueNotifier messageText = ValueNotifier('');
+ bool openAttach = false;
@override
void initState() {
@@ -122,23 +120,115 @@ class _AiMessageBarState extends State {
return IgnorePointer(
ignoring: state.onResponsing,
- child: Column(
- children: [
- fileContainer(),
- Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Expanded(
- child: Column(
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 24).copyWith(
+ top: openAttach ||
+ (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: Column(
+ children: [
+ Stack(
+ children: [
+ fileContainer(),
+ AnimatedVisibility(
+ isVisible: openAttach,
+ duration: DesignConfig.lowAnimationDuration,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12, vertical: 24),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (historyState.bot!.attachmentType!
+ .contains('pdf'))
+ attachBtn(
+ title: "PDF",
+ icon: CupertinoIcons.doc_fill,
+ color: Colors.redAccent,
+ click: () async {
+ FilePickerResult? result =
+ await MediaService.pickPdfFile();
+ if (result != null) {
+ state.file =
+ FilesModel(result.files.single.path!);
+ // Do something with the selected PDF file
+ }
+ },
+ ),
+ if (historyState.bot!.attachmentType!
+ .contains('image'))
+ attachBtn(
+ title: "تصویر",
+ icon: CupertinoIcons.photo,
+ color: Colors.deepOrangeAccent,
+ click: () async {
+ 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) return;
+ }
+ if (pickedFile == null) return;
+ state.file = kIsWeb
+ ? FilesModel(pickedFile.path)
+ : FilesModel(file!.path);
+ },
+ ),
+ if (historyState.bot!.attachmentType!
+ .contains('audio'))
+ attachBtn(
+ title: "صوت",
+ icon: CupertinoIcons.music_note_2,
+ color: Colors.indigoAccent,
+ click: () async {
+ FilePickerResult? result =
+ await MediaService.pickAudioFile();
+ if (result != null) {
+ state.file =
+ FilesModel(result.files.single.path!);
+ }
+ },
+ )
+ ],
+ ),
+ )),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
children: [
- audioContainer(),
- Container(
+ Expanded(
+ child: Container(
+ // height: 50,
decoration: BoxDecoration(
boxShadow: DesignConfig.defaultShadow,
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: Theme.of(context).colorScheme.border),
- borderRadius: BorderRadius.circular(360)),
+ borderRadius: DesignConfig.highBorderRadius),
child: Row(
children: [
const SizedBox(
@@ -152,145 +242,170 @@ class _AiMessageBarState extends State {
valueListenable: messageText,
builder: (context, value, child) {
return Row(
+ crossAxisAlignment:
+ CrossAxisAlignment.end,
children: [
- (snapshot.hasData &&
- snapshot.data! !=
- RecordState.stop)
- ? MessageBarBtn(
- enable: true,
- icon: DidvanIcons
- .stop_circle_solid,
- click: () async {
- final path =
- await record.stop();
- state.file = FilesModel(
- path.toString());
- _timer.cancel();
- _countTimer.value = 0;
- state.update();
- },
- )
- : widget.bot.attachmentType!
- .contains(
- 'audio') &&
- value.isEmpty &&
- state.file == null &&
- widget.bot
- .attachment !=
- 0
- ? MessageBarBtn(
- enable: false,
- icon: DidvanIcons
- .mic_regular,
- click: () async {
- if (await record
- .hasPermission()) {
- Directory?
- downloadDir =
- await getDownloadsDirectory();
+ Padding(
+ padding: const EdgeInsets.only(
+ bottom: 8.0),
+ child: (snapshot.hasData &&
+ snapshot.data! !=
+ RecordState.stop)
+ ? MessageBarBtn(
+ enable: true,
+ icon: DidvanIcons
+ .stop_circle_solid,
+ click: () async {
+ final path =
+ await record
+ .stop();
+ state.file = FilesModel(
+ path.toString(),
+ isRecorded: true);
+ _timer.cancel();
+ _countTimer.value = 0;
+ state.update();
+ },
+ )
+ : widget.bot.attachmentType!
+ .contains(
+ 'audio') &&
+ value.isEmpty &&
+ state.file ==
+ null &&
+ widget.bot
+ .attachment !=
+ 0
+ ? MessageBarBtn(
+ enable: false,
+ icon: DidvanIcons
+ .mic_regular,
+ click: () async {
+ if (await record
+ .hasPermission()) {
+ Directory?
+ downloadDir =
+ await getDownloadsDirectory();
- record.start(
- const RecordConfig(),
- path:
- '${downloadDir!.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a');
- startTimer();
- }
- },
- )
- : MessageBarBtn(
- enable:
- value.isNotEmpty,
- icon: DidvanIcons
- .send_light,
- click: () async {
- if (value.isEmpty) {
- return;
- }
+ record.start(
+ const RecordConfig(),
+ path:
+ '${downloadDir!.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a');
+ startTimer();
+ }
+ },
+ )
+ : 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 ==
- null
- ? null
- : p.basename(state
- .file!
- .path),
- finished: true,
- role: 'user',
- createdAt: DateTime
- .now()
- .subtract(const Duration(
- minutes:
- 210))
- .toIso8601String(),
- ));
- } else {
- state.messages.add(MessageModel(
- dateTime: DateTime
+ 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 ==
+ null
+ ? null
+ : p.basename(state
+ .file!
+ .path),
+ finished:
+ true,
+ role: 'user',
+ createdAt: 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 ==
- null
- ? null
- : p.basename(state
- .file!
- .path),
- role:
- 'user',
- createdAt: DateTime
- .now()
- .subtract(
- const Duration(minutes: 210))
- .toIso8601String(),
- )
- ]));
- }
- state.message
- .clear();
- messageText.value =
- state.message
- .text;
- await state
- .postMessage(
- widget.bot);
- },
- ),
- const SizedBox(
- width: 12,
+ ));
+ } 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 ==
+ null
+ ? null
+ : p.basename(state.file!.path),
+ 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(
@@ -312,17 +427,25 @@ class _AiMessageBarState extends State {
child: Row(
mainAxisAlignment:
MainAxisAlignment
- .center,
+ .spaceBetween,
children: [
- SpinKitWave(
- color: Theme.of(
- context)
- .colorScheme
- .primary,
- size: 32,
- ),
- const SizedBox(
- width: 24,
+ Directionality(
+ textDirection:
+ TextDirection
+ .ltr,
+ child: Row(
+ mainAxisAlignment:
+ MainAxisAlignment
+ .center,
+ children: List
+ .generate(
+ 3,
+ (index) =>
+ SpinKitWave(
+ color: Theme.of(context).colorScheme.primary.withOpacity(0.4),
+ size: 32,
+ itemCount: 10,
+ ))),
),
ValueListenableBuilder<
int>(
@@ -334,100 +457,114 @@ class _AiMessageBarState extends State {
DidvanText(DateTimeUtils.normalizeTimeDuration(Duration(
seconds:
value))),
- )
+ ),
],
),
)
- : Form(
- child: TextFormField(
- textInputAction:
- TextInputAction
- .newline,
- style:
- Theme.of(context)
+ : state.file != null &&
+ state.file!
+ .isRecorded
+ ? audioContainer()
+ : Form(
+ child:
+ TextFormField(
+ textInputAction:
+ TextInputAction
+ .newline,
+ style: Theme.of(
+ context)
.textTheme
.bodyMedium,
- maxLines: 2,
- minLines: 1,
- // keyboardType: TextInputType.text,
+ minLines: 1,
+ maxLines:
+ 6, // Set this
+ // expands: true, //
+ // keyboardType: TextInputType.text,
+ keyboardType:
+ TextInputType
+ .multiline,
+ controller:
+ state.message,
+ focusNode: widget
+ .focusNode,
+ enabled: !(state
+ .file !=
+ null &&
+ widget.bot
+ .attachment ==
+ 1),
- controller:
- state.message,
- focusNode:
- widget.focusNode,
- 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(),
+ ),
- 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;
- },
- )),
+ onChanged:
+ (value) {
+ messageText
+ .value =
+ value;
+ },
+ )),
),
),
if (snapshot.hasData)
- 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: 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(),
+ ),
],
);
});
@@ -438,178 +575,107 @@ class _AiMessageBarState extends State {
),
],
),
+ )),
+ const SizedBox(
+ width: 12,
),
+ ValueListenableBuilder(
+ valueListenable: messageText,
+ builder: (context, value, child) {
+ if (context
+ .read()
+ .bot!
+ .attachmentType!
+ .isNotEmpty &&
+ (widget.bot.attachment != 0 &&
+ (widget.bot.attachment == 1 &&
+ value.isEmpty)) ||
+ widget.bot.attachment == 2) {
+ return SizedBox(
+ width: 46,
+ height: 46,
+ child: Center(
+ child: InkWell(
+ onTap: () {
+ if (state.file != null) {
+ state.file = null;
+ state.update();
+ return;
+ }
+ setState(() {
+ openAttach = !openAttach;
+ });
+ },
+ child: Icon(
+ state.file != null
+ ? DidvanIcons.close_solid
+ : Icons.attach_file_rounded,
+ color: Theme.of(context)
+ .colorScheme
+ .focusedBorder,
+ ),
+ )));
+ }
+ return const SizedBox();
+ }),
],
- )),
- const SizedBox(
- width: 12,
),
- ValueListenableBuilder(
- valueListenable: messageText,
- builder: (context, value, child) {
- if (context
- .read()
- .bot!
- .attachmentType!
- .isNotEmpty &&
- (widget.bot.attachment != 0 &&
- (widget.bot.attachment == 1 &&
- value.isEmpty)) ||
- widget.bot.attachment == 2) {
- return SizedBox(
- width: 46,
- height: 46,
- child: Center(
- child: PopupMenuButton(
- onOpened: () {},
- surfaceTintColor: Colors.transparent,
- onSelected: (value) async {
- switch (value) {
- case 'Pdf':
- FilePickerResult? result =
- await MediaService.pickPdfFile();
- if (result != null) {
- state.file = FilesModel(
- result.files.single.path!);
- // Do something with the selected PDF file
- }
- // else {
- //// User cancelled the file selection
- // }
- break;
-
- case 'Image':
- 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) return;
- }
- if (pickedFile == null) return;
- state.file = kIsWeb
- ? FilesModel(pickedFile.path)
- : FilesModel(file!.path);
-
- break;
-
- case 'Audio':
- FilePickerResult? result =
- await MediaService.pickAudioFile();
- if (result != null) {
- state.file = FilesModel(
- result.files.single.path!);
- }
- break;
- default:
- }
-
- state.update();
- },
- itemBuilder: (BuildContext context) => [
- if (historyState.bot!.attachmentType!
- .contains('pdf'))
- AiMessageBar.popUpBtns(
- value: 'Pdf',
- icon: Icons.picture_as_pdf),
- if (historyState.bot!.attachmentType!
- .contains('image'))
- AiMessageBar.popUpBtns(
- value: 'Image', icon: Icons.image),
- if (historyState.bot!.attachmentType!
- .contains('audio'))
- AiMessageBar.popUpBtns(
- value: 'Audio',
- icon: Icons.audio_file),
- ],
- offset: Offset(-20,
- widget.focusNode!.hasFocus ? -999 : 999),
- position: PopupMenuPosition.over,
- useRootNavigator: true,
- child: Icon(
- Icons.attach_file_rounded,
- color: Theme.of(context)
- .colorScheme
- .focusedBorder,
- ),
- ),
- ));
- }
- return const SizedBox();
- }),
- ],
- ),
- ],
+ ),
+ ],
+ ),
),
);
},
);
}
- AnimatedVisibility audioContainer() {
+ InkWell attachBtn(
+ {required final String title,
+ required final IconData icon,
+ final Color? color,
+ final Function()? click}) {
+ final state = context.read();
+ return InkWell(
+ onTap: () async {
+ await click?.call();
+ state.update();
+ setState(() {
+ openAttach = false;
+ });
+ },
+ 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();
- return AnimatedVisibility(
- isVisible: state.file != null &&
- lookupMimeType(state.file!.path)!.startsWith('audio/'),
- duration: DesignConfig.lowAnimationDuration,
- child: SizedBox(
- width: MediaQuery.sizeOf(context).width,
- child: Padding(
- padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
- child: Container(
- height: 46,
- decoration: BoxDecoration(
- boxShadow: DesignConfig.defaultShadow,
- color: Theme.of(context).colorScheme.surface,
- borderRadius: BorderRadius.circular(360)),
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: state.file == null
- ? const SizedBox()
- : MyVoiceMessageView(
- size: 32,
- controller: VoiceController(
- audioSrc: state.file!.path.startsWith('/uploads')
- ? '${RequestHelper.baseUrl + state.file!.path}?accessToken=${RequestService.token}'
- : state.file!.path,
- onComplete: () {
- /// do something on complete
- },
- onPause: () {
- /// do something on pause
- },
- onPlaying: () {
- /// do something on playing
- },
- onError: (err) {
- /// do somethin on error
- },
- isFile: !state.file!.path.startsWith('/uploads'),
- maxDuration: const Duration(seconds: 10),
- ),
- innerPadding: 0,
- cornerRadius: 20,
- circlesColor: Theme.of(context).colorScheme.primary,
- activeSliderColor: Theme.of(context).colorScheme.primary,
- trashClick: () async {
- state.file = null;
- state.update();
- },
- ),
- ),
- ),
+ return SizedBox(
+ width: MediaQuery.sizeOf(context).width,
+ child: AudioWave(
+ file: state.file!.path,
+ loadingPaddingSize: 8.0,
),
);
}
@@ -621,8 +687,7 @@ class _AiMessageBarState extends State {
basename = p.basename(state.file!.path);
}
return AnimatedVisibility(
- isVisible: state.file != null &&
- !lookupMimeType(state.file!.path)!.startsWith('audio/'),
+ isVisible: state.file != null && !state.file!.isRecorded,
duration: DesignConfig.lowAnimationDuration,
child: Container(
decoration: BoxDecoration(
@@ -630,50 +695,50 @@ class _AiMessageBarState extends State {
color: Theme.of(context).colorScheme.border,
),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
- margin: const EdgeInsets.only(bottom: 8),
+ margin: const EdgeInsets.only(bottom: 8, left: 12, right: 12),
child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- children: [
- const Icon(Icons.file_copy),
- const SizedBox(
- width: 12,
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- SizedBox(
- width: 260,
- height: 24,
- child: MarqueeText(
- text: basename,
- style: const TextStyle(fontSize: 14),
- stop: const Duration(seconds: 3),
- ),
- ),
- if (state.file != null)
- 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,
- );
- })
- ],
- )
- ],
+ state.file != null && state.file!.isImage
+ ? SizedBox(
+ width: 32,
+ height: 42,
+ child: ClipRRect(
+ borderRadius: DesignConfig.lowBorderRadius,
+ child: Image.file(
+ state.file!.main,
+ fit: BoxFit.cover,
+ )))
+ : const Icon(Icons.file_copy),
+ const SizedBox(
+ width: 12,
),
- InkWell(
- onTap: () {
- state.file = null;
- state.update();
- },
- child: const Icon(DidvanIcons.close_circle_solid))
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ height: 24,
+ child: MarqueeText(
+ text: basename,
+ style: const TextStyle(fontSize: 14),
+ stop: const Duration(seconds: 3),
+ ),
+ ),
+ if (state.file != null)
+ 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,
+ );
+ })
+ ],
+ ),
+ )
],
),
),
diff --git a/lib/views/ai/widgets/audio_wave.dart b/lib/views/ai/widgets/audio_wave.dart
new file mode 100644
index 0000000..6be3f11
--- /dev/null
+++ b/lib/views/ai/widgets/audio_wave.dart
@@ -0,0 +1,306 @@
+// ignore_for_file: implementation_imports, library_private_types_in_public_api
+
+import 'dart:math';
+
+import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/services/network/request.dart';
+import 'package:didvan/services/network/request_helper.dart';
+import 'package:didvan/utils/date_time.dart';
+import 'package:didvan/utils/media.dart';
+import 'package:didvan/views/ai/widgets/message_bar_btn.dart';
+import 'package:didvan/views/widgets/didvan/text.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_spinkit/flutter_spinkit.dart';
+import 'package:just_audio/just_audio.dart';
+
+class AudioWave extends StatefulWidget {
+ final String file;
+ final double loadingPaddingSize;
+ const AudioWave({Key? key, required this.file, this.loadingPaddingSize = 0})
+ : super(key: key);
+
+ @override
+ _AudioWaveState createState() => _AudioWaveState();
+}
+
+class _AudioWaveState extends State {
+ final int itemCount = 35;
+ final AudioPlayer audioPlayer = AudioPlayer();
+ final ValueNotifier> randoms = ValueNotifier([]);
+ final ValueNotifier> randomsDisable = ValueNotifier([]);
+
+ Duration totalDuration = Duration.zero;
+ double currentPosition = 0;
+ bool loading = true;
+ bool faile = false;
+
+ @override
+ void initState() {
+ super.initState();
+ try {
+ init();
+ listeners();
+ } catch (e) {
+ if (kDebugMode) {
+ print('Error occurred: $e');
+ }
+
+ rethrow;
+ }
+ }
+
+ void setRandoms() {
+ for (var i = 0; i < itemCount; i++) {
+ randoms.value.add(0);
+ randomsDisable.value.add(5.74.w() * Random().nextDouble() + .26.w());
+ }
+ }
+
+ Future init() async {
+ try {
+ final path = widget.file;
+ if (widget.file.startsWith('/uploads')) {
+ final audioSource = LockCachingAudioSource(Uri.parse(
+ '${RequestHelper.baseUrl + path}?accessToken=${RequestService.token}'));
+ totalDuration =
+ await audioPlayer.setAudioSource(audioSource) ?? Duration.zero;
+ } else {
+ totalDuration = await audioPlayer.setFilePath(path) ?? Duration.zero;
+ }
+ setRandoms();
+ setState(() {
+ loading = false;
+ });
+ } catch (e) {
+ setState(() {
+ faile = true;
+ loading = false;
+ });
+
+ if (kDebugMode) {
+ print('Error occurred: $e');
+ }
+ }
+ }
+
+ Future listeners() async {
+ audioPlayer.positionStream.listen((position) async {
+ for (var i = 0; i < itemCount; i++) {
+ if (i <
+ ((position.inMilliseconds * 40) / totalDuration.inMilliseconds)) {
+ final ran = randomsDisable.value[i];
+ randoms.value[i] = ran;
+ } else {
+ randoms.value[i] = 0;
+ }
+ }
+ if (position.inMilliseconds >= totalDuration.inMilliseconds) {
+ audioPlayer.stop();
+ audioPlayer.seek(Duration.zero);
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ audioPlayer.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: 46,
+ child: loading
+ ? Padding(
+ padding:
+ EdgeInsets.symmetric(vertical: widget.loadingPaddingSize),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: List.generate(
+ 5,
+ (index) => SpinKitWave(
+ color: Theme.of(context)
+ .colorScheme
+ .primary
+ .withOpacity(0.4),
+ size: 32,
+ itemCount: 10,
+ ))),
+ )
+ : Directionality(
+ textDirection: TextDirection.ltr,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ StreamBuilder(
+ stream: audioPlayer.playerStateStream,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const SizedBox();
+ }
+ return MessageBarBtn(
+ enable: true,
+ icon: faile
+ ? DidvanIcons.refresh_solid
+ : snapshot.data!.playing
+ ? DidvanIcons.pause_solid
+ : DidvanIcons.play_solid,
+ click: () async {
+ if (faile) {
+ randoms.value.clear();
+ randomsDisable.value.clear();
+ setState(() {
+ loading = true;
+ faile = false;
+ });
+ init();
+ return;
+ }
+ if (snapshot.data!.playing) {
+ await audioPlayer.pause();
+ } else {
+ await audioPlayer.play();
+ }
+ },
+ );
+ }),
+ faile
+ ? const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 8.0),
+ child: DidvanText(
+ 'خطا در بارگزاری فایل صوتی',
+ fontSize: 12,
+ ),
+ )
+ : StreamBuilder(
+ stream: audioPlayer.positionStream,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const SizedBox();
+ }
+ currentPosition =
+ snapshot.data!.inMilliseconds.toDouble();
+ return Expanded(
+ child: Row(
+ children: [
+ ValueListenableBuilder(
+ valueListenable: randoms,
+ builder: (context, value, child) {
+ return Expanded(
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ noise(values: randoms.value),
+ noise(
+ values: randomsDisable.value,
+ color: Theme.of(context)
+ .colorScheme
+ .primary
+ .withOpacity(0.4)),
+ if (totalDuration != Duration.zero)
+ Opacity(
+ opacity: 0,
+ child: Container(
+ width: 50.5.w(),
+ color: Colors.transparent
+ .withOpacity(1),
+ child: Theme(
+ data: Theme.of(context)
+ .copyWith(
+ sliderTheme:
+ SliderThemeData(
+ thumbShape:
+ SliderComponentShape
+ .noThumb,
+ minThumbSeparation: 0,
+ ),
+ splashColor:
+ Colors.transparent,
+ ),
+ child: Slider(
+ value: currentPosition,
+ max: totalDuration
+ .inMilliseconds
+ .toDouble() +
+ const Duration(
+ milliseconds:
+ 10)
+ .inMilliseconds
+ .toDouble(),
+ onChangeStart: (value) {
+ // audioPlayer.pause();
+ },
+ onChanged: (value) {
+ for (var i = 0;
+ i < itemCount;
+ i++) {
+ if (i <
+ ((value * 40) /
+ totalDuration
+ .inMilliseconds)) {
+ final ran =
+ randomsDisable
+ .value[i];
+ randoms.value[i] =
+ ran;
+ } else {
+ randoms.value[i] = 0;
+ }
+ }
+ setState(() {
+ currentPosition = value;
+ });
+ },
+ onChangeEnd: (value) {
+ audioPlayer.seek(Duration(
+ milliseconds:
+ value.round()));
+ audioPlayer.play();
+ },
+ ),
+ ),
+ ),
+ ),
+ ],
+ ));
+ },
+ ),
+ DidvanText(
+ DateTimeUtils.normalizeTimeDuration(
+ snapshot.data! == Duration.zero
+ ? totalDuration
+ : snapshot.data!)),
+ ],
+ ),
+ );
+ },
+ )
+ ],
+ ),
+ ),
+ );
+ }
+
+ Row noise({required final List values, final Color? color}) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: values
+ .map(
+ (e) => Container(
+ margin: EdgeInsets.symmetric(horizontal: .2.w()),
+ width: .56.w(),
+ height: e,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(1000),
+ color: color ?? Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ )
+ .toList(),
+ );
+ }
+}
diff --git a/lib/views/ai/widgets/voice_message_view.dart b/lib/views/ai/widgets/voice_message_view.dart
deleted file mode 100644
index 1b6da9a..0000000
--- a/lib/views/ai/widgets/voice_message_view.dart
+++ /dev/null
@@ -1,294 +0,0 @@
-// ignore_for_file: implementation_imports, unused_element, empty_catches
-
-import 'package:didvan/constants/app_icons.dart';
-import 'package:didvan/views/ai/widgets/message_bar_btn.dart';
-import 'package:flutter/material.dart';
-import 'package:voice_message_package/src/helpers/play_status.dart';
-import 'package:voice_message_package/src/helpers/utils.dart';
-import 'package:voice_message_package/src/voice_controller.dart';
-import 'package:voice_message_package/src/widgets/noises.dart';
-import 'package:voice_message_package/src/widgets/play_pause_button.dart';
-
-/// A widget that displays a voice message view with play/pause functionality.
-///
-/// The [VoiceMessageView] widget is used to display a voice message with customizable appearance and behavior.
-/// It provides a play/pause button, a progress slider, and a counter for the remaining time.
-/// The appearance of the widget can be customized using various properties such as background color, slider color, and text styles.
-///
-class MyVoiceMessageView extends StatelessWidget {
- const MyVoiceMessageView(
- {Key? key,
- required this.controller,
- this.backgroundColor = Colors.transparent,
- this.activeSliderColor = Colors.red,
- this.notActiveSliderColor,
- this.circlesColor = Colors.red,
- this.innerPadding = 12,
- this.cornerRadius = 20,
- // this.playerWidth = 170,
- this.size = 38,
- this.refreshIcon = const Icon(
- Icons.refresh,
- color: Colors.white,
- ),
- this.pauseIcon = const Icon(
- Icons.pause_rounded,
- color: Colors.white,
- ),
- this.playIcon = const Icon(
- Icons.play_arrow_rounded,
- color: Colors.white,
- ),
- this.stopDownloadingIcon = const Icon(
- Icons.close,
- color: Colors.white,
- ),
- this.playPauseButtonDecoration,
- this.circlesTextStyle = const TextStyle(
- color: Colors.white,
- fontSize: 10,
- fontWeight: FontWeight.bold,
- ),
- this.counterTextStyle = const TextStyle(
- fontSize: 11,
- fontWeight: FontWeight.w500,
- ),
- this.playPauseButtonLoadingColor = Colors.white,
- this.trashClick,
- this.showSpeed = false})
- : super(key: key);
-
- /// The controller for the voice message view.
- final VoiceController controller;
-
- /// The background color of the voice message view.
- final Color backgroundColor;
-
- ///
- final Color circlesColor;
-
- /// The color of the active slider.
- final Color activeSliderColor;
-
- /// The color of the not active slider.
- final Color? notActiveSliderColor;
-
- /// The text style of the circles.
- final TextStyle circlesTextStyle;
-
- /// The text style of the counter.
- final TextStyle counterTextStyle;
-
- /// The padding between the inner content and the outer container.
- final double innerPadding;
-
- /// The corner radius of the outer container.
- final double cornerRadius;
-
- /// The size of the play/pause button.
- final double size;
-
- /// The refresh icon of the play/pause button.
- final Widget refreshIcon;
-
- /// The pause icon of the play/pause button.
- final Widget pauseIcon;
-
- /// The play icon of the play/pause button.
- final Widget playIcon;
-
- /// The stop downloading icon of the play/pause button.
- final Widget stopDownloadingIcon;
-
- /// The play Decoration of the play/pause button.
- final Decoration? playPauseButtonDecoration;
-
- /// The loading Color of the play/pause button.
- final Color playPauseButtonLoadingColor;
-
- final Function()? trashClick;
-
- final bool showSpeed;
-
- @override
-
- /// Build voice message view.
- Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final color = circlesColor;
- final newTHeme = theme.copyWith(
- sliderTheme: SliderThemeData(
- trackShape: CustomTrackShape(),
- thumbShape: SliderComponentShape.noThumb,
- minThumbSeparation: 0,
- ),
- splashColor: Colors.transparent,
- );
-
- return Directionality(
- textDirection: TextDirection.ltr,
- child: Container(
- width: 160 + (controller.noiseCount * .72.w()),
- padding: EdgeInsets.all(innerPadding),
- decoration: BoxDecoration(
- color: backgroundColor,
- borderRadius: BorderRadius.circular(cornerRadius),
- ),
- child: ValueListenableBuilder(
- /// update ui when change play status
- valueListenable: controller.updater,
- builder: (context, value, child) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const SizedBox(width: 12),
-
- /// play pause button
- PlayPauseButton(
- controller: controller,
- color: color,
- loadingColor: playPauseButtonLoadingColor,
- size: size,
- refreshIcon: refreshIcon,
- pauseIcon: pauseIcon,
- playIcon: playIcon,
- stopDownloadingIcon: stopDownloadingIcon,
- buttonDecoration: playPauseButtonDecoration,
- ),
- const SizedBox(width: 12),
-
- ///
-
- /// slider & noises
- Expanded(
- child: _noises(newTHeme),
- ),
- const SizedBox(width: 12),
- Text(controller.remindingTime, style: counterTextStyle),
-
- const SizedBox(width: 12),
-
- ///
-
- /// speed button
- if (showSpeed) _changeSpeedButton(color),
-
- ///
- if (trashClick != null)
- MessageBarBtn(
- enable: true,
- icon: DidvanIcons.trash_solid,
- color: Theme.of(context).colorScheme.error,
- click: () async {
- trashClick?.call();
- }),
- const SizedBox(width: 12),
- ],
- );
- },
- ),
- ),
- );
- }
-
- SizedBox _noises(ThemeData newTHeme) => SizedBox(
- child: Stack(
- alignment: Alignment.center,
- children: [
- /// noises
- Noises(
- rList: controller.randoms!,
- activeSliderColor: activeSliderColor,
- ),
-
- /// slider
- AnimatedBuilder(
- animation: CurvedAnimation(
- parent: controller.animController,
- curve: Curves.ease,
- ),
- builder: (BuildContext context, Widget? child) {
- return Positioned(
- left: controller.animController.value,
- child: Container(
- width: controller.noiseWidth,
- height: 6.w(),
- color: notActiveSliderColor ?? backgroundColor,
- ),
- );
- },
- ),
- Opacity(
- opacity: 0,
- child: Container(
- width: controller.noiseWidth,
- color: Colors.transparent.withOpacity(1),
- child: Theme(
- data: newTHeme,
- child: Slider(
- value: controller.currentMillSeconds,
- max: controller.maxMillSeconds,
- onChangeStart: controller.onChangeSliderStart,
- onChanged: controller.onChanging,
- onChangeEnd: (value) {
- controller.onSeek(
- Duration(milliseconds: value.toInt()),
- );
- controller.play();
- },
- ),
- ),
- ),
- ),
- ],
- ),
- );
-
- Transform _changeSpeedButton(Color color) => Transform.translate(
- offset: const Offset(0, -7),
- child: GestureDetector(
- onTap: () {
- controller.changeSpeed();
- },
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
- decoration: BoxDecoration(
- color: color,
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text(
- controller.speed.playSpeedStr,
- style: circlesTextStyle,
- ),
- ),
- ),
- );
-}
-
-///
-/// A custom track shape for a slider that is rounded rectangular in shape.
-/// Extends the [RoundedRectSliderTrackShape] class.
-class CustomTrackShape extends RoundedRectSliderTrackShape {
- @override
-
- /// Returns the preferred rectangle for the voice message view.
- ///
- /// The preferred rectangle is calculated based on the current state and layout
- /// of the voice message view. It represents the area where the view should be
- /// displayed on the screen.
- ///
- /// Returns a [Rect] object representing the preferred rectangle.
- Rect getPreferredRect({
- required RenderBox parentBox,
- Offset offset = Offset.zero,
- required SliderThemeData sliderTheme,
- bool isEnabled = false,
- bool isDiscrete = false,
- }) {
- const double trackHeight = 10;
- final double trackLeft = offset.dx,
- trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
- final double trackWidth = parentBox.size.width;
- return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
- }
-}
diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart
index 54fde6a..bedce95 100644
--- a/lib/views/authentication/screens/username.dart
+++ b/lib/views/authentication/screens/username.dart
@@ -1,3 +1,4 @@
+import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/views/authentication/authentication_state.dart';
import 'package:didvan/views/authentication/widgets/authentication_layout.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
@@ -6,7 +7,7 @@ import 'package:didvan/views/widgets/didvan/text_field.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher.dart';
+// import 'package:url_launcher/url_launcher.dart';
class UsernameInput extends StatefulWidget {
const UsernameInput({
@@ -78,10 +79,8 @@ class _UsernameInputState extends State {
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.primary),
recognizer: TapGestureRecognizer()
- ..onTap = () => launchUrl(
- Uri.parse(
- 'https://didvan.app/terms-of-use#conditions',
- ),
+ ..onTap = () => launchUrlString(
+ 'https://didvan.app/terms-of-use#conditions',
),
),
const TextSpan(text: 'و\n'),
@@ -92,10 +91,8 @@ class _UsernameInputState extends State {
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.primary),
recognizer: TapGestureRecognizer()
- ..onTap = () => launchUrl(
- Uri.parse(
- 'https://didvan.app/terms-of-use#privacy',
- ),
+ ..onTap = () => launchUrlString(
+ 'https://didvan.app/terms-of-use#privacy',
),
),
const TextSpan(text: 'را میپذیرم'),
diff --git a/lib/views/direct/direct_state.dart b/lib/views/direct/direct_state.dart
index feacc78..7eeb965 100644
--- a/lib/views/direct/direct_state.dart
+++ b/lib/views/direct/direct_state.dart
@@ -51,6 +51,7 @@ class DirectState extends CoreProvier {
recordedFile!.delete();
recordedFile = null;
notifyListeners();
+ update();
}
Future startRecording() async {
diff --git a/lib/views/direct/widgets/audio_widget.dart b/lib/views/direct/widgets/audio_widget.dart
index 4f32ae8..91b42a8 100644
--- a/lib/views/direct/widgets/audio_widget.dart
+++ b/lib/views/direct/widgets/audio_widget.dart
@@ -1,5 +1,3 @@
-// ignore_for_file: unused_element
-
import 'dart:io';
import 'package:didvan/config/theme_data.dart';
@@ -7,60 +5,62 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/services/media/media.dart';
-import 'package:didvan/services/network/request.dart';
-import 'package:didvan/services/network/request_helper.dart';
-import 'package:didvan/views/ai/widgets/voice_message_view.dart';
+import 'package:didvan/views/widgets/audio/audio_slider.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:provider/provider.dart';
-import 'package:voice_message_package/voice_message_package.dart';
class AudioWidget extends StatelessWidget {
final String? audioUrl;
final File? audioFile;
final int id;
final StudioDetailsData? audioMetaData;
- final Function()? deleteClidk;
const AudioWidget({
Key? key,
this.audioUrl,
this.audioFile,
required this.id,
this.audioMetaData,
- this.deleteClidk,
}) : super(key: key);
@override
Widget build(BuildContext context) {
- return MyVoiceMessageView(
- size: 32,
- controller: VoiceController(
- audioSrc: audioUrl != null
- ? '${RequestHelper.baseUrl + audioUrl!}?accessToken=${RequestService.token}'
- : audioFile!.path,
- onComplete: () {
- /// do something on complete
- },
- onPause: () {
- /// do something on pause
- },
- onPlaying: () {
- /// do something on playing
- },
- onError: (err) {
- /// do somethin on error
- },
- isFile: audioFile != null,
- maxDuration: const Duration(seconds: 10),
- ),
- innerPadding: 0,
- cornerRadius: 20,
- circlesColor: Theme.of(context).colorScheme.primary,
- activeSliderColor: Theme.of(context).colorScheme.primary,
- backgroundColor: Colors.transparent,
- trashClick: deleteClidk,
+ return StreamBuilder(
+ stream: MediaService.audioPlayer.playingStream,
+ builder: (context, snapshot) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 12),
+ child: Row(
+ children: [
+ Expanded(
+ child: AudioSlider(
+ tag: audioMetaData != null
+ ? '${audioMetaData!.type}-$id'
+ : 'message-$id',
+ duration: audioMetaData?.duration,
+ showTimer: true,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ _AudioControllerButton(
+ audioFile: audioFile,
+ audioUrl: audioUrl,
+ id: id,
+ audioMetaData: audioMetaData,
+ ),
+ ],
+ );
+ },
);
}
}
diff --git a/lib/views/direct/widgets/downloadable_audio_widget.dart b/lib/views/direct/widgets/downloadable_audio_widget.dart
new file mode 100644
index 0000000..da3ded1
--- /dev/null
+++ b/lib/views/direct/widgets/downloadable_audio_widget.dart
@@ -0,0 +1,30 @@
+// ignore_for_file: unused_element
+
+import 'dart:io';
+
+import 'package:didvan/models/studio_details_data.dart';
+import 'package:didvan/views/ai/widgets/audio_wave.dart';
+import 'package:flutter/material.dart';
+
+class DownloadableAudioWidget extends StatelessWidget {
+ final String? audioUrl;
+ final File? audioFile;
+ final int id;
+ final StudioDetailsData? audioMetaData;
+ final Function()? deleteClidk;
+ const DownloadableAudioWidget({
+ Key? key,
+ this.audioUrl,
+ this.audioFile,
+ required this.id,
+ this.audioMetaData,
+ this.deleteClidk,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: MediaQuery.sizeOf(context).width / 1.6,
+ child: AudioWave(file: audioUrl != null ? audioUrl! : audioFile!.path));
+ }
+}
diff --git a/lib/views/direct/widgets/message.dart b/lib/views/direct/widgets/message.dart
index 7769ed8..a3fa72c 100644
--- a/lib/views/direct/widgets/message.dart
+++ b/lib/views/direct/widgets/message.dart
@@ -4,7 +4,7 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/message_data/message_data.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/direct/direct_state.dart';
-import 'package:didvan/views/direct/widgets/audio_widget.dart';
+import 'package:didvan/views/direct/widgets/downloadable_audio_widget.dart';
import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
@@ -84,7 +84,7 @@ class Message extends StatelessWidget {
children: [
if (message.text != null) DidvanText(message.text!),
if (message.audio != null || message.audioFile != null)
- AudioWidget(
+ DownloadableAudioWidget(
audioFile: message.audioFile,
audioUrl: message.audio,
id: message.id,
diff --git a/lib/views/direct/widgets/message_box.dart b/lib/views/direct/widgets/message_box.dart
index 3ac2153..beb47b7 100644
--- a/lib/views/direct/widgets/message_box.dart
+++ b/lib/views/direct/widgets/message_box.dart
@@ -1,8 +1,9 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/views/ai/widgets/message_bar_btn.dart';
import 'package:didvan/views/direct/direct_state.dart';
-import 'package:didvan/views/direct/widgets/audio_widget.dart';
+import 'package:didvan/views/direct/widgets/downloadable_audio_widget.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/foundation.dart';
@@ -219,21 +220,30 @@ class _RecordChecking extends StatelessWidget {
},
color: Theme.of(context).colorScheme.focusedBorder,
),
+ const SizedBox(
+ width: 12,
+ ),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
- child: AudioWidget(
+ child: DownloadableAudioWidget(
audioFile: state.recordedFile!,
id: 0,
- deleteClidk: () => state.deleteRecordedFile,
+ // deleteClidk: () => state.deleteRecordedFile,
),
),
),
- // DidvanIconButton(
- // icon: DidvanIcons.trash_solid,
- // color: Theme.of(context).colorScheme.secondary,
- // onPressed: state.deleteRecordedFile,
- // ),
+ const SizedBox(
+ width: 12,
+ ),
+ MessageBarBtn(
+ enable: true,
+ icon: DidvanIcons.trash_solid,
+ color: Theme.of(context).colorScheme.error,
+ click: () => state.deleteRecordedFile()),
+ const SizedBox(
+ width: 12,
+ ),
],
);
}
diff --git a/lib/views/home/home.dart b/lib/views/home/home.dart
index 4422836..7d20912 100644
--- a/lib/views/home/home.dart
+++ b/lib/views/home/home.dart
@@ -61,7 +61,10 @@ class _HomeState extends State
_tabController.addListener(() {
state.currentPageIndex = _tabController.index;
if (_tabController.index == 2) {
+ homeScaffKey.currentState!.closeDrawer();
+
context.read().getChats();
+ context.read().getBots();
}
});
@@ -166,7 +169,8 @@ class _HomeState extends State
data: ActionSheetData(
onConfirmed: () async {
await state
- .deleteAllChat();
+ .deleteAllChat(
+ refresh: false);
},
content: Column(
children: [
@@ -463,7 +467,8 @@ class _HomeState extends State
onTap: () async {
if (title.text.isNotEmpty) {
await state.changeNameChat(
- chat.id!, index, title.text);
+ chat.id!, index, title.text,
+ refresh: false);
title.clear();
}
if (chat.isEditing != null) {
@@ -489,12 +494,12 @@ class _HomeState extends State
onSelected: (value) async {
switch (value) {
case 'حذف پیام':
- await state.deleteChat(chat.id!, index);
+ await state.deleteChat(chat.id!, index, refresh: false);
break;
case 'آرشیو':
- await state.archivedChat(chat.id!, index);
- await state.getChats();
+ await state.archivedChat(chat.id!, index,
+ refresh: false);
break;
default:
}
diff --git a/lib/views/home/home_state.dart b/lib/views/home/home_state.dart
index 4e66b0e..0af3c2a 100644
--- a/lib/views/home/home_state.dart
+++ b/lib/views/home/home_state.dart
@@ -196,7 +196,7 @@ class HomeState extends CoreProvier {
MenuItemType(
label: 'سها',
asset: Assets.saha,
- link: 'https://saha.didvan.app',
+ link: 'https://saha.didvan.app/app',
),
MenuItemType(
label: 'هوشان',
diff --git a/lib/views/home/main/main_page.dart b/lib/views/home/main/main_page.dart
index 61bf327..557362f 100644
--- a/lib/views/home/main/main_page.dart
+++ b/lib/views/home/main/main_page.dart
@@ -2,6 +2,7 @@ import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/home_page_content/home_page_list.dart';
import 'package:didvan/routes/routes.dart';
+import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/views/home/main/main_page_state.dart';
import 'package:didvan/views/home/main/widgets/banner.dart';
import 'package:didvan/views/home/main/widgets/general_item.dart';
@@ -12,7 +13,7 @@ import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+// import 'package:url_launcher/url_launcher_string.dart';
import 'package:didvan/services/network/request.dart';
class MainPage extends StatefulWidget {
diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart
index 7507d8d..d110d35 100644
--- a/lib/views/home/main/main_page_state.dart
+++ b/lib/views/home/main/main_page_state.dart
@@ -5,11 +5,12 @@ import 'package:didvan/models/requests/news.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/routes/routes.dart';
+import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:flutter/material.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+// import 'package:url_launcher/url_launcher_string.dart';
class MainPageState extends CoreProvier {
late MainPageContent content;
diff --git a/lib/views/home/main/widgets/banner.dart b/lib/views/home/main/widgets/banner.dart
index cfc3cb3..4ed1f6f 100644
--- a/lib/views/home/main/widgets/banner.dart
+++ b/lib/views/home/main/widgets/banner.dart
@@ -1,16 +1,12 @@
-import 'dart:math';
-import 'package:didvan/config/design_config.dart';
-import 'package:didvan/config/theme_data.dart';
-import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/services/app_initalizer.dart';
+import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/home/main/main_page_state.dart';
-import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/slider.dart';
-import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+// import 'package:url_launcher/url_launcher_string.dart';
class MainPageBanner extends StatelessWidget {
final bool isFirst;
@@ -26,7 +22,8 @@ class MainPageBanner extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 4),
child: GestureDetector(
onTap: () => item.link == null || item.link!.isEmpty
- ? _openInteractiveViewer(context, item.image)
+ ? ActionSheetUtils.openInteractiveViewer(
+ context, item.image, false)
: launchUrlString(item.link!, mode: LaunchMode.inAppWebView),
child: SkeletonImage(
imageUrl: item.image,
@@ -40,62 +37,4 @@ class MainPageBanner extends StatelessWidget {
height: (MediaQuery.of(context).size.width - 8) * 9.0 / 16.0,
);
}
-
- void _openInteractiveViewer(BuildContext context, String image) {
- showDialog(
- context: context,
- builder: (context) => Stack(
- children: [
- Positioned.fill(
- child: InteractiveViewer(
- child: Center(
- child: SkeletonImage(
- width: min(MediaQuery.of(context).size.width,
- MediaQuery.of(context).size.height),
- imageUrl: image,
- ),
- ),
- ),
- ),
- const Positioned(
- right: 24,
- top: 24,
- child: _BackButton(),
- ),
- ],
- ),
- );
- }
-}
-
-class _BackButton extends StatefulWidget {
- const _BackButton({Key? key}) : super(key: key);
-
- @override
- __BackButtonState createState() => __BackButtonState();
-}
-
-class __BackButtonState extends State<_BackButton> {
- @override
- Widget build(BuildContext context) {
- return AnimatedVisibility(
- duration: DesignConfig.lowAnimationDuration,
- isVisible: true,
- child: InkWrapper(
- borderRadius: DesignConfig.lowBorderRadius,
- onPressed: Navigator.of(context).pop,
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.splash,
- border: Border.all(color: Theme.of(context).colorScheme.border),
- borderRadius: DesignConfig.lowBorderRadius,
- ),
- child: const Icon(
- DidvanIcons.back_regular,
- size: 32,
- ),
- ),
- ),
- );
- }
}
diff --git a/lib/views/home/main/widgets/podcast_item.dart b/lib/views/home/main/widgets/podcast_item.dart
index 39069c4..597a9fd 100644
--- a/lib/views/home/main/widgets/podcast_item.dart
+++ b/lib/views/home/main/widgets/podcast_item.dart
@@ -69,26 +69,28 @@ class _MainPagePodcastItemState extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- SizedBox(
- width:
- MediaQuery.of(context).size.width * 2 / 3 - 60,
- child: AudioWidget(
- id: widget.content.id,
- audioUrl: widget.content.link,
- audioMetaData: StudioDetailsData(
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 12.0),
+ child: SizedBox(
+ child: AudioWidget(
id: widget.content.id,
- duration: widget.content.duration!,
- title: widget.content.title,
- description: '',
- image: widget.content.image,
- link: widget.content.link,
- iframe: null,
- createdAt: '',
- order: 1,
- marked: widget.content.marked,
- comments: 0,
- tags: [],
- type: 'podcast',
+ audioUrl: widget.content.link,
+ audioMetaData: StudioDetailsData(
+ id: widget.content.id,
+ duration: widget.content.duration!,
+ title: widget.content.title,
+ description: '',
+ image: widget.content.image,
+ link: widget.content.link,
+ iframe: null,
+ createdAt: '',
+ order: 1,
+ marked: widget.content.marked,
+ comments: 0,
+ tags: [],
+ type: 'podcast',
+ ),
),
),
),
diff --git a/lib/views/home/new_statistic/new_statistic.dart b/lib/views/home/new_statistic/new_statistic.dart
index 4c23bed..658a829 100644
--- a/lib/views/home/new_statistic/new_statistic.dart
+++ b/lib/views/home/new_statistic/new_statistic.dart
@@ -96,6 +96,9 @@ class _NewStatisticState extends State {
itemsInStatics(context, state, 3),
itemsInStatics(context, state, 4),
itemsInStatics(context, state, 5, hasDivider: false),
+ const SizedBox(
+ height: 24,
+ ),
],
),
),
@@ -135,6 +138,7 @@ class _NewStatisticState extends State {
physics: const ScrollPhysics(),
itemCount: state.contents[id].contents.length,
itemBuilder: (context, index) => StatMainCard(
+ id: id,
statistic: state.contents[id].contents[index],
),
),
diff --git a/lib/views/home/search/widgets/search_result_item.dart b/lib/views/home/search/widgets/search_result_item.dart
index d187b9a..31adb25 100644
--- a/lib/views/home/search/widgets/search_result_item.dart
+++ b/lib/views/home/search/widgets/search_result_item.dart
@@ -15,7 +15,7 @@ import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+import 'package:didvan/services/app_initalizer.dart';
class SearchResultItem extends StatelessWidget {
final OverviewData item;
diff --git a/lib/views/home/widgets/categories.dart b/lib/views/home/widgets/categories.dart
index c350827..92b4eea 100644
--- a/lib/views/home/widgets/categories.dart
+++ b/lib/views/home/widgets/categories.dart
@@ -6,7 +6,7 @@ import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+import 'package:didvan/services/app_initalizer.dart';
class MainCategories extends StatelessWidget {
const MainCategories({super.key});
diff --git a/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart b/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart
index 1641c2a..1e975c7 100644
--- a/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart
+++ b/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart
@@ -17,7 +17,7 @@ import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher.dart';
+import 'package:didvan/services/app_initalizer.dart';
class StudioDetailsWidget extends StatelessWidget {
final void Function(int id, bool value) onMarkChanged;
@@ -65,7 +65,7 @@ class StudioDetailsWidget extends StatelessWidget {
key: ValueKey(state.studio.id),
data: state.studio.description,
onAnchorTap: (href, _, __) =>
- launchUrl(Uri.parse(href!)),
+ launchUrlString(href!),
style: {
'*': Style(
direction: TextDirection.rtl,
diff --git a/lib/views/profile/profile.dart b/lib/views/profile/profile.dart
index b70b858..3844690 100644
--- a/lib/views/profile/profile.dart
+++ b/lib/views/profile/profile.dart
@@ -22,7 +22,7 @@ import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher.dart';
+import 'package:didvan/services/app_initalizer.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({Key? key}) : super(key: key);
@@ -222,7 +222,7 @@ class _ProfilePageState extends State {
icon: DidvanIcons.info_circle_regular,
title: 'معرفی دیدوان',
onTap: () =>
- launchUrl(Uri.parse('https://didvan.app/#info')),
+ launchUrlString('https://didvan.app/#info'),
),
const DidvanDivider(),
MenuOption(
@@ -232,12 +232,16 @@ class _ProfilePageState extends State {
state.showContactUs = !state.showContactUs;
state.update();
},
- trailing: Icon(
- state.showContactUs
- ? DidvanIcons.angle_up_regular
- : DidvanIcons.angle_down_regular,
- size: 18,
- color: Theme.of(context).colorScheme.title,
+ trailing: Row(
+ children: [
+ Icon(
+ state.showContactUs
+ ? DidvanIcons.angle_up_regular
+ : DidvanIcons.angle_down_regular,
+ size: 18,
+ color: Theme.of(context).colorScheme.title,
+ ),
+ ],
)),
AnimatedVisibility(
isVisible: state.showContactUs,
@@ -316,9 +320,8 @@ class _ProfilePageState extends State {
MenuOption(
icon: DidvanIcons.alert_regular,
title: 'حریم خصوصی',
- onTap: () => launchUrl(
- Uri.parse(
- 'https://didvan.app/terms-of-use#privacy'),
+ onTap: () => launchUrlString(
+ 'https://didvan.app/terms-of-use#privacy',
),
),
],
diff --git a/lib/views/web/web_view.dart b/lib/views/web/web_view.dart
new file mode 100644
index 0000000..05f0e0b
--- /dev/null
+++ b/lib/views/web/web_view.dart
@@ -0,0 +1,75 @@
+// ignore_for_file: library_private_types_in_public_api, deprecated_member_use
+
+import 'package:didvan/constants/assets.dart';
+import 'package:flutter/material.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+class WebView extends StatefulWidget {
+ final String src;
+ const WebView({Key? key, required this.src}) : super(key: key);
+
+ @override
+ _WebViewState createState() => _WebViewState();
+}
+
+class _WebViewState extends State {
+ late WebViewController controller;
+ bool loading = true;
+ int progress = 0;
+ @override
+ void initState() {
+ controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(
+ NavigationDelegate(
+ onProgress: (int progress) {
+ // Update loading bar.
+ setState(() {
+ this.progress = progress;
+ });
+ },
+ onPageStarted: (String url) {},
+ onPageFinished: (String url) async {
+ await Future.delayed(const Duration(seconds: 2));
+ setState(() {
+ if (progress == 100) loading = false;
+ });
+ },
+ onHttpError: (HttpResponseError error) {},
+ onWebResourceError: (WebResourceError error) {
+ // navigatorKey.currentState!.pop();
+ },
+ onNavigationRequest: (NavigationRequest request) {
+ // if (request.url.startsWith('https://www.youtube.com/')) {
+ // return NavigationDecision.prevent;
+ // }
+ return NavigationDecision.navigate;
+ },
+ ),
+ )
+ ..loadRequest(Uri.parse(widget.src));
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return WillPopScope(
+ onWillPop: () async {
+ if (await controller.canGoBack()) {
+ await controller.goBack();
+ return false;
+ } else {
+ return true;
+ }
+ },
+ child: loading
+ ? Center(
+ child: Image.asset(
+ Assets.loadingAnimation,
+ width: 60,
+ height: 60,
+ ),
+ )
+ : WebViewWidget(controller: controller));
+ }
+}
diff --git a/lib/views/widgets/back_button.dart b/lib/views/widgets/back_button.dart
new file mode 100644
index 0000000..e78bd20
--- /dev/null
+++ b/lib/views/widgets/back_button.dart
@@ -0,0 +1,40 @@
+// ignore_for_file: library_private_types_in_public_api
+
+import 'package:didvan/config/design_config.dart';
+import 'package:didvan/config/theme_data.dart';
+import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/views/widgets/animated_visibility.dart';
+import 'package:didvan/views/widgets/ink_wrapper.dart';
+import 'package:flutter/material.dart';
+
+class BackButton extends StatefulWidget {
+ const BackButton({Key? key}) : super(key: key);
+
+ @override
+ __BackButtonState createState() => __BackButtonState();
+}
+
+class __BackButtonState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedVisibility(
+ duration: DesignConfig.lowAnimationDuration,
+ isVisible: true,
+ child: InkWrapper(
+ borderRadius: DesignConfig.lowBorderRadius,
+ onPressed: Navigator.of(context).pop,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.splash,
+ border: Border.all(color: Theme.of(context).colorScheme.border),
+ borderRadius: DesignConfig.lowBorderRadius,
+ ),
+ child: const Icon(
+ DidvanIcons.back_regular,
+ size: 32,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart
index 500c0cc..05c3d4a 100644
--- a/lib/views/widgets/didvan/page_view.dart
+++ b/lib/views/widgets/didvan/page_view.dart
@@ -23,7 +23,7 @@ import 'package:didvan/views/widgets/item_title.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
-import 'package:url_launcher/url_launcher.dart';
+import 'package:didvan/services/app_initalizer.dart';
class DidvanPageView extends StatefulWidget {
final List items;
@@ -309,7 +309,7 @@ class _DidvanPageViewState extends State {
),
);
} else {
- launchUrl(Uri.parse(href));
+ launchUrlString(href);
}
},
style: {
diff --git a/lib/views/widgets/logo_app_bar.dart b/lib/views/widgets/logo_app_bar.dart
index ea3b626..1fc4d41 100644
--- a/lib/views/widgets/logo_app_bar.dart
+++ b/lib/views/widgets/logo_app_bar.dart
@@ -33,7 +33,7 @@ class LogoAppBar extends StatelessWidget implements PreferredSizeWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(20)),
color: Theme.of(context).colorScheme.surface,
- boxShadow: state.currentPageIndex == 1 || state.filtering
+ boxShadow: state.filtering
? null
: [
BoxShadow(
diff --git a/lib/views/widgets/overview/multitype.dart b/lib/views/widgets/overview/multitype.dart
index 0eb788d..2795148 100644
--- a/lib/views/widgets/overview/multitype.dart
+++ b/lib/views/widgets/overview/multitype.dart
@@ -19,7 +19,7 @@ import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
-import 'package:url_launcher/url_launcher_string.dart';
+import 'package:didvan/services/app_initalizer.dart';
class MultitypeOverview extends StatelessWidget {
final OverviewData item;
diff --git a/lib/views/widgets/skeleton_image.dart b/lib/views/widgets/skeleton_image.dart
index 8894ca1..f6912de 100644
--- a/lib/views/widgets/skeleton_image.dart
+++ b/lib/views/widgets/skeleton_image.dart
@@ -11,6 +11,8 @@ class SkeletonImage extends StatelessWidget {
final String imageUrl;
final double? width;
final double? height;
+ final double? pWidth;
+ final double? pHeight;
final BorderRadius? borderRadius;
final double? aspectRatio;
const SkeletonImage({
@@ -20,6 +22,8 @@ class SkeletonImage extends StatelessWidget {
this.aspectRatio,
this.width,
this.height,
+ this.pWidth,
+ this.pHeight,
}) : super(key: key);
@override
@@ -39,7 +43,10 @@ class SkeletonImage extends StatelessWidget {
imageUrl: imageUrl.startsWith('http')
? imageUrl
: RequestHelper.baseUrl + imageUrl,
- placeholder: (context, _) => const ShimmerPlaceholder(),
+ placeholder: (context, _) => ShimmerPlaceholder(
+ width: pWidth,
+ height: pHeight,
+ ),
),
),
);
diff --git a/pubspec.lock b/pubspec.lock
index 3ec0782..bc9c460 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -359,7 +359,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_cache_manager:
- dependency: transitive
+ dependency: "direct main"
description:
name: flutter_cache_manager
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
@@ -1074,22 +1074,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
- syncfusion_flutter_core:
- dependency: transitive
- description:
- name: syncfusion_flutter_core
- sha256: e17dcc7a1d0701e84d0a83c0040503cdcc6c72e44db0d733ab4c706dd5b8b9f8
- url: "https://pub.dev"
- source: hosted
- version: "25.2.7"
- syncfusion_flutter_sliders:
- dependency: transitive
- description:
- name: syncfusion_flutter_sliders
- sha256: "842a452fd73fd61fbebbff72d726ffea4cdaacf088ca2738aeaf7f4454b3861e"
- url: "https://pub.dev"
- source: hosted
- version: "25.2.7"
synchronized:
dependency: transitive
description:
@@ -1154,70 +1138,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
- url_launcher:
- dependency: "direct main"
- description:
- name: url_launcher
- sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
- url: "https://pub.dev"
- source: hosted
- version: "6.3.0"
- url_launcher_android:
- dependency: transitive
- description:
- name: url_launcher_android
- sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
- url: "https://pub.dev"
- source: hosted
- version: "6.3.2"
- url_launcher_ios:
- dependency: transitive
- description:
- name: url_launcher_ios
- sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
- url: "https://pub.dev"
- source: hosted
- version: "6.3.0"
- url_launcher_linux:
- dependency: transitive
- description:
- name: url_launcher_linux
- sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
- url: "https://pub.dev"
- source: hosted
- version: "3.1.1"
- url_launcher_macos:
- dependency: transitive
- description:
- name: url_launcher_macos
- sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
- url: "https://pub.dev"
- source: hosted
- version: "3.2.0"
- url_launcher_platform_interface:
- dependency: transitive
- description:
- name: url_launcher_platform_interface
- sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
- url: "https://pub.dev"
- source: hosted
- version: "2.3.2"
- url_launcher_web:
- dependency: transitive
- description:
- name: url_launcher_web
- sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
- url: "https://pub.dev"
- source: hosted
- version: "2.3.1"
- url_launcher_windows:
- dependency: transitive
- description:
- name: url_launcher_windows
- sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
- url: "https://pub.dev"
- source: hosted
- version: "3.1.1"
uuid:
dependency: transitive
description:
@@ -1306,14 +1226,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
- voice_message_package:
- dependency: "direct main"
- description:
- name: voice_message_package
- sha256: cd7a717751e60908d37624309c6995c1c62c1bab6b54f7d15c2d71289f0b0cb6
- url: "https://pub.dev"
- source: hosted
- version: "2.2.1"
wakelock_plus:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index c65f51b..461a21d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -57,12 +57,12 @@ dependencies:
bot_toast: ^4.0.1
flutter_secure_storage: ^8.0.0
flutter_html: ^3.0.0-alpha.2
- url_launcher: ^6.0.18
+ # url_launcher: ^6.0.18
audio_video_progress_bar: ^2.0.0
image_cropper: ^1.5.0
firebase_core: ^3.1.0
firebase_messaging: ^15.0.1
- webview_flutter: ^4.2.0
+ webview_flutter: ^4.8.0
expandable_bottom_sheet: ^1.1.1+1
permission_handler: ^11.0.0
# better_player:
@@ -89,9 +89,10 @@ dependencies:
flutter_markdown: ^0.7.3+1
file_picker: ^8.0.5
marquee: ^2.2.3
- voice_message_package: ^2.2.1
mime: ^1.0.2
path: any
+ flutter_cache_manager: any
+
dev_dependencies:
flutter_test:
sdk: flutter