From 341f9786a5f47e874bbeb2181f8f92541f1b1655 Mon Sep 17 00:00:00 2001 From: OkaykOrhmn Date: Tue, 10 Sep 2024 14:50:54 +0330 Subject: [PATCH] houshvan version 3 -- 20/06/1403 -- Rhmn --- lib/assets/images/features/ai-dark.svg | 8 + lib/assets/images/features/ai-light.svg | 8 + lib/constants/assets.dart | 2 +- lib/models/ai/bots_model.dart | 3 + lib/models/ai/chats_model.dart | 8 +- lib/models/ai/files_model.dart | 8 +- lib/models/news_details_data.dart | 2 +- lib/models/notification_message.dart | 18 +- lib/models/view/app_bar_data.dart | 2 +- lib/routes/route_generator.dart | 6 + lib/routes/routes.dart | 1 + lib/services/ai/ai_api_service.dart | 6 +- .../home_widget_repository.dart | 5 +- lib/services/app_initalizer.dart | 7 +- lib/services/media/media.dart | 2 +- lib/utils/action_sheet.dart | 46 + lib/utils/media.dart | 29 + lib/views/ai/ai.dart | 5 +- lib/views/ai/ai_chat_page.dart | 381 +++---- lib/views/ai/ai_chat_state.dart | 3 +- lib/views/ai/history_ai_chat_page.dart | 30 +- lib/views/ai/history_ai_chat_state.dart | 24 +- lib/views/ai/widgets/ai_message_bar.dart | 955 ++++++++++-------- lib/views/ai/widgets/audio_wave.dart | 306 ++++++ lib/views/ai/widgets/voice_message_view.dart | 294 ------ .../authentication/screens/username.dart | 15 +- lib/views/direct/direct_state.dart | 1 + lib/views/direct/widgets/audio_widget.dart | 70 +- .../widgets/downloadable_audio_widget.dart | 30 + lib/views/direct/widgets/message.dart | 4 +- lib/views/direct/widgets/message_box.dart | 26 +- lib/views/home/home.dart | 15 +- lib/views/home/home_state.dart | 2 +- lib/views/home/main/main_page.dart | 3 +- lib/views/home/main/main_page_state.dart | 3 +- lib/views/home/main/widgets/banner.dart | 71 +- lib/views/home/main/widgets/podcast_item.dart | 40 +- .../home/new_statistic/new_statistic.dart | 4 + .../search/widgets/search_result_item.dart | 2 +- lib/views/home/widgets/categories.dart | 2 +- .../widgets/studio_details_widget.dart | 4 +- lib/views/profile/profile.dart | 25 +- lib/views/web/web_view.dart | 75 ++ lib/views/widgets/back_button.dart | 40 + lib/views/widgets/didvan/page_view.dart | 4 +- lib/views/widgets/logo_app_bar.dart | 2 +- lib/views/widgets/overview/multitype.dart | 2 +- lib/views/widgets/skeleton_image.dart | 9 +- pubspec.lock | 90 +- pubspec.yaml | 7 +- 50 files changed, 1492 insertions(+), 1213 deletions(-) create mode 100644 lib/assets/images/features/ai-dark.svg create mode 100644 lib/assets/images/features/ai-light.svg create mode 100644 lib/utils/media.dart create mode 100644 lib/views/ai/widgets/audio_wave.dart delete mode 100644 lib/views/ai/widgets/voice_message_view.dart create mode 100644 lib/views/direct/widgets/downloadable_audio_widget.dart create mode 100644 lib/views/web/web_view.dart create mode 100644 lib/views/widgets/back_button.dart 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