diff --git a/lib/main.dart b/lib/main.dart index 1654180..24a782b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ // ignore_for_file: deprecated_member_use +import 'dart:async'; + import 'package:android_intent_plus/android_intent.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:didvan/config/theme_data.dart'; @@ -12,9 +14,11 @@ import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/route_generator.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/app_home_widget/home_widget_repository.dart'; +import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/notification/firebase_api.dart'; import 'package:didvan/services/notification/notification_service.dart'; +import 'package:didvan/utils/my_custom_scroll_behavior.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/podcasts/podcasts_state.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; @@ -23,6 +27,7 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:get/get.dart'; import 'package:home_widget/home_widget.dart'; import 'package:provider/provider.dart'; @@ -56,21 +61,28 @@ Future _backgroundCallbackHomeWidget(Uri? uri) async { } void main() async { - WidgetsFlutterBinding.ensureInitialized(); - try { - HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget); - HomeWidget.registerInteractivityCallback(_backgroundCallbackHomeWidget); + await runZonedGuarded(() async { + WidgetsFlutterBinding.ensureInitialized(); - await NotificationService.initializeNotification(); - FirebaseMessaging.onBackgroundMessage(_initPushNotification); - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform); - await FirebaseApi().initNotification(); - } catch (e) { - debugPrint; - } + try { + if (!kIsWeb) { + HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget); + HomeWidget.registerInteractivityCallback(_backgroundCallbackHomeWidget); + await NotificationService.initializeNotification(); + } - runApp(const Didvan()); + FirebaseMessaging.onBackgroundMessage(_initPushNotification); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform); + await FirebaseApi().initNotification(); + } catch (e) { + debugPrint; + } + + runApp(const Didvan()); + }, (error, stack) async { + error.printError(); + }); } class Didvan extends StatefulWidget { @@ -88,10 +100,12 @@ class _DidvanState extends State with WidgetsBindingObserver { @override void initState() { - NotificationService.startListeningNotificationEvents(); + if (!kIsWeb) { + NotificationService.startListeningNotificationEvents(); + } + WidgetsBinding.instance.addObserver(this); super.initState(); - WidgetsBinding.instance.addObserver(this); } @override @@ -108,15 +122,17 @@ class _DidvanState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) async { - if (state == AppLifecycleState.resumed) { - var r = await HomeWidget.getWidgetData("cRoute", defaultValue: ''); - if (r!.toString() != Routes.splash) { - await HomeWidgetRepository.decideWhereToGo(); + if (!kIsWeb) { + if (state == AppLifecycleState.resumed) { + var r = await HomeWidget.getWidgetData("cRoute", defaultValue: ''); + if (r!.toString() != Routes.splash) { + await HomeWidgetRepository.decideWhereToGo(); - NotificationMessage? data = HomeWidgetRepository.data; + NotificationMessage? data = HomeWidgetRepository.data; - if (data != null) { - await HomeWidgetRepository.decideWhereToGoNotif(); + if (data != null) { + await HomeWidgetRepository.decideWhereToGoNotif(); + } } } } @@ -150,6 +166,7 @@ class _DidvanState extends State with WidgetsBindingObserver { color: Theme.of(context).colorScheme.surface, child: SafeArea( child: MaterialApp( + scrollBehavior: MyCustomScrollBehavior(), navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, title: 'Didvan', diff --git a/lib/models/ai/chats_model.dart b/lib/models/ai/chats_model.dart index fb19b48..c797586 100644 --- a/lib/models/ai/chats_model.dart +++ b/lib/models/ai/chats_model.dart @@ -1,4 +1,5 @@ import 'package:didvan/models/ai/bots_model.dart'; +import 'package:didvan/models/ai/files_model.dart'; class ChatsModel { int? id; @@ -97,6 +98,7 @@ class Prompts { bool? finished; bool? error; bool? audio; + FilesModel? fileLocal; Prompts({ this.id, @@ -108,8 +110,9 @@ class Prompts { this.createdAt, this.finished, this.error, + this.fileLocal, this.audio, - }); + }) {} Prompts.fromJson(Map json) { id = json['id']; @@ -141,6 +144,7 @@ class Prompts { String? text, String? file, String? fileName, + FilesModel? fileLocal, String? role, String? createdAt, bool? finished, @@ -152,6 +156,7 @@ class Prompts { text: text ?? this.text, file: file ?? this.file, fileName: fileName ?? this.fileName, + fileLocal: fileLocal ?? this.fileLocal, role: role ?? this.role, createdAt: createdAt ?? this.createdAt, finished: finished ?? this.finished, diff --git a/lib/models/ai/files_model.dart b/lib/models/ai/files_model.dart index 61b0751..4a92c06 100644 --- a/lib/models/ai/files_model.dart +++ b/lib/models/ai/files_model.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as p; @@ -9,14 +10,38 @@ class FilesModel { late String extname; late File main; final bool isRecorded; - late bool isAudio; - late bool isImage; + final bool? audio; + final bool? image; + final bool? network; + final Uint8List? bytes; - FilesModel(this.path, {this.isRecorded = false}) { - basename = p.basename(path); + FilesModel( + this.path, { + final String? name, + this.isRecorded = false, + this.audio, + this.image, + this.network, + this.bytes, + }) { + basename = name ?? p.basename(path); extname = p.extension(path); main = File(path); - isAudio = lookupMimeType(path)?.startsWith('audio/') ?? false; - isImage = lookupMimeType(path)?.startsWith('image/') ?? false; + print(isAudio()); + print(isImage()); + print(isNetwork()); + print(path); + } + + bool isAudio() { + return audio ?? lookupMimeType(path)?.startsWith('audio/') ?? false; + } + + bool isImage() { + return image ?? lookupMimeType(path)?.startsWith('image/') ?? false; + } + + bool isNetwork() { + return network ?? path.startsWith('blob:') || path.startsWith('/uploads'); } } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 6ceb0d9..6c32ec3 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -67,7 +67,9 @@ import '../views/notification_time/notification_time.dart'; class RouteGenerator { static Route generateRoute(RouteSettings settings) { - HomeWidget.saveWidgetData("cRoute", settings.name!); + if (!kIsWeb) { + HomeWidget.saveWidgetData("cRoute", settings.name!); + } switch (settings.name) { case Routes.splash: return _createRoute( diff --git a/lib/services/ai/ai_api_service.dart b/lib/services/ai/ai_api_service.dart index 7f68fd2..60f74c4 100644 --- a/lib/services/ai/ai_api_service.dart +++ b/lib/services/ai/ai_api_service.dart @@ -4,7 +4,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:didvan/models/ai/files_model.dart'; import 'package:didvan/services/storage/storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get_connect/http/src/multipart/multipart_file.dart'; import 'package:http/http.dart' as http; import 'package:mime/mime.dart'; import 'package:path/path.dart' as p; @@ -17,7 +20,7 @@ class AiApiService { {required final String url, required final String message, final int? chatId, - final File? file, + final FilesModel? file, final bool? edite}) async { final headers = { "Authorization": "Bearer ${await StorageService.getValue(key: 'token')}", @@ -35,26 +38,69 @@ class AiApiService { request.fields['edit'] = edite.toString().toLowerCase(); } if (file != null) { - int length = 0; - try { - length = await file.length(); - // ... - } catch (e) { - // Handle the error or return an error response + // final file = file; + // final filePath = file.path; + // final mimeType = filePath != null ? lookupMimeType(filePath) : null; + // final contentType = mimeType != null ? MediaType.parse(mimeType) : null; + + // final fileReadStream = file.readStream; + // if (fileReadStream == null) { + // throw Exception('Cannot read file from null stream'); + // } + // final stream = http.ByteStream(fileReadStream); + // final multipartFile = http.MultipartFile( + // 'file', + // stream, + // file.size, + // filename: file.name, + // contentType: contentType, + // ); + // request.files.add(multipartFile); + Uint8List bytes; + if (kIsWeb) { + if (file.bytes != null) { + bytes = file.bytes!; + } else { + final Uri audioUri = Uri.parse(file.path); + final http.Response audioResponse = await http.get(audioUri); + bytes = audioResponse.bodyBytes; + } + request.files.add(http.MultipartFile.fromBytes( + 'file', + bytes, + filename: file.isAudio() + ? 'wav' + : file.isImage() + ? 'png' + : 'pdf', // You can set a filename here + contentType: parser.MediaType.parse(file.isAudio() + ? 'audio/mpeg' + : file.isImage() + ? 'image/png' + : 'application/pdf'), // Set the MIME type + ) // Use MediaType.parse to parse the MIME type + ); + } else { + int length = 0; + try { + length = await file.main.length(); + // ... + } catch (e) { + // Handle the error or return an error response + } + String? mimeType = lookupMimeType( + file.path); // Use MIME type instead of file extension + mimeType ??= 'application/octet-stream'; + if (mimeType.startsWith('audio')) { + mimeType = 'audio/${p.extension(file.path).replaceAll('.', '')}'; + } + request.files.add( + http.MultipartFile('file', file.main.readAsBytes().asStream(), length, + filename: file.basename, + contentType: parser.MediaType.parse( + mimeType)), // Use MediaType.parse to parse the MIME type + ); } - String basename = p.basename(file.path); - String? mimeType = - lookupMimeType(file.path); // Use MIME type instead of file extension - mimeType ??= 'application/octet-stream'; - if (mimeType.startsWith('audio')) { - mimeType = 'audio/${p.extension(file.path).replaceAll('.', '')}'; - } - request.files.add( - http.MultipartFile('file', file.readAsBytes().asStream(), length, - filename: basename, - contentType: parser.MediaType.parse( - mimeType)), // Use MediaType.parse to parse the MIME type - ); } // print("req: ${request.files}"); @@ -76,7 +122,7 @@ class AiApiService { // Handle other non-200 responses throw Exception('Failed to load data'); } else { - return response.stream; + return response.stream.asBroadcastStream(); } } catch (e) { // Handle any other errors diff --git a/lib/services/app_home_widget/home_widget_repository.dart b/lib/services/app_home_widget/home_widget_repository.dart index 00d64a2..942ad0c 100644 --- a/lib/services/app_home_widget/home_widget_repository.dart +++ b/lib/services/app_home_widget/home_widget_repository.dart @@ -17,7 +17,7 @@ import '../network/request_helper.dart'; class HomeWidgetRepository { static Future fetchWidget() async { - RequestService.token = await StorageService.getValue(key: 'token'); + // RequestService.token = await StorageService.getValue(key: 'token'); final service = RequestService( RequestHelper.widgetNews(), ); @@ -90,7 +90,7 @@ class HomeWidgetRepository { ); if (data.link!.startsWith('http')) { - launchUrlString( + openInWebView( '${data.link}', mode: LaunchMode.inAppWebView, ); @@ -219,7 +219,7 @@ class HomeWidgetRepository { } } else { if (data.link!.startsWith('http')) { - launchUrlString( + openInWebView( '${data.link}', mode: LaunchMode.inAppWebView, ); diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart index 240f88f..a55f951 100644 --- a/lib/services/app_initalizer.dart +++ b/lib/services/app_initalizer.dart @@ -13,10 +13,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher_string.dart'; enum LaunchMode { inAppWebView } -void launchUrlString(String src, {dynamic mode}) { +void openInWebView(String src, {dynamic mode}) { + if (kIsWeb) { + launchUrlString(src); + return; + } navigatorKey.currentState!.pushNamed(Routes.web, arguments: src); } diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 4f4b331..53bb709 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -1,8 +1,10 @@ import 'package:didvan/constants/assets.dart'; +import 'package:didvan/main.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/studio_details_data.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/providers/media.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/services/storage/storage.dart'; @@ -121,6 +123,7 @@ class MediaService { return result; } catch (e) { e.printError(info: 'Pick PDF Fail'); + // navigatorKey.currentState!.pop(); return null; } } diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart index 79e3e1a..9f440a1 100644 --- a/lib/utils/action_sheet.dart +++ b/lib/utils/action_sheet.dart @@ -410,11 +410,13 @@ class ActionSheetUtils { ? ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: Image.file(File(image))) - : SkeletonImage( - width: min(MediaQuery.of(context).size.width, - MediaQuery.of(context).size.height), - imageUrl: image, - ), + : image.startsWith('blob:') + ? ClipRRect( + borderRadius: DesignConfig.lowBorderRadius, + child: Image.network(image)) + : SkeletonImage( + imageUrl: image, + ), ), ), ), diff --git a/lib/utils/media.dart b/lib/utils/media.dart index 434ea95..4c182eb 100644 --- a/lib/utils/media.dart +++ b/lib/utils/media.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Get screen media. @@ -7,9 +8,9 @@ final MediaQueryData media = /// This extention help us to make widget responsive. extension NumberParsing on num { - double w() => this * media.size.width / 100; + double w() => this * media.size.width / (kIsWeb ? 250 : 100); - double h() => this * media.size.height / 100; + double h() => this * media.size.height / (kIsWeb ? 550 : 100); } /// diff --git a/lib/utils/my_custom_scroll_behavior.dart b/lib/utils/my_custom_scroll_behavior.dart new file mode 100644 index 0000000..8a87a3c --- /dev/null +++ b/lib/utils/my_custom_scroll_behavior.dart @@ -0,0 +1,11 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class MyCustomScrollBehavior extends MaterialScrollBehavior { + // Override behavior methods and getters like dragDevices + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} diff --git a/lib/views/ai/ai.dart b/lib/views/ai/ai.dart index 2bdfa1f..36024e5 100644 --- a/lib/views/ai/ai.dart +++ b/lib/views/ai/ai.dart @@ -63,8 +63,12 @@ class _AiState extends State { Icon( DidvanIcons.ai_solid, size: MediaQuery.sizeOf(context).width / 5, + color: Theme.of(context).colorScheme.title, + ), + DidvanText( + 'هوشان', + color: Theme.of(context).colorScheme.title, ), - const DidvanText('هوشان'), const SizedBox( height: 24, ), @@ -74,8 +78,12 @@ class _AiState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(DidvanIcons.caret_down_solid), - Text(bot.name.toString()), + Icon( + DidvanIcons.caret_down_solid, + color: Theme.of(context).colorScheme.title, + ), + DidvanText(bot.name.toString(), + color: Theme.of(context).colorScheme.title), ], ), ), @@ -177,7 +185,10 @@ class _AiState extends State { topLeft: Radius.circular(12), bottomLeft: Radius.circular(12)), boxShadow: DesignConfig.defaultShadow), - child: const Icon(DidvanIcons.angle_left_light), + child: Icon( + DidvanIcons.angle_left_light, + color: Theme.of(context).colorScheme.title, + ), )), ) ], diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart index 40a30b5..659c98c 100644 --- a/lib/views/ai/ai_chat_page.dart +++ b/lib/views/ai/ai_chat_page.dart @@ -21,6 +21,7 @@ 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -324,8 +325,8 @@ class _AiChatPageState extends State { Widget messageBubble(Prompts message, BuildContext context, AiChatState state, int index, int mIndex) { - FilesModel? file = - message.file == null ? null : FilesModel(message.file.toString()); + FilesModel? file = message.fileLocal ?? + (message.file == null ? null : FilesModel(message.file.toString())); MarkdownStyleSheet defaultMarkdownStyleSheet = MarkdownStyleSheet( pPadding: const EdgeInsets.all(0.8), @@ -418,13 +419,13 @@ class _AiChatPageState extends State { children: [ if (message.role.toString().contains('user') && file != null) - file.isAudio + file.isAudio() ? Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), child: AudioWave(file: file.path), ) - : file.isImage + : file.isImage() ? Padding( padding: const EdgeInsets.all(8.0), child: messageImage(file), @@ -605,7 +606,7 @@ class _AiChatPageState extends State { stop: const Duration(seconds: 3), ), ), - if (state.file != null) + if (state.file != null && !kIsWeb) FutureBuilder( future: state.file!.main.length(), builder: (context, snapshot) { @@ -627,14 +628,18 @@ class _AiChatPageState extends State { Widget messageImage(FilesModel file) { return GestureDetector( - onTap: () => ActionSheetUtils(context).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, - ) + onTap: () => ActionSheetUtils(context) + .openInteractiveViewer(context, file.path, !file.isNetwork()), + child: file.isNetwork() + ? file.path.startsWith('blob:') + ? ClipRRect( + borderRadius: DesignConfig.lowBorderRadius, + child: Image.network(file.path)) + : 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 f65c9e0..82ad607 100644 --- a/lib/views/ai/ai_chat_state.dart +++ b/lib/views/ai/ai_chat_state.dart @@ -1,6 +1,11 @@ +import 'dart:async'; import 'dart:convert'; - import 'package:didvan/main.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:provider/provider.dart'; import 'package:didvan/models/ai/bots_model.dart'; import 'package:didvan/models/ai/chats_model.dart'; import 'package:didvan/models/ai/files_model.dart'; @@ -14,15 +19,10 @@ import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:persian_number_utility/persian_number_utility.dart'; -import 'package:provider/provider.dart'; class AiChatState extends CoreProvier { ValueNotifier> messageOnstream = - ValueNotifier(const Stream.empty()); + ValueNotifier(const Stream.empty().asBroadcastStream()); List messages = []; bool onResponsing = false; bool loading = false; @@ -34,6 +34,11 @@ class AiChatState extends CoreProvier { FilesModel? file; TextEditingController message = TextEditingController(); + @override + void dispose() { + super.dispose(); + } + Future _scrolledEnd() async { WidgetsBinding.instance.addPostFrameCallback((_) async { await scrollController.animateTo( @@ -53,7 +58,6 @@ class AiChatState extends CoreProvier { await ActionSheetUtils(navigatorKey.currentContext!).showAlert(AlertData( message: 'خطا در برقراری ارتباط', aLertType: ALertType.error)); - update(); } @@ -94,21 +98,16 @@ class AiChatState extends CoreProvier { messages.add(MessageModel( dateTime: prompt.createdAt.toString(), prompts: [prompt])); } - // chats.add("chats: $chats"); } appState = AppState.idle; loading = false; onResponsing = false; - - // Add this code to scroll to maxScrollExtent after the ListView is built update(); - return; } appState = AppState.failed; loading = false; onResponsing = false; - update(); } @@ -122,28 +121,22 @@ class AiChatState extends CoreProvier { await service.put(); if (service.isSuccess) { appState = AppState.idle; - - // Add this code to scroll to maxScrollExtent after the ListView is built if (chat == null) { chat = ChatsModel(id: chatId, placeholder: placeholder); } else { chat = chat!.copyWith(placeholder: placeholder); } changingPlaceHolder.value = false; - update(); - return; } appState = AppState.failed; changingPlaceHolder.value = false; - update(); } Future postMessage(BotsModel bot) async { onResponsing = true; - update(); String message = messages.last.prompts.last.text!; @@ -161,27 +154,50 @@ class AiChatState extends CoreProvier { url: '/${bot.id}/${bot.name}'.toLowerCase(), message: message, chatId: chatId, - file: file?.main, + file: file, edite: isEdite); final res = await AiApiService.getResponse(req).catchError((e) { _onError(e); - return e; + // return e; }); String responseMessgae = ''; String dataMessgae = ''; file = null; update(); - var r = res.listen((value) async { - var str = utf8.decode(value); - if (str.contains('{{{')) { - dataMessgae += str; - update(); - return; - } - responseMessgae += str; - messageOnstream.value = Stream.value(responseMessgae); + final stream = res + .transform(utf8.decoder) + .transform(const LineSplitter()); // <--- Add this line + + final r = stream.listen((str) { + // var str = utf8.decode(value); + if (!kIsWeb) { + if (str.contains('{{{')) { + dataMessgae += str; + update(); + return; + } + } + + responseMessgae += str; + if (kIsWeb) { + try { + int startIndex = responseMessgae.indexOf('{{{'); +// + 3 to include the }}} characters + + String slicedText = + responseMessgae.substring(startIndex, responseMessgae.length); + dataMessgae = slicedText; + responseMessgae = responseMessgae.replaceAll(dataMessgae, ''); + print( + "slicedText: $slicedText"); // Output: {{{ HUMAN_MESSAGE_ID: 2028, AI_MESSAGE_ID: 2029 }}} + } catch (e) { + e.printError(); + } + } + messageOnstream.value = Stream.value(responseMessgae); + print("responseMessgae: $str"); // update(); }); @@ -189,7 +205,6 @@ class AiChatState extends CoreProvier { if (chatId == null) { final service = await getChatId(); navigatorKey.currentContext!.read().getChats(); - if (!service.isSuccess) { _onError(null); return; @@ -201,7 +216,6 @@ class AiChatState extends CoreProvier { messages.last.prompts.last = messages.last.prompts.last.copyWith(error: true); messageOnstream.value = const Stream.empty(); - update(); _scrolledEnd(); return; @@ -214,10 +228,8 @@ class AiChatState extends CoreProvier { aiMessageId = data['AI_MESSAGE_ID']; } catch (e) { e.printError(); - return; } - // Access the values messages.last.prompts.last = messages.last.prompts.last .copyWith(finished: true, text: responseMessgae, id: aiMessageId); if (messages.last.prompts.length > 2) { @@ -241,7 +253,7 @@ class AiChatState extends CoreProvier { _scrolledEnd(); }); - r.onError(_onError); + r.onError((e) => _onError(e)); } Future deleteMessage(int id, int mIndex, int index) async { @@ -253,17 +265,13 @@ class AiChatState extends CoreProvier { } else { messages[mIndex].prompts.removeAt(index); } - appState = AppState.idle; - update(); - return; } appState = AppState.failed; await ActionSheetUtils(navigatorKey.currentContext!).showAlert(AlertData( message: 'خطا در برقراری ارتباط', aLertType: ALertType.error)); - update(); } } diff --git a/lib/views/ai/widgets/ai_message_bar.dart b/lib/views/ai/widgets/ai_message_bar.dart index b16e2b2..953e6b7 100644 --- a/lib/views/ai/widgets/ai_message_bar.dart +++ b/lib/views/ai/widgets/ai_message_bar.dart @@ -27,6 +27,7 @@ import 'package:get/get.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; import 'package:record/record.dart'; @@ -80,6 +81,7 @@ class AiMessageBar extends StatefulWidget { class _AiMessageBarState extends State { final ValueNotifier messageText = ValueNotifier(''); bool openAttach = false; + String? path; @override void initState() { @@ -159,8 +161,24 @@ class _AiMessageBarState extends State { FilePickerResult? result = await MediaService.pickPdfFile(); if (result != null) { - state.file = - FilesModel(result.files.single.path!); + // if (kIsWeb) { + // Uint8List bytes = result.files.first + // .bytes!; // Access the bytes property + + // File file = File.fromRawPath(bytes); + // state.file = FilesModel(file.path, + // name: result.files.first.name, + // bytes: bytes, + // audio: false, + // image: false); + // print(result.files.first.name); + // } else { + state.file = FilesModel( + result.files.single.path!, + audio: false, + image: false); + // } + openAttach = false; } Future.delayed( @@ -214,8 +232,10 @@ class _AiMessageBarState extends State { return; } state.file = kIsWeb - ? FilesModel(pickedFile.path) - : FilesModel(file!.path); + ? FilesModel(pickedFile.path, + image: true, audio: false) + : FilesModel(file!.path, + image: true, audio: false); openAttach = false; await Future.delayed( Duration.zero, @@ -231,11 +251,28 @@ class _AiMessageBarState extends State { color: Colors.indigoAccent, click: () async { MediaService.onLoadingPickFile(context); + FilePickerResult? result = await MediaService.pickAudioFile(); if (result != null) { - state.file = - FilesModel(result.files.single.path!); + if (kIsWeb) { + Uint8List bytes = result.files.first + .bytes!; // Access the bytes property + + File file = File.fromRawPath(bytes); + + state.file = FilesModel(file.path, + name: result.files.first.name, + bytes: bytes, + audio: true, + image: false); + print(result.files.first.name); + } else { + state.file = FilesModel( + result.files.single.path!, + audio: true, + image: false); + } openAttach = false; } await Future.delayed( @@ -290,13 +327,16 @@ class _AiMessageBarState extends State { icon: DidvanIcons .stop_circle_solid, click: () async { - final path = - await record - .stop(); + path = await record + .stop(); + print( + "pathhhhhhh is: $path"); state.file = FilesModel( path.toString(), - isRecorded: true); + isRecorded: true, + audio: true, + image: false); _timer.cancel(); _countTimer.value = 0; state.update(); @@ -318,15 +358,29 @@ class _AiMessageBarState extends State { click: () async { if (await record .hasPermission()) { - Directory? - downloadDir = - await getApplicationDocumentsDirectory(); + try { + String path = + ''; +// + if (!kIsWeb) { + Directory? + downloadDir = + await getApplicationDocumentsDirectory(); + path = p.join( + downloadDir + .path, + '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.wav'); + } - record.start( - const RecordConfig(), - path: - '${downloadDir.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a'); - startTimer(); + record.start( + const RecordConfig(), + path: + path); + startTimer(); + } catch (e) { + print( + 'Error starting recording: $e'); + } } }, ) @@ -381,12 +435,8 @@ class _AiMessageBarState extends State { .file ?.path, fileName: state - .file == - null - ? null - : p.basename(state - .file! - .path), + .file + ?.basename, finished: true, role: 'user', @@ -417,10 +467,11 @@ class _AiMessageBarState extends State { file: state .file ?.path, - fileName: state.file == - null - ? null - : p.basename(state.file!.path), + fileName: state + .file + ?.basename, + fileLocal: + state.file, role: 'user', createdAt: DateTime.now() @@ -715,10 +766,6 @@ class _AiMessageBarState extends State { AnimatedVisibility fileContainer() { final state = context.watch(); - String basename = ''; - if (state.file != null) { - basename = p.basename(state.file!.path); - } return AnimatedVisibility( isVisible: state.file != null && !state.file!.isRecorded, duration: DesignConfig.lowAnimationDuration, @@ -731,16 +778,21 @@ class _AiMessageBarState extends State { margin: const EdgeInsets.only(bottom: 8, left: 12, right: 12), child: Row( children: [ - state.file != null && state.file!.isImage + 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, - ))) + child: state.file!.isNetwork() + ? Image.network( + state.file!.path, + fit: BoxFit.cover, + ) + : Image.file( + state.file!.main, + fit: BoxFit.cover, + ))) : const Icon(Icons.file_copy), const SizedBox( width: 12, @@ -752,12 +804,12 @@ class _AiMessageBarState extends State { SizedBox( height: 24, child: MarqueeText( - text: basename, + text: state.file != null ? state.file!.basename : '', style: const TextStyle(fontSize: 14), stop: const Duration(seconds: 3), ), ), - if (state.file != null) + if (state.file != null && !kIsWeb) FutureBuilder( future: state.file!.main.length(), builder: (context, snapshot) { diff --git a/lib/views/ai/widgets/audio_wave.dart b/lib/views/ai/widgets/audio_wave.dart index 4b0dede..884272a 100644 --- a/lib/views/ai/widgets/audio_wave.dart +++ b/lib/views/ai/widgets/audio_wave.dart @@ -27,7 +27,7 @@ class AudioWave extends StatefulWidget { class _AudioWaveState extends State { final int itemCount = 35; - final AudioPlayer audioPlayer = AudioPlayer(); + final AudioPlayer audioPlayer = AudioPlayer()..setPitch(1); final ValueNotifier> randoms = ValueNotifier([]); final ValueNotifier> randomsDisable = ValueNotifier([]); @@ -40,7 +40,9 @@ class _AudioWaveState extends State { void initState() { super.initState(); try { - init(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await init(); + }); listeners(); } catch (e) { if (kDebugMode) { @@ -54,19 +56,23 @@ class _AudioWaveState extends State { void setRandoms() { for (var i = 0; i < itemCount; i++) { randoms.value.add(0); - randomsDisable.value.add(5.74.w() * Random().nextDouble() + .26.w()); + randomsDisable.value.add(2 + Random().nextDouble() * (42 - 2)); } } Future init() async { try { final path = widget.file; - if (widget.file.startsWith('/uploads')) { - final audioSource = LockCachingAudioSource(Uri.parse( + if (widget.file.startsWith('blob:')) { + totalDuration = await audioPlayer + .setAudioSource(AudioSource.uri(Uri.parse(path))) ?? + Duration.zero; + } else if (widget.file.startsWith('/uploads')) { + final audioSource = AudioSource.uri(Uri.parse( '${RequestHelper.baseUrl + path}?accessToken=${RequestService.token}')); - await audioSource.request(); - totalDuration = - await audioPlayer.setAudioSource(audioSource) ?? Duration.zero; + totalDuration = await audioPlayer.setUrl( + '${RequestHelper.baseUrl + path}?accessToken=${RequestService.token}') ?? + Duration.zero; } else { totalDuration = await audioPlayer.setFilePath(path) ?? Duration.zero; } @@ -114,7 +120,8 @@ class _AudioWaveState extends State { } @override - void dispose() { + void dispose() async { + await audioPlayer.stop(); audioPlayer.dispose(); super.dispose(); } @@ -215,7 +222,7 @@ class _AudioWaveState extends State { Opacity( opacity: 0, child: Container( - width: 50.5.w(), + width: 12, color: Colors.transparent .withOpacity(1), child: Theme( @@ -302,8 +309,8 @@ class _AudioWaveState extends State { children: values .map( (e) => Container( - margin: EdgeInsets.symmetric(horizontal: .2.w()), - width: .56.w(), + margin: const EdgeInsets.symmetric(horizontal: 1), + width: 2, height: e, decoration: BoxDecoration( borderRadius: BorderRadius.circular(1000), diff --git a/lib/views/authentication/authentication_state.dart b/lib/views/authentication/authentication_state.dart index c9f74af..5a483ba 100644 --- a/lib/views/authentication/authentication_state.dart +++ b/lib/views/authentication/authentication_state.dart @@ -6,6 +6,7 @@ import 'package:didvan/providers/user.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/foundation.dart'; import 'package:home_widget/home_widget.dart'; class AuthenticationState extends CoreProvier { @@ -52,7 +53,9 @@ class AuthenticationState extends CoreProvier { final token = service.result['token']; await userProvider.setAndGetToken(newToken: token); RequestService.token = token; - HomeWidget.saveWidgetData("token", token.toString()); + if (!kIsWeb) { + HomeWidget.saveWidgetData("token", token.toString()); + } await userProvider.getUserInfo(); appState = AppState.idle; return token; diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart index bedce95..16cde21 100644 --- a/lib/views/authentication/screens/username.dart +++ b/lib/views/authentication/screens/username.dart @@ -79,7 +79,7 @@ class _UsernameInputState extends State { .bodySmall! .copyWith(color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer() - ..onTap = () => launchUrlString( + ..onTap = () => openInWebView( 'https://didvan.app/terms-of-use#conditions', ), ), @@ -91,7 +91,7 @@ class _UsernameInputState extends State { .bodySmall! .copyWith(color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer() - ..onTap = () => launchUrlString( + ..onTap = () => openInWebView( 'https://didvan.app/terms-of-use#privacy', ), ), diff --git a/lib/views/customize_category/customize_category_state.dart b/lib/views/customize_category/customize_category_state.dart index 7c5a4f9..b48578a 100644 --- a/lib/views/customize_category/customize_category_state.dart +++ b/lib/views/customize_category/customize_category_state.dart @@ -1,6 +1,7 @@ import 'package:didvan/models/customize_categories/favorites_response.dart'; import 'package:didvan/models/enums.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import '../../models/view/alert_data.dart'; import '../../providers/core.dart'; @@ -100,7 +101,9 @@ class CustomizeCategoryState extends CoreProvier { ); appState = AppState.idle; - await HomeWidgetRepository.fetchWidget(); + if (!kIsWeb) { + await HomeWidgetRepository.fetchWidget(); + } return; } @@ -134,7 +137,9 @@ class CustomizeCategoryState extends CoreProvier { ); appState = AppState.idle; - await HomeWidgetRepository.fetchWidget(); + if (!kIsWeb) { + await HomeWidgetRepository.fetchWidget(); + } return; } diff --git a/lib/views/home/home.dart b/lib/views/home/home.dart index 3e7fbde..4e9d5f5 100644 --- a/lib/views/home/home.dart +++ b/lib/views/home/home.dart @@ -32,6 +32,7 @@ import 'package:didvan/views/widgets/didvan/bnb.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -52,7 +53,9 @@ class _HomeState extends State @override void initState() { - NotificationService.startListeningNotificationEvents(); + if (!kIsWeb) { + NotificationService.startListeningNotificationEvents(); + } final state = context.read(); DesignConfig.updateSystemUiOverlayStyle(); @@ -67,16 +70,17 @@ class _HomeState extends State context.read().getBots(); } }); - - Future.delayed(Duration.zero, () { - HomeWidgetRepository.fetchWidget(); - HomeWidgetRepository.decideWhereToGo(); - NotificationMessage? data = HomeWidgetRepository.data; - if (data != null) { - HomeWidgetRepository.decideWhereToGoNotif(); - } - AppInitializer.handleCLick(state, _tabController); - }); + if (!kIsWeb) { + Future.delayed(Duration.zero, () { + HomeWidgetRepository.fetchWidget(); + HomeWidgetRepository.decideWhereToGo(); + NotificationMessage? data = HomeWidgetRepository.data; + if (data != null) { + HomeWidgetRepository.decideWhereToGoNotif(); + } + AppInitializer.handleCLick(state, _tabController); + }); + } state.refresh(); context.read().addListener(() { state.refresh(); @@ -120,8 +124,12 @@ class _HomeState extends State Icon( DidvanIcons.ai_solid, size: MediaQuery.sizeOf(context).width / 5, + color: Theme.of(context).colorScheme.title, + ), + DidvanText( + 'هوشان', + color: Theme.of(context).colorScheme.title, ), - const DidvanText('هوشان'), const SizedBox( height: 24, ), @@ -569,7 +577,7 @@ class _HomeState extends State Icon( icon, color: enable - ? null + ? Theme.of(context).colorScheme.title : Theme.of(context).colorScheme.disabledText, ), const SizedBox( @@ -580,7 +588,7 @@ class _HomeState extends State DidvanText(text, fontSize: 16, color: enable - ? null + ? Theme.of(context).colorScheme.title : Theme.of(context).colorScheme.disabledText), // if (!enable) Text('در حال توسعه ...') ], diff --git a/lib/views/home/main/main_page.dart b/lib/views/home/main/main_page.dart index 557362f..76de130 100644 --- a/lib/views/home/main/main_page.dart +++ b/lib/views/home/main/main_page.dart @@ -126,7 +126,7 @@ class _MainPageSection extends StatelessWidget { void _moreHandler(BuildContext context) { if (list.link.startsWith('http')) { - launchUrlString( + openInWebView( '${list.link}?accessToken=${RequestService.token}', mode: LaunchMode.inAppWebView, ); diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart index 71d4324..75fc3de 100644 --- a/lib/views/home/main/main_page_state.dart +++ b/lib/views/home/main/main_page_state.dart @@ -99,7 +99,7 @@ class MainPageState extends CoreProvier { return; } if (link.startsWith('http')) { - launchUrlString( + openInWebView( '$link?accessToken=${RequestService.token}', mode: LaunchMode.inAppWebView, ); diff --git a/lib/views/home/main/widgets/banner.dart b/lib/views/home/main/widgets/banner.dart index 201035e..569d9a1 100644 --- a/lib/views/home/main/widgets/banner.dart +++ b/lib/views/home/main/widgets/banner.dart @@ -23,7 +23,7 @@ class MainPageBanner extends StatelessWidget { onTap: () => item.link == null || item.link!.isEmpty ? ActionSheetUtils(context) .openInteractiveViewer(context, item.image, false) - : launchUrlString(item.link!, mode: LaunchMode.inAppWebView), + : openInWebView(item.link!, mode: LaunchMode.inAppWebView), child: SkeletonImage( imageUrl: item.image, ), diff --git a/lib/views/home/new_statistic/new_statistic.dart b/lib/views/home/new_statistic/new_statistic.dart index 6b5647d..41d4199 100644 --- a/lib/views/home/new_statistic/new_statistic.dart +++ b/lib/views/home/new_statistic/new_statistic.dart @@ -138,7 +138,7 @@ class _NewStatisticState extends State { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - physics: const ScrollPhysics(), + physics: const BouncingScrollPhysics(), itemCount: state.contents[id].contents.length, itemBuilder: (context, index) => StatMainCard( id: id, diff --git a/lib/views/home/search/widgets/search_result_item.dart b/lib/views/home/search/widgets/search_result_item.dart index 31adb25..11e35dd 100644 --- a/lib/views/home/search/widgets/search_result_item.dart +++ b/lib/views/home/search/widgets/search_result_item.dart @@ -90,7 +90,7 @@ class SearchResultItem extends StatelessWidget { return; } if (_targetPageRouteName == null && item.link != null) { - launchUrlString( + openInWebView( '${item.link!}?accessToken=${RequestService.token}', mode: LaunchMode.inAppWebView, ); diff --git a/lib/views/home/widgets/categories.dart b/lib/views/home/widgets/categories.dart index 92b4eea..b8639e7 100644 --- a/lib/views/home/widgets/categories.dart +++ b/lib/views/home/widgets/categories.dart @@ -13,7 +13,7 @@ class MainCategories extends StatelessWidget { void _onTap(String link, BuildContext context) { if (link.startsWith('http')) { - launchUrlString( + openInWebView( '$link?accessToken=${RequestService.token}', mode: LaunchMode.inAppWebView, ); 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 1e975c7..34f106c 100644 --- a/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart +++ b/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart @@ -65,7 +65,7 @@ class StudioDetailsWidget extends StatelessWidget { key: ValueKey(state.studio.id), data: state.studio.description, onAnchorTap: (href, _, __) => - launchUrlString(href!), + openInWebView(href!), style: { '*': Style( direction: TextDirection.rtl, diff --git a/lib/views/profile/profile.dart b/lib/views/profile/profile.dart index 731e492..d933cf9 100644 --- a/lib/views/profile/profile.dart +++ b/lib/views/profile/profile.dart @@ -222,7 +222,7 @@ class _ProfilePageState extends State { icon: DidvanIcons.info_circle_regular, title: 'معرفی دیدوان', onTap: () => - launchUrlString('https://didvan.app/#info'), + openInWebView('https://didvan.app/#info'), ), const DidvanDivider(), MenuOption( @@ -338,7 +338,7 @@ class _ProfilePageState extends State { MenuOption( icon: DidvanIcons.alert_regular, title: 'حریم خصوصی', - onTap: () => launchUrlString( + onTap: () => openInWebView( 'https://didvan.app/terms-of-use#privacy', ), ), diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index 41e0de4..0ee87f8 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -310,7 +310,7 @@ class _DidvanPageViewState extends State { ), ); } else { - launchUrlString(href); + openInWebView(href); } }, style: { diff --git a/lib/views/widgets/overview/multitype.dart b/lib/views/widgets/overview/multitype.dart index 2795148..038e741 100644 --- a/lib/views/widgets/overview/multitype.dart +++ b/lib/views/widgets/overview/multitype.dart @@ -105,7 +105,7 @@ class MultitypeOverview extends StatelessWidget { return; } if (_targetPageRouteName == null && item.link != null) { - launchUrlString( + openInWebView( '${item.link!}?accessToken=${RequestService.token}', mode: LaunchMode.inAppWebView, ); diff --git a/pubspec.lock b/pubspec.lock index bc9c460..1dbcbfa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1138,6 +1138,70 @@ 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: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + url: "https://pub.dev" + source: hosted + version: "3.2.0" + 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: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + url: "https://pub.dev" + source: hosted + version: "3.1.2" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 461a21d..ba42aa4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,6 +92,7 @@ dependencies: mime: ^1.0.2 path: any flutter_cache_manager: any + url_launcher: ^6.3.0 dev_dependencies: flutter_test: